UNPKG

@reusable-ui/collapsible

Version:

A capability of UI to expand/reduce its size or toggle the visibility.

121 lines (120 loc) 4.74 kB
// react: import { // hooks: useRef, useState, useMemo, } from 'react'; // reusable-ui utilities: import { // hooks: useIsomorphicLayoutEffect, useEvent, } from '@reusable-ui/hooks'; // react helper hooks // internals: import { // features: usesLastKnownExpandedSize, } from './features/lastKnownExpandedSize.js'; export const useLastKnownExpandedSize = (collapsibleApi) => { // states: const { state } = collapsibleApi; // refs: const [ref, setRef] = useState(null); // utilities: /** * The last known size of `<ComponentElement>` when the collapsible state is `expanded`. */ let [lastKnownExpandedSize, setLastKnownExpandedSize] = useState(undefined); const updateSize = useEvent((size) => { // conditions: if (lastKnownExpandedSize && (lastKnownExpandedSize.inlineSize === size.inlineSize) && (lastKnownExpandedSize.blockSize === size.blockSize)) return; // no change => ignore // updates: setLastKnownExpandedSize(// sync lastKnownExpandedSize = size); }); const measureExpandedSize = useEvent(() => { // conditions: if (!ref) return; // the ref is not ready yet // measures: const hasInitialExpandingClass = ref.classList.contains('expanding'); const hasInitialExpandedClass = ref.classList.contains('expanded'); try { if (hasInitialExpandingClass) { ref.classList.remove('expanding'); // a *hack* temporary hide `.expanding` class } // if if (!hasInitialExpandedClass) { ref.classList.add('expanded'); // a *hack* temporary add `.expanded` class } // if const style = getComputedStyle(ref); if (style.display !== 'none') { // the <ComponentElement> is rendered const size = { inlineSize: Number.parseFloat(style.inlineSize), blockSize: Number.parseFloat(style.blockSize), }; updateSize(size); } // if } finally { if (hasInitialExpandingClass) ref.classList.add('expanding'); // a *hack* restore `.expanding` class if (!hasInitialExpandedClass) ref.classList.remove('expanded'); // a *hack* remove `.expanded` class } // try }); // handlers: const observerHandleResize = useEvent((entries) => { // conditions: if (state !== 3 /* CollapsibleState.Expanded */) return; // only interested of fully_expanded state, ignore the size when fully_collapsed|collapsing|expanding const size = entries[0]?.borderBoxSize?.[0]; if (!size) return; // actions: updateSize(size); }); // effects: // force to perform size measurement prior to expanding event (before the browser has chance to perform animation): const prevStateRef = useRef(state); useIsomorphicLayoutEffect(() => { // conditions: if (prevStateRef.current === state) return; // no change => ignore prevStateRef.current = state; // sync if (state !== 2 /* CollapsibleState.Expanding */) return; // only interested of expanding event // actions: measureExpandedSize(); }, [state]); // while the <ComponentElement> is still on expanded state, continously monitors the size of the inspecting element: useIsomorphicLayoutEffect(() => { // conditions: if (!ref) return; // the ref is not set => ignore // setups: const observer = new ResizeObserver(observerHandleResize); observer.observe(ref, { box: 'border-box' }); // cleanups: return () => { observer.disconnect(); }; }, [ref]); // features: const { lastKnownExpandedSizeVars } = usesLastKnownExpandedSize(); // styles: const style = useMemo(() => ({ // values: [lastKnownExpandedSizeVars.inlineSize .slice(4, -1) // fix: var(--customProp) => --customProp ]: (lastKnownExpandedSize?.inlineSize !== undefined) ? `${lastKnownExpandedSize.inlineSize}px` : undefined, [lastKnownExpandedSizeVars.blockSize .slice(4, -1) // fix: var(--customProp) => --customProp ]: (lastKnownExpandedSize?.blockSize !== undefined) ? `${lastKnownExpandedSize.blockSize}px` : undefined, }), [lastKnownExpandedSizeVars.inlineSize, lastKnownExpandedSizeVars.blockSize, lastKnownExpandedSize]); // api: return { setRef, style, }; }; //#endregion lastKnownExpandedSize