feat: animate mobile overlay panel entrance
Slide the overlay panel up from the bottom with a scrim fade on open, matching the surface sheet entrance.
This commit is contained in:
@@ -16,6 +16,9 @@ interface MobileOverlayPanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OVERLAY_ROOT_ID = 'mobile-overlay-root';
|
const OVERLAY_ROOT_ID = 'mobile-overlay-root';
|
||||||
|
// Entrance animation: classic slide up from the bottom + scrim fade.
|
||||||
|
const ENTER_DELAY_MS = 16;
|
||||||
|
const ENTER_DURATION_MS = 200;
|
||||||
|
|
||||||
const ensureOverlayRoot = () => {
|
const ensureOverlayRoot = () => {
|
||||||
if (typeof document === 'undefined') return null;
|
if (typeof document === 'undefined') return null;
|
||||||
@@ -39,11 +42,22 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
|
|||||||
renderHeader,
|
renderHeader,
|
||||||
}) => {
|
}) => {
|
||||||
const overlayRootRef = React.useRef<HTMLElement | null>(null);
|
const overlayRootRef = React.useRef<HTMLElement | null>(null);
|
||||||
|
const [entered, setEntered] = React.useState(false);
|
||||||
|
|
||||||
if (typeof document !== 'undefined' && !overlayRootRef.current) {
|
if (typeof document !== 'undefined' && !overlayRootRef.current) {
|
||||||
overlayRootRef.current = ensureOverlayRoot();
|
overlayRootRef.current = ensureOverlayRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replay the enter transition on each open (rise + scrim fade).
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setEntered(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = window.setTimeout(() => setEntered(true), ENTER_DELAY_MS);
|
||||||
|
return () => window.clearTimeout(id);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
return;
|
return;
|
||||||
@@ -70,7 +84,10 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
|
|||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-[60] flex flex-col bg-[rgb(0_0_0_/_0.45)]"
|
className={cn(
|
||||||
|
'fixed inset-0 z-[60] flex flex-col bg-[rgb(0_0_0_/_0.45)] transition-opacity duration-200 ease-out',
|
||||||
|
entered ? 'opacity-100' : 'opacity-0',
|
||||||
|
)}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@@ -81,6 +98,10 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
|
|||||||
'mx-auto max-w-lg',
|
'mx-auto max-w-lg',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
style={{
|
||||||
|
transform: entered ? 'none' : 'translateY(100%)',
|
||||||
|
transition: `transform ${ENTER_DURATION_MS}ms cubic-bezier(0.32, 0.72, 0, 1)`,
|
||||||
|
}}
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user