fanyucomponents
Version:
一款以 純邏輯為核心、無樣式綁定 的 React 元件套件
116 lines (115 loc) • 4.39 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import { useLayoutEffect, useRef, useState, useCallback } from "react";
export const Collapse = ({ as, state: show, style, children, ...rest }) => {
const Tag = as !== null && as !== void 0 ? as : "div";
const innerRef = useRef(null);
const [collapsed, setCollapsed] = useState(!show);
const transitionTimerRef = useRef(null);
const resizeObserverRef = useRef(null);
// 清理函數
const cleanup = useCallback(() => {
if (transitionTimerRef.current) {
clearTimeout(transitionTimerRef.current);
transitionTimerRef.current = null;
}
if (resizeObserverRef.current) {
resizeObserverRef.current.disconnect();
resizeObserverRef.current = null;
}
}, []);
// 處理過渡結束
const handleTransitionEnd = useCallback((e) => {
if (e.propertyName !== "height")
return;
const el = innerRef.current;
if (!el)
return;
if (show) {
el.style.height = "auto";
// 監聽內容變化以保持正確高度
if (!resizeObserverRef.current) {
resizeObserverRef.current = new ResizeObserver(() => {
if (el.style.height === "auto") {
el.style.height = "auto"; // 確保保持 auto 狀態
}
});
resizeObserverRef.current.observe(el);
}
}
else {
// 收起動畫完成後才設置 collapsed 狀態
setCollapsed(true);
}
// 清理定時器
if (transitionTimerRef.current) {
clearTimeout(transitionTimerRef.current);
transitionTimerRef.current = null;
}
}, [show]);
// 設置高度並啟動動畫
const setElementHeight = useCallback((targetHeight) => {
const el = innerRef.current;
if (!el)
return;
el.style.height = targetHeight;
}, []);
// 啟動備用定時器(防止 transitionend 未觸發)
const startFallbackTimer = useCallback(() => {
const el = innerRef.current;
if (!el)
return;
const transitionDuration = parseInt(el.style.transitionDuration || "500");
transitionTimerRef.current = setTimeout(() => {
if (show) {
el.style.height = "auto";
if (!resizeObserverRef.current) {
resizeObserverRef.current = new ResizeObserver(() => {
if (el.style.height === "auto") {
el.style.height = "auto";
}
});
resizeObserverRef.current.observe(el);
}
}
else {
// 收起動畫完成後才設置 collapsed 狀態
setCollapsed(true);
}
}, transitionDuration + 50); // 額外 50ms 緩衝時間
}, [show]);
useLayoutEffect(() => {
const el = innerRef.current;
if (!el)
return;
// 先清理之前的狀態
cleanup();
if (show) {
setCollapsed(false);
// 從收合狀態展開
el.style.height = "0px";
// 強制重繪,確保起始狀態生效
void el.offsetHeight;
setElementHeight(`${el.scrollHeight}px`);
}
else {
// 從展開狀態收合 - 不立即設置 collapsed,讓動畫執行
el.style.height = `${el.scrollHeight}px`;
// 強制重繪,確保起始狀態生效
void el.offsetHeight;
setElementHeight("0px");
// 不在這裡設置 setCollapsed(true),讓動畫完成後再設置
}
el.addEventListener("transitionend", handleTransitionEnd);
startFallbackTimer();
return () => {
cleanup();
el.removeEventListener("transitionend", handleTransitionEnd);
};
}, [show, cleanup, handleTransitionEnd, setElementHeight, startFallbackTimer]);
return (_jsx(Tag, { ref: innerRef, style: {
overflow: "hidden",
maxHeight: collapsed ? "0px" : undefined,
...style,
}, ...rest, children: children }));
};
Collapse.displayName = "Collapse";