UNPKG

@reusable-ui/collapsible

Version:

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

195 lines (136 loc) 6.07 kB
// react: import { // react: default as React, // 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' import { // states: CollapsibleState, CollapsibleApi, } from './collapsible.js' // hooks: // states: //#region lastKnownExpandedSize export interface LastKnownExpandedSizeApi<TElement extends Element = HTMLElement> { setRef : React.Ref<TElement> // setter ref style : React.CSSProperties } export const useLastKnownExpandedSize = <TElement extends Element = HTMLElement>(collapsibleApi: CollapsibleApi<TElement>): LastKnownExpandedSizeApi<TElement> => { // states: const {state} = collapsibleApi; // refs: const [ref, setRef] = useState<TElement|null>(null); // utilities: /** * The last known size of `<ComponentElement>` when the collapsible state is `expanded`. */ let [lastKnownExpandedSize, setLastKnownExpandedSize] = useState<ResizeObserverSize|undefined>(undefined); const updateSize = useEvent((size: ResizeObserverSize): void => { // conditions: if ( lastKnownExpandedSize && (lastKnownExpandedSize.inlineSize === size.inlineSize) && (lastKnownExpandedSize.blockSize === size.blockSize ) ) return; // no change => ignore // updates: setLastKnownExpandedSize( // sync lastKnownExpandedSize = size ); }); const measureExpandedSize = useEvent((): void => { // 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 : ResizeObserverSize = { 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<ResizeObserverCallback>((entries) => { // conditions: if (state !== CollapsibleState.Expanded) return; // only interested of fully_expanded state, ignore the size when fully_collapsed|collapsing|expanding const size : ResizeObserverSize|null|undefined = 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<CollapsibleState>(state); useIsomorphicLayoutEffect(() => { // conditions: if (prevStateRef.current === state) return; // no change => ignore prevStateRef.current = state; // sync if (state !== 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<React.CSSProperties>(() => ({ // 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