mirror of
https://github.com/aphrody-labs/aphrody-m3-react.git
synced 2026-10-13 20:07:22 +00:00
84 lines
2.8 KiB
TypeScript
84 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import * as React from "react";
|
|
|
|
import { EASING } from "./create-transition.js";
|
|
import { BaseTransitionProps, useTransitionProps } from "./use-transition-state.js";
|
|
|
|
export interface CollapseProps extends BaseTransitionProps {
|
|
/** The axis along which to collapse. Defaults to `vertical` (height). */
|
|
orientation?: "vertical" | "horizontal";
|
|
/**
|
|
* The size to keep visible while collapsed (e.g. `'0px'` or `'40px'` for a
|
|
* peeking header). Defaults to `'0px'`.
|
|
*/
|
|
collapsedSize?: string;
|
|
}
|
|
|
|
/**
|
|
* Collapses its child along one axis by animating its measured size between
|
|
* `collapsedSize` and its natural extent. The MUI `Collapse` equivalent.
|
|
*
|
|
* The child is wrapped in a clipping container so that overflow is hidden
|
|
* during the animation. The natural size is measured from a live ref each
|
|
* commit, so dynamic content is handled.
|
|
*/
|
|
export function Collapse(props: CollapseProps): React.ReactElement | null {
|
|
const { orientation = "vertical", collapsedSize = "0px", children } = props;
|
|
const { mounted, phase, visible, durationMs } = useTransitionProps(props, 300, 250);
|
|
|
|
const innerRef = React.useRef<HTMLDivElement>(null);
|
|
const [naturalSize, setNaturalSize] = React.useState<number>(0);
|
|
const isHorizontal = orientation === "horizontal";
|
|
|
|
// Measure the child's natural extent before paint whenever it should be
|
|
// shown, so the enter animation has a concrete target size.
|
|
React.useLayoutEffect(() => {
|
|
const node = innerRef.current;
|
|
if (!node) {
|
|
return;
|
|
}
|
|
const measured = isHorizontal ? node.scrollWidth : node.scrollHeight;
|
|
setNaturalSize(measured);
|
|
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- phase and children are re-measurement triggers: the natural size changes with them although the body does not read them
|
|
}, [isHorizontal, phase, children]);
|
|
|
|
if (!mounted) {
|
|
return null;
|
|
}
|
|
|
|
const sizeProp = isHorizontal ? "width" : "height";
|
|
// `entered` lets the content size itself naturally (responsive); the other
|
|
// phases use an explicit pixel size so the transition can interpolate.
|
|
const targetSize = phase === "entered" ? "auto" : visible ? `${naturalSize}px` : collapsedSize;
|
|
|
|
const containerStyle: React.CSSProperties = {
|
|
[sizeProp]: targetSize,
|
|
overflow: "hidden",
|
|
transition: `${sizeProp} ${durationMs}ms ${EASING.standard}`,
|
|
willChange: sizeProp,
|
|
};
|
|
|
|
const innerStyle: React.CSSProperties = isHorizontal
|
|
? { display: "inline-block", height: "100%" }
|
|
: {};
|
|
|
|
return (
|
|
<div
|
|
style={containerStyle}
|
|
inert={!visible && collapsedSize === "0px"}
|
|
aria-hidden={!visible && collapsedSize === "0px"}
|
|
>
|
|
<div ref={innerRef} style={innerStyle}>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|