wix-style-react
Version:
wix-style-react
138 lines • 5.88 kB
JavaScript
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