react-collapsed
Version:
A React custom-hook for creating flexible and accessible expand/collapse components.
500 lines (486 loc) • 16.9 kB
JavaScript
/**
* react-collapsed v4.2.0
*
* Copyright (c) 2019-2024, Rogin Farrer
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __objRest = (source, exclude) => {
var target = {};
for (var prop in source)
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
target[prop] = source[prop];
if (source != null && __getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(source)) {
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
target[prop] = source[prop];
}
return target;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
useCollapse: () => useCollapse
});
module.exports = __toCommonJS(src_exports);
var import_react5 = require("react");
// src/utils/index.ts
var import_react4 = require("react");
// src/utils/CollapseError.ts
var import_tiny_warning = __toESM(require("tiny-warning"));
var CollapseError = class extends Error {
constructor(message) {
super(`react-collapsed: ${message}`);
}
};
var collapseWarning = (...args) => {
return (0, import_tiny_warning.default)(args[0], `[react-collapsed] -- ${args[1]}`);
};
// src/utils/useEvent.ts
var import_react = require("react");
function useEvent(callback) {
const ref = (0, import_react.useRef)(callback);
(0, import_react.useEffect)(() => {
ref.current = callback;
});
return (0, import_react.useCallback)((...args) => {
var _a;
return (_a = ref.current) == null ? void 0 : _a.call(ref, ...args);
}, []);
}
// src/utils/useControlledState.ts
var import_react2 = require("react");
function useControlledState(value, defaultValue, callback) {
const [state, setState] = (0, import_react2.useState)(defaultValue);
const initiallyControlled = (0, import_react2.useRef)(typeof value !== "undefined");
const effectiveValue = initiallyControlled.current ? value : state;
const cb = useEvent(callback);
const onChange = (0, import_react2.useCallback)(
(update) => {
const setter = update;
const newValue = typeof update === "function" ? setter(effectiveValue) : update;
if (!initiallyControlled.current) {
setState(newValue);
}
cb == null ? void 0 : cb(newValue);
},
[cb, effectiveValue]
);
(0, import_react2.useEffect)(() => {
collapseWarning(
!(initiallyControlled.current && value == null),
"`isExpanded` state is changing from controlled to uncontrolled. useCollapse should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the `isExpanded` prop."
);
collapseWarning(
!(!initiallyControlled.current && value != null),
"`isExpanded` state is changing from uncontrolled to controlled. useCollapse should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the `isExpanded` prop."
);
}, [value]);
return [effectiveValue, onChange];
}
// src/utils/usePrefersReducedMotion.ts
var import_react3 = require("react");
var QUERY = "(prefers-reduced-motion: reduce)";
function usePrefersReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] = (0, import_react3.useState)(false);
(0, import_react3.useEffect)(() => {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
return;
}
const mediaQueryList = window.matchMedia(QUERY);
setPrefersReducedMotion(mediaQueryList.matches);
const listener = (event) => {
setPrefersReducedMotion(event.matches);
};
if (mediaQueryList.addEventListener) {
mediaQueryList.addEventListener("change", listener);
return () => {
mediaQueryList.removeEventListener("change", listener);
};
} else if (mediaQueryList.addListener) {
mediaQueryList.addListener(listener);
return () => {
mediaQueryList.removeListener(listener);
};
}
return void 0;
}, []);
return prefersReducedMotion;
}
// src/utils/useId.ts
var React = __toESM(require("react"));
var __useId = React["useId".toString()] || (() => void 0);
function useReactId() {
const id2 = __useId();
return id2 != null ? id2 : "";
}
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
var serverHandoffComplete = false;
var id = 0;
var genId = () => ++id;
function useUniqueId(idFromProps) {
const initialId = idFromProps || (serverHandoffComplete ? genId() : null);
const [id2, setId] = React.useState(initialId);
useIsomorphicLayoutEffect(() => {
if (id2 === null) {
setId(genId());
}
}, []);
React.useEffect(() => {
if (serverHandoffComplete === false) {
serverHandoffComplete = true;
}
}, []);
return id2 != null ? String(id2) : void 0;
}
function useId(idOverride) {
const reactId = useReactId();
const uniqueId = useUniqueId(idOverride);
if (typeof idOverride === "string") {
return idOverride;
}
if (typeof reactId === "string") {
return reactId;
}
return uniqueId;
}
// src/utils/setAnimationTimeout.ts
function setAnimationTimeout(callback, timeout) {
const startTime = performance.now();
const frame = {};
function call() {
frame.id = requestAnimationFrame((now) => {
if (now - startTime > timeout) {
callback();
} else {
call();
}
});
}
call();
return frame;
}
function clearAnimationTimeout(frame) {
if (frame.id)
cancelAnimationFrame(frame.id);
}
// src/utils/index.ts
function getElementHeight(el) {
if (!(el == null ? void 0 : el.current)) {
collapseWarning(
true,
`Was not able to find a ref to the collapse element via \`getCollapseProps\`. Ensure that the element exposes its \`ref\` prop. If it exposes the ref prop under a different name (like \`innerRef\`), use the \`refKey\` property to change it. Example:
const collapseProps = getCollapseProps({refKey: 'innerRef'})`
);
return 0;
}
return el.current.scrollHeight;
}
function getAutoHeightDuration(height) {
if (!height || typeof height === "string") {
return 0;
}
const constant = height / 36;
return Math.round((4 + 15 * constant ** 0.25 + constant / 5) * 10);
}
function assignRef(ref, value) {
if (ref == null)
return;
if (typeof ref === "function") {
ref(value);
} else {
try {
ref.current = value;
} catch (error) {
throw new CollapseError(`Cannot assign value "${value}" to ref "${ref}"`);
}
}
}
function mergeRefs(...refs) {
if (refs.every((ref) => ref == null)) {
return null;
}
return (node) => {
refs.forEach((ref) => {
assignRef(ref, node);
});
};
}
function usePaddingWarning(element) {
let warn = (el) => {
};
if (true !== "production") {
warn = (el) => {
if (!(el == null ? void 0 : el.current)) {
return;
}
const { paddingTop, paddingBottom } = window.getComputedStyle(el.current);
const hasPadding = paddingTop && paddingTop !== "0px" || paddingBottom && paddingBottom !== "0px";
collapseWarning(
!hasPadding,
`Padding applied to the collapse element will cause the animation to break and not perform as expected. To fix, apply equivalent padding to the direct descendent of the collapse element. Example:
Before: <div {...getCollapseProps({style: {padding: 10}})}>{children}</div>
After: <div {...getCollapseProps()}>
<div style={{padding: 10}}>
{children}
</div>
</div>`
);
};
}
(0, import_react4.useEffect)(() => {
warn(element);
}, [element]);
}
// src/index.ts
var useLayoutEffect2 = typeof window === "undefined" ? import_react5.useEffect : import_react5.useLayoutEffect;
function useCollapse(_a = {}) {
var _b = _a, {
duration,
easing = "cubic-bezier(0.4, 0, 0.2, 1)",
onTransitionStateChange: propOnTransitionStateChange = () => {
},
isExpanded: configIsExpanded,
defaultExpanded = false,
hasDisabledAnimation,
id: id2
} = _b, initialConfig = __objRest(_b, [
"duration",
"easing",
"onTransitionStateChange",
"isExpanded",
"defaultExpanded",
"hasDisabledAnimation",
"id"
]);
const onTransitionStateChange = useEvent(propOnTransitionStateChange);
const uniqueId = useId(id2 ? `${id2}` : void 0);
const [isExpanded, setExpanded] = useControlledState(
configIsExpanded,
defaultExpanded
);
const prevExpanded = (0, import_react5.useRef)(isExpanded);
const [isAnimating, setIsAnimating] = (0, import_react5.useState)(false);
const prefersReducedMotion = usePrefersReducedMotion();
const disableAnimation = hasDisabledAnimation != null ? hasDisabledAnimation : prefersReducedMotion;
const frameId = (0, import_react5.useRef)();
const endFrameId = (0, import_react5.useRef)();
const collapseElRef = (0, import_react5.useRef)(null);
const [toggleEl, setToggleEl] = (0, import_react5.useState)(null);
usePaddingWarning(collapseElRef);
const collapsedHeight = `${initialConfig.collapsedHeight || 0}px`;
function setStyles(newStyles) {
if (!collapseElRef.current)
return;
const target = collapseElRef.current;
for (const property in newStyles) {
const value = newStyles[property];
if (value) {
target.style[property] = value;
} else {
target.style.removeProperty(property);
}
}
}
useLayoutEffect2(() => {
const collapse = collapseElRef.current;
if (!collapse)
return;
if (isExpanded === prevExpanded.current)
return;
prevExpanded.current = isExpanded;
function getDuration(height) {
if (disableAnimation) {
return 0;
}
return duration != null ? duration : getAutoHeightDuration(height);
}
const getTransitionStyles = (height) => `height ${getDuration(height)}ms ${easing}`;
const setTransitionEndTimeout = (duration2) => {
function endTransition() {
if (isExpanded) {
setStyles({
height: "",
overflow: "",
transition: "",
display: ""
});
onTransitionStateChange("expandEnd");
} else {
setStyles({ transition: "" });
onTransitionStateChange("collapseEnd");
}
setIsAnimating(false);
}
if (endFrameId.current) {
clearAnimationTimeout(endFrameId.current);
}
endFrameId.current = setAnimationTimeout(endTransition, duration2);
};
setIsAnimating(true);
if (isExpanded) {
frameId.current = requestAnimationFrame(() => {
onTransitionStateChange("expandStart");
setStyles({
display: "block",
overflow: "hidden",
height: collapsedHeight
});
frameId.current = requestAnimationFrame(() => {
onTransitionStateChange("expanding");
const height = getElementHeight(collapseElRef);
setTransitionEndTimeout(getDuration(height));
if (collapseElRef.current) {
collapseElRef.current.style.transition = getTransitionStyles(height);
collapseElRef.current.style.height = `${height}px`;
}
});
});
} else {
frameId.current = requestAnimationFrame(() => {
onTransitionStateChange("collapseStart");
const height = getElementHeight(collapseElRef);
setTransitionEndTimeout(getDuration(height));
setStyles({
transition: getTransitionStyles(height),
height: `${height}px`
});
frameId.current = requestAnimationFrame(() => {
onTransitionStateChange("collapsing");
setStyles({
height: collapsedHeight,
overflow: "hidden"
});
});
});
}
return () => {
if (frameId.current)
cancelAnimationFrame(frameId.current);
if (endFrameId.current)
clearAnimationTimeout(endFrameId.current);
};
}, [
isExpanded,
collapsedHeight,
disableAnimation,
duration,
easing,
onTransitionStateChange
]);
return {
isExpanded,
setExpanded,
getToggleProps(args) {
const _a2 = __spreadValues({
refKey: "ref",
onClick() {
},
disabled: false
}, args), { disabled, onClick, refKey } = _a2, rest = __objRest(_a2, ["disabled", "onClick", "refKey"]);
const isButton = toggleEl ? toggleEl.tagName === "BUTTON" : void 0;
const theirRef = args == null ? void 0 : args[refKey || "ref"];
const props = {
id: `react-collapsed-toggle-${uniqueId}`,
"aria-controls": `react-collapsed-panel-${uniqueId}`,
"aria-expanded": isExpanded,
onClick(evt) {
if (disabled)
return;
onClick == null ? void 0 : onClick(evt);
setExpanded((n) => !n);
},
[refKey || "ref"]: mergeRefs(theirRef, setToggleEl)
};
const buttonProps = {
type: "button",
disabled: disabled ? true : void 0
};
const fakeButtonProps = {
"aria-disabled": disabled ? true : void 0,
role: "button",
tabIndex: disabled ? -1 : 0
};
if (isButton === false) {
return __spreadValues(__spreadValues(__spreadValues({}, props), fakeButtonProps), rest);
} else if (isButton === true) {
return __spreadValues(__spreadValues(__spreadValues({}, props), buttonProps), rest);
} else {
return __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, props), buttonProps), fakeButtonProps), rest);
}
},
getCollapseProps(args) {
const { style, refKey } = __spreadValues({ refKey: "ref", style: {} }, args);
const theirRef = args == null ? void 0 : args[refKey || "ref"];
return __spreadProps(__spreadValues({
id: `react-collapsed-panel-${uniqueId}`,
"aria-hidden": !isExpanded,
"aria-labelledby": `react-collapsed-toggle-${uniqueId}`,
role: "region"
}, args), {
[refKey || "ref"]: mergeRefs(collapseElRef, theirRef),
style: __spreadValues(__spreadValues({
boxSizing: "border-box"
}, !isAnimating && !isExpanded ? {
// collapsed and not animating
display: collapsedHeight === "0px" ? "none" : "block",
height: collapsedHeight,
overflow: "hidden"
} : {}), style)
});
}
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
useCollapse
});
//# sourceMappingURL=index.cjs.dev.js.map