From 539b90a3896bde7606f132920de2cb901dec8b8c Mon Sep 17 00:00:00 2001 From: Andrey Morozov Date: Fri, 6 Dec 2024 15:55:18 +0300 Subject: [PATCH] feat(Popup): add transition complete events transition --- src/components/Popup/Popup.scss | 3 +- src/components/Popup/Popup.tsx | 30 ++++++++++++++++++-- src/hooks/private/index.ts | 5 ++-- src/hooks/private/usePrevious/README.md | 13 +++++++++ src/hooks/private/usePrevious/index.ts | 1 + src/hooks/private/usePrevious/usePrevious.ts | 11 +++++++ 6 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 src/hooks/private/usePrevious/README.md create mode 100644 src/hooks/private/usePrevious/index.ts create mode 100644 src/hooks/private/usePrevious/usePrevious.ts diff --git a/src/components/Popup/Popup.scss b/src/components/Popup/Popup.scss index b46bda18b..ab1eb21c0 100644 --- a/src/components/Popup/Popup.scss +++ b/src/components/Popup/Popup.scss @@ -7,6 +7,7 @@ $arrow-offset: 9px; $arrow-border: 5px; $arrow-circle-width: 28px; $arrow-circle-height: 30px; +$transition-duration: 100ms; $transition-distance: 10px; #{$block} { @@ -42,7 +43,7 @@ $transition-distance: 10px; @at-root [data-floating-ui-status='open'] &, [data-floating-ui-status='close'] & { - transition-duration: 100ms; + transition-duration: $transition-duration; } @at-root [data-floating-ui-status='initial'] &, diff --git a/src/components/Popup/Popup.tsx b/src/components/Popup/Popup.tsx index 4c45da490..335f3a05b 100644 --- a/src/components/Popup/Popup.tsx +++ b/src/components/Popup/Popup.tsx @@ -28,6 +28,7 @@ import type { } from '@floating-ui/react'; import {useForkRef} from '../../hooks'; +import {usePrevious} from '../../hooks/private'; import {Portal} from '../Portal'; import type {AriaLabelingProps, DOMProps, QAProps} from '../types'; import {block} from '../utils/cn'; @@ -116,8 +117,12 @@ export interface PopupProps extends DOMProps, AriaLabelingProps, QAProps { role?: UseRoleProps['role']; /** HTML `id` attribute */ id?: string; - // CSS property `z-index` + /** CSS property `z-index` */ zIndex?: number; + /** Callback called when `Popup` is opened and "in" transition is completed */ + onTransitionInComplete?: () => void; + /** Callback called when `Popup` is closed and "out" transition is completed */ + onTransitionOutComplete?: () => void; } const b = block('popup'); @@ -155,6 +160,8 @@ export function Popup({ id, role: roleProp, zIndex = 1000, + onTransitionInComplete, + onTransitionOutComplete, ...restProps }: PopupProps) { const contentRef = React.useRef(null); @@ -240,6 +247,7 @@ export function Popup({ }, [setGetAnchorProps, getReferenceProps]); const {isMounted, status} = useTransitionStatus(context, {duration: TRANSITION_DURATION}); + const previousStatus = usePrevious(status); React.useEffect(() => { if (isMounted && elements.reference && elements.floating) { @@ -255,6 +263,24 @@ export function Popup({ initialFocusRef, ); + const handleTransitionEnd = React.useCallback( + (event: React.TransitionEvent) => { + // There are two simultaneous transitions running at the same time + // Use specific name to only notify once + if (status === 'open' && event.propertyName === 'transform') { + onTransitionInComplete?.(); + } + }, + [status, onTransitionInComplete], + ); + + // Cannot use transitionend event for "out" transition due to unmounting from the DOM + React.useEffect(() => { + if (status === 'unmounted' && previousStatus === 'close') { + onTransitionOutComplete?.(); + } + }, [status, previousStatus, onTransitionOutComplete]); + return isMounted || keepMounted ? (
(value: T): T | undefined { + const currentRef = React.useRef(value); + const previousRef = React.useRef(); + if (currentRef.current !== value) { + previousRef.current = currentRef.current; + currentRef.current = value; + } + return previousRef.current; +}