diff --git a/apis/nucleus/src/components/Cell.jsx b/apis/nucleus/src/components/Cell.jsx index 2d93ee331..712f922b8 100644 --- a/apis/nucleus/src/components/Cell.jsx +++ b/apis/nucleus/src/components/Cell.jsx @@ -10,6 +10,8 @@ import Footer from './Footer'; import Supernova from './Supernova'; import Placeholder from './Placeholder'; +import useRect from '../hooks/useRect'; + const useStyles = makeStyles(() => ({ content: { position: 'relative', @@ -67,7 +69,7 @@ const Content = React.forwardRef(({ children, showError }, ref) => { export default function Cell({ api, onInitial }) { const [, setChanged] = useState(0); - + const [contentRef, contentRect, contentNode] = useRect(); const theme = useTheme(); useEffect(() => { const onChanged = () => setChanged(Date.now()); @@ -87,7 +89,6 @@ export default function Cell({ api, onInitial }) { const SN = showRequirements(objectProps.sn, objectProps.layout) ? Requirements : Supernova; const Comp = !objectProps.sn ? Placeholder : SN; const showError = objectProps.error || objectProps.dataErrors.length; - return ( @@ -97,9 +98,9 @@ export default function Cell({ api, onInitial }) { - + {showError ? ( - + ) : ( )} diff --git a/apis/nucleus/src/components/Supernova.jsx b/apis/nucleus/src/components/Supernova.jsx index cba46065d..c2b8ae7c3 100644 --- a/apis/nucleus/src/components/Supernova.jsx +++ b/apis/nucleus/src/components/Supernova.jsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import useRect from '../hooks/useRect'; -const constrainElement = (el, d) => { +const setStyle = (el, d) => { /* eslint-disable no-param-reassign */ if (d) { el.style.width = `${d.width}px`; @@ -18,211 +19,95 @@ const constrainElement = (el, d) => { /* eslint-enable no-param-reassign */ }; -const scheduleRender = (props, prev, initial, contentElement) => { - if (prev) { - prev.reject(); +const constrainElement = ({ snNode, parentNode, sn, snContext, layout }) => { + const parentRect = parentNode.getBoundingClientRect(); + const r = + typeof snContext.logicalSize === 'function' ? snContext.logicalSize(layout, sn) : sn.logicalSize({ layout }); + const logicalSize = r || undefined; + let width; + let height; + let left = 0; + let top = 0; + if (r) { + const parentRatio = parentRect.width / parentRect.height; + const rRatio = r.width / r.height; + + if (parentRatio > rRatio) { + // parent is wider -> limit height + ({ height } = parentRect); + width = height * rRatio; + left = (parentRect.width - width) / 2; + top = 0; + } else { + ({ width } = parentRect); + height = width / rRatio; + left = 0; + top = (parentRect.height - height) / 2; + } } - const prom = {}; - - const p = new Promise(resolve => { - const timeout = setTimeout(() => { - const parentRect = contentElement.parentElement.parentElement.getBoundingClientRect(); - const r = - typeof props.snContext.logicalSize === 'function' - ? props.snContext.logicalSize(props.layout, props.sn) - : props.sn.logicalSize({ layout: props.layout }); - const logicalSize = r || undefined; - if (r) { - // const rect = that.element.getBoundingClientRect(); - - const parentRatio = parentRect.width / parentRect.height; - const rRatio = r.width / r.height; - - let width; - let height; - let left = 0; - let top = 0; - - if (parentRatio > rRatio) { - // parent is wider -> limit height - ({ height } = parentRect); - width = height * rRatio; - left = (parentRect.width - width) / 2; - top = 0; - } else { - ({ width } = parentRect); - height = width / rRatio; - left = 0; - top = (parentRect.height - height) / 2; - } - - constrainElement(contentElement, { - top, - left, - width, - height, - }); - } else { - constrainElement(contentElement); - } - - initial.mount(); - - Promise.resolve( - props.sn.component.render({ - layout: props.layout, - options: props.snOptions || {}, - context: { - permissions: (props.snContext || {}).permissions, - theme: (props.snContext || {}).theme, - rtl: (props.snContext || {}).rtl, - localeInfo: (props.snContext || {}).localeInfo, - logicalSize, - }, - }) - ) - .then(() => { - initial.rendered(); - // props.sn.component.didUpdate(); // TODO - should check if component is in update stage - }) - .then(resolve); - }, 0); - - prom.reject = () => { - clearTimeout(timeout); - resolve(); - }; - }); - - prom.then = p.then; - - return prom; + setStyle(snNode, r ? { left, top, width, height } : undefined); + return logicalSize; }; -class Supernova extends React.Component { - constructor(props) { - super(props); - this.initial = { - mount: () => { - this.props.sn.component.created({ - options: this.props.snOptions, - context: this.props.snContext, - }); - this.props.sn.component.mounted(this.contentElement); - this.initial.mount = () => {}; - }, - rendered: () => { - if (this.props.snOptions && typeof this.props.snOptions.onInitialRender === 'function') { - this.props.snOptions.onInitialRender.call(null); - } - this.initial.rendered = () => {}; +const Supernova = ({ sn, snOptions: options, snContext, layout, parentNode }) => { + const { component } = sn; + const style = { + position: 'absolute', + top: 0, + bottom: 0, + right: 0, + left: 0, + }; + const [renderCnt, setRenderCnt] = useState(0); + const [snRef, snRect, snNode] = useRect(); + const [logicalSize, setLogicalSize] = useState({ width: 0, height: 0 }); + + const render = () => + component.render({ + layout, + options, + context: { + permissions: (snContext || {}).permissions, + theme: (snContext || {}).theme, + rtl: (snContext || {}).rtl, + localeInfo: (snContext || {}).localeInfo, + logicalSize, }, + }); + + // Mount / Unmount / ThemeChanged + useEffect(() => { + if (!snNode) return undefined; + setLogicalSize(constrainElement({ snNode, parentNode, sn, snContext, layout })); + component.created({ options, snContext }); + component.mounted(snNode); + snContext.theme.on('changed', render); + return () => { + component.willUnmount(); + snContext.theme.removeListener('changed', render); }; - } + }, [snNode]); - componentDidMount() { - let resizeObserver; - - this.dimensions = this.element.getBoundingClientRect(); - - if (typeof ResizeObserver !== 'undefined') { - resizeObserver = new ResizeObserver(() => { - const dims = this.element.getBoundingClientRect(); - if (dims.width !== this.dimensions.width || dims.height !== this.dimensions.height) { - this.dimensions = dims; - this.setState({}); - } - }); - resizeObserver.observe(this.element); - } - - const onThemeChanged = () => { - this.setState({}); - }; - - this.props.snContext.theme.on('changed', onThemeChanged); - this.theme = this.props.snContext.theme; - - this.onUnmount = () => { - this.onUnmount = null; - this.props.snContext.theme.removeListener('changed', onThemeChanged); - if (resizeObserver) { - resizeObserver.unobserve(this.element); - resizeObserver.disconnect(); - resizeObserver = null; + // Render + useEffect(() => { + if (!snRect) return undefined; + if (renderCnt === 0) { + if (typeof options.onInitialRender === 'function') { + options.onInitialRender.call(null); } - - this.props.sn.component.willUnmount(); - if (this.next) { - this.next.reject(); - } - }; - - this.next = scheduleRender( - { - snOptions: this.props.snOptions, - snContext: this.props.snContext, - sn: this.props.sn, - layout: this.props.layout, - }, - this.next, - this.initial, - this.contentElement - ); - } - - shouldComponentUpdate(nextProps) { - const update = - nextProps.sn && - !(nextProps.layout && nextProps.layout.qSelectionInfo && nextProps.layout.qSelectionInfo.qInSelections); - if (!update) { - return false; + render(); + setRenderCnt(renderCnt + 1); + } else { + // Debounce render + const handle = setTimeout(() => { + render(); + setRenderCnt(renderCnt + 1); + }, 100); + return () => clearTimeout(handle); } - - return true; - - // const should = nextProps.sn.component.shouldUpdate({ - // layout: nextProps.layout, - // options: {}, - // }); - - // return should; - } - - componentDidUpdate() { - this.next = scheduleRender(this.props, this.next, this.initial, this.contentElement); - } - - componentWillUnmount() { - this.onUnmount(); - } - - render() { - const style = { - position: 'absolute', - top: 0, - bottom: 0, - right: 0, - left: 0, - }; - return ( -
{ - this.element = element; - }} - > -
{ - this.contentElement = element; - }} - /> -
- ); - } -} + return undefined; + }, [snRect, layout]); + return
; +}; export default Supernova; diff --git a/apis/nucleus/src/hooks/useRect.js b/apis/nucleus/src/hooks/useRect.js new file mode 100644 index 000000000..cc0dc6ef7 --- /dev/null +++ b/apis/nucleus/src/hooks/useRect.js @@ -0,0 +1,33 @@ +import { useState, useCallback, useLayoutEffect } from 'react'; + +export default function useRect() { + const [node, setNode] = useState(); + const [rect, setRect] = useState(); + const callbackRef = useCallback(ref => { + if (!ref) { + return; + } + setNode(ref); + }, []); + const handleResize = () => { + const { left, top, width, height } = node.getBoundingClientRect(); + setRect({ left, top, width, height }); + }; + useLayoutEffect(() => { + if (!node) return undefined; + if (typeof ResizeObserver === 'function') { + let resizeObserver = new ResizeObserver(handleResize); + resizeObserver.observe(node); + return () => { + resizeObserver.unobserve(node); + resizeObserver.disconnect(node); + resizeObserver = null; + }; + } + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [node]); + return [callbackRef, rect, node]; +} diff --git a/rollup.config.js b/rollup.config.js index e95bb857f..9fbfb5bf9 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -109,6 +109,7 @@ const config = isEsm => { react: [ 'useState', 'useEffect', + 'useLayoutEffect', 'useRef', 'useContext', 'useCallback',