Skip to content

Commit

Permalink
feat(Popup): add transition complete events
Browse files Browse the repository at this point in the history
transition
  • Loading branch information
amje committed Dec 6, 2024
1 parent b6f7912 commit 539b90a
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 5 deletions.
3 changes: 2 additions & 1 deletion src/components/Popup/Popup.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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} {
Expand Down Expand Up @@ -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'] &,
Expand Down
30 changes: 28 additions & 2 deletions src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -155,6 +160,8 @@ export function Popup({
id,
role: roleProp,
zIndex = 1000,
onTransitionInComplete,
onTransitionOutComplete,
...restProps
}: PopupProps) {
const contentRef = React.useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -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) {
Expand All @@ -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 ? (
<Portal disablePortal={disablePortal}>
<FloatingFocusManager
Expand All @@ -279,7 +305,7 @@ export function Popup({
}}
data-floating-ui-placement={finalPlacement}
data-floating-ui-status={status}
{...getFloatingProps(floatingProps)}
{...getFloatingProps({...floatingProps, onTransitionEnd: handleTransitionEnd})}
>
<div
ref={contentRef}
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/private/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ export * from './useCheckbox';
export * from './useCloseOnTimeout';
export * from './useConditionallyControlledState';
export * from './useElementSize';
export * from './useFormResetHandler';
export * from './useHover';
export * from './usePrevious';
export * from './useRadio';
export * from './useRadioGroup';
export * from './useRestoreFocus';
export * from './useUpdateEffect';
export * from './useTooltipVisible';
export * from './useFormResetHandler';
export * from './useUpdateEffect';
13 changes: 13 additions & 0 deletions src/hooks/private/usePrevious/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# usePrevious

The `usePrevious` hook stores the previous value during renders

## Properties

| Name | Description | Type | Default |
| :---- | :------------------------------- | :---: | :-----: |
| value | Any value, usually from useState | `any` | |

## Result

Previous value
1 change: 1 addition & 0 deletions src/hooks/private/usePrevious/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {usePrevious} from './usePrevious';
11 changes: 11 additions & 0 deletions src/hooks/private/usePrevious/usePrevious.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';

export function usePrevious<T>(value: T): T | undefined {
const currentRef = React.useRef<T>(value);
const previousRef = React.useRef<T>();
if (currentRef.current !== value) {
previousRef.current = currentRef.current;
currentRef.current = value;
}
return previousRef.current;
}

0 comments on commit 539b90a

Please sign in to comment.