@reusable-ui/collapsible
Version:
A capability of UI to expand/reduce its size or toggle the visibility.
195 lines (136 loc) • 6.07 kB
text/typescript
// 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