@reusable-ui/collapsible
Version:
A capability of UI to expand/reduce its size or toggle the visibility.
121 lines (120 loc) • 4.74 kB
JavaScript
// 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