UNPKG

wix-style-react

Version:
138 lines 5.88 kB
import { useState, useRef } from 'react'; import { noop, callAll, getElementHeight, getAutoHeightDuration, mergeRefs, useUniqueId, useEffectAfterMount, useControlledState, } from './utils'; const easeInOut = 'cubic-bezier(0.4, 0, 0.2, 1)'; export default function useCollapse({ duration, easing = easeInOut, collapseStyles = {}, expandStyles = {}, onExpandStart = noop, onExpandEnd = noop, onCollapseStart = noop, onCollapseEnd = noop, isExpanded: configIsExpanded, defaultExpanded = false, hasDisabledAnimation = false, ...initialConfig } = {}) { const [isExpanded, setExpanded] = useControlledState(configIsExpanded, defaultExpanded); const uniqueId = useUniqueId(); const el = useRef(null); const collapsedHeight = `${initialConfig.collapsedHeight || 0}px`; const collapsedStyles = { display: collapsedHeight === '0px' ? 'none' : 'block', height: collapsedHeight, overflow: 'hidden', }; const [exitAnimationEnded, setExitAnimationEnded] = useState(!configIsExpanded); const [styles, setStylesRaw] = useState(isExpanded ? {} : collapsedStyles); const setStyles = (newStyles) => { // We rely on reading information from layout // at arbitrary times, so ensure all style changes // happen before we might attempt to read them. setStylesRaw(newStyles); }; const mergeStyles = (newStyles) => { setStyles(oldStyles => ({ ...oldStyles, ...newStyles })); }; function getTransitionStyles(height) { if (hasDisabledAnimation) { return {}; } const _duration = duration || getAutoHeightDuration(height); return { transition: `height ${_duration}ms ${easing}`, }; } useEffectAfterMount(() => { let animationFrameTimer; let innerAnimationFrameTimer; if (isExpanded) { setExitAnimationEnded(false); animationFrameTimer = window.requestAnimationFrame(() => { onExpandStart(); mergeStyles({ ...expandStyles, willChange: 'height', display: 'block', overflow: 'hidden', }); innerAnimationFrameTimer = window.requestAnimationFrame(() => { const height = getElementHeight(el); mergeStyles({ ...getTransitionStyles(height), height, }); }); }); } else { animationFrameTimer = window.requestAnimationFrame(() => { onCollapseStart(); const height = getElementHeight(el); mergeStyles({ ...collapseStyles, ...getTransitionStyles(height), willChange: 'height', height, }); innerAnimationFrameTimer = window.requestAnimationFrame(() => { mergeStyles({ height: collapsedHeight, overflow: 'hidden', }); }); }); } return () => { window.cancelAnimationFrame(animationFrameTimer); window.cancelAnimationFrame(innerAnimationFrameTimer); }; }, [isExpanded, collapsedHeight]); const handleTransitionEnd = (e) => { // Sometimes onTransitionEnd is triggered by another transition, // such as a nested collapse panel transitioning. But we only // want to handle this if this component's element is transitioning if (e.target !== el.current || e.propertyName !== 'height') { return; } // The height comparisons below are a final check before // completing the transition // Sometimes this callback is run even though we've already begun // transitioning the other direction // The conditions give us the opportunity to bail out, // which will prevent the collapsed content from flashing on the screen if (isExpanded) { const height = getElementHeight(el); // If the height at the end of the transition // matches the height we're animating to, if (height === styles.height) { setStyles({}); } else { // If the heights don't match, this could be due the height // of the content changing mid-transition mergeStyles({ height }); } onExpandEnd(); // If the height we should be animating to matches the collapsed height, // it's safe to apply the collapsed overrides } else if (styles.height === collapsedHeight) { setStyles(collapsedStyles); setExitAnimationEnded(true); onCollapseEnd(); } }; function getCollapseProps({ style = {}, onTransitionEnd = noop, refKey = 'ref', ...rest } = {}) { const theirRef = rest[refKey]; return { id: `react-collapsed-panel-${uniqueId}`, 'aria-hidden': !isExpanded, ...rest, [refKey]: mergeRefs(el, theirRef), onTransitionEnd: callAll(handleTransitionEnd, onTransitionEnd), style: { boxSizing: 'border-box', // additional styles passed, e.g. getCollapseProps({style: {}}) ...style, // style overrides from state ...styles, }, }; } return { getCollapseProps, isExpanded, setExpanded, exitAnimationEnded: process.env.NODE_ENV === 'test' ? !isExpanded : exitAnimationEnded, }; } //# sourceMappingURL=useCollapse.js.map