Skip to content

Commit e95546b

Browse files
author
Eric Olkowski
committed
Refactored transitionend handling
1 parent dfe8464 commit e95546b

File tree

8 files changed

+88
-187
lines changed

8 files changed

+88
-187
lines changed

packages/react-core/src/components/Alert/Alert.tsx

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -144,49 +144,27 @@ export const Alert: React.FunctionComponent<AlertProps> = ({
144144
const [containsFocus, setContainsFocus] = useState<boolean | undefined>();
145145
const shouldDismiss = timedOut && timedOutAnimation && !isMouseOver && !containsFocus;
146146
const [isDismissed, setIsDismissed] = React.useState(false);
147-
const { hasAnimations } = React.useContext(AlertGroupContext);
147+
const { hasAnimations, updateTransitionEnd } = React.useContext(AlertGroupContext);
148148
const { offstageRight } = alertGroupStyles.modifiers;
149149

150150
const getParentAlertGroupItem = () => divRef.current?.closest(`.${alertGroupStyles.alertGroupItem}`);
151151
useEffect(() => {
152152
const shouldSetDismissed = shouldDismiss && !isDismissed;
153-
if (shouldSetDismissed && hasAnimations) {
154-
const alertGroupItem = getParentAlertGroupItem();
155-
alertGroupItem?.classList.add(offstageRight);
156-
}
157-
158-
if (shouldSetDismissed && !hasAnimations) {
159-
setIsDismissed(true);
153+
if (!shouldSetDismissed) {
154+
return;
160155
}
161-
}, [shouldDismiss, hasAnimations, isDismissed]);
162156

163-
React.useEffect(() => {
164-
const handleOnTransitionEnd = (event: TransitionEvent) => {
165-
const prefersReducedMotion = !window.matchMedia('(prefers-reduced-motion: no-preference)')?.matches;
166-
const parentAlertGroupItem = getParentAlertGroupItem();
167-
if (
168-
parentAlertGroupItem?.contains(event.target as Node) &&
169-
// If a user has no motion preference, we want to target the grid template rows transition
170-
// so that the onClose is called after the "slide up" animation of other alerts finishes. Otherwise
171-
// we want to target the opacity transition since no other transition with be firing.
172-
((prefersReducedMotion && event.propertyName === 'opacity') ||
173-
(!prefersReducedMotion && event.propertyName === 'grid-template-rows')) &&
174-
(event.target as HTMLElement).className.includes(offstageRight) &&
175-
!isDismissed &&
176-
shouldDismiss
177-
) {
178-
setIsDismissed(true);
179-
}
180-
};
157+
const alertGroupItem = getParentAlertGroupItem();
158+
alertGroupItem?.classList.add(offstageRight);
181159

182160
if (hasAnimations) {
183-
window.addEventListener('transitionend', handleOnTransitionEnd);
161+
updateTransitionEnd(() => {
162+
setIsDismissed(true);
163+
});
164+
} else {
165+
setIsDismissed(true);
184166
}
185-
186-
return () => {
187-
window.removeEventListener('transitionend', handleOnTransitionEnd);
188-
};
189-
}, [hasAnimations, shouldDismiss, isDismissed]);
167+
}, [shouldDismiss, isDismissed]);
190168

191169
React.useEffect(() => {
192170
const calculatedTimeout = timeout === true ? 8000 : Number(timeout);

packages/react-core/src/components/Alert/AlertActionCloseButton.tsx

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,48 +26,20 @@ export const AlertActionCloseButton: React.FunctionComponent<AlertActionCloseBut
2626
variantLabel,
2727
...props
2828
}: AlertActionCloseButtonProps) => {
29-
const [shouldDismissOnTransition, setShouldDismissOnTransition] = React.useState(false);
3029
const closeButtonRef = React.useRef(null);
31-
const { hasAnimations } = React.useContext(AlertGroupContext);
30+
const { hasAnimations, updateTransitionEnd } = React.useContext(AlertGroupContext);
3231
const { offstageRight } = alertGroupStyles.modifiers;
3332

3433
const getParentAlertGroupItem = () => closeButtonRef.current?.closest(`.${alertGroupStyles.alertGroupItem}`);
3534
const handleOnClick = () => {
3635
if (hasAnimations) {
3736
getParentAlertGroupItem()?.classList.add(offstageRight);
38-
setShouldDismissOnTransition(true);
37+
updateTransitionEnd(() => onClose());
3938
} else {
4039
onClose();
4140
}
4241
};
4342

44-
React.useEffect(() => {
45-
const handleOnTransitionEnd = (event: TransitionEvent) => {
46-
const prefersReducedMotion = !window.matchMedia('(prefers-reduced-motion: no-preference)')?.matches;
47-
const parentAlertGroupItem = getParentAlertGroupItem();
48-
if (
49-
shouldDismissOnTransition &&
50-
parentAlertGroupItem?.contains(event.target as Node) &&
51-
// If a user has no motion preference, we want to target the grid template rows transition
52-
// so that the onClose is called after the "slide up" animation of other alerts finishes. Otherwise
53-
// we want to target the opacity transition since no other transition with be firing.
54-
((prefersReducedMotion && event.propertyName === 'opacity') ||
55-
(!prefersReducedMotion && event.propertyName === 'grid-template-rows')) &&
56-
(event.target as HTMLElement).className.includes(offstageRight)
57-
) {
58-
onClose();
59-
}
60-
};
61-
62-
if (hasAnimations) {
63-
window.addEventListener('transitionend', handleOnTransitionEnd);
64-
}
65-
66-
return () => {
67-
window.removeEventListener('transitionend', handleOnTransitionEnd);
68-
};
69-
}, [hasAnimations, shouldDismissOnTransition]);
70-
7143
return (
7244
<AlertContext.Consumer>
7345
{({ title, variantLabel: alertVariantLabel }) => (

packages/react-core/src/components/Alert/AlertGroup.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ export interface AlertGroupProps extends Omit<React.HTMLProps<HTMLUListElement>,
88
className?: string;
99
/** Alerts to be rendered in the AlertGroup */
1010
children?: React.ReactNode;
11-
/** Flag indicating whether alerts will have animations when being added or removed from the AlertGroup. */
11+
/** @beta Flag to indicate whether Alerts are animated upon rendering and being dismissed. This is intended
12+
* to be set to false for testing purposes only.
13+
*/
1214
hasAnimations?: boolean;
1315
/** Toast notifications are positioned at the top right corner of the viewport */
1416
isToast?: boolean;

packages/react-core/src/components/Alert/AlertGroupContext.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import * as React from 'react';
22

33
interface AlertGroupContext {
44
hasAnimations?: boolean;
5+
updateTransitionEnd?: (onTransitionEnd: React.Dispatch<React.SetStateAction<() => void>>) => void;
56
}
67

7-
export const AlertGroupContext = React.createContext<AlertGroupContext>({ hasAnimations: false });
8+
export const AlertGroupContext = React.createContext<AlertGroupContext>({
9+
hasAnimations: false,
10+
updateTransitionEnd: () => {}
11+
});
Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Children } from 'react';
1+
import { Children, useState } from 'react';
22
import { css } from '@patternfly/react-styles';
33
import styles from '@patternfly/react-styles/css/components/Alert/alert-group';
44
import { AlertGroupProps } from './AlertGroup';
@@ -14,36 +14,58 @@ export const AlertGroupInline: React.FunctionComponent<AlertGroupProps> = ({
1414
onOverflowClick,
1515
overflowMessage,
1616
...props
17-
}: AlertGroupProps) => (
18-
<AlertGroupContext.Provider value={{ hasAnimations }}>
19-
<ul
20-
role="list"
21-
aria-live={isLiveRegion ? 'polite' : null}
22-
aria-atomic={isLiveRegion ? false : null}
23-
className={css(styles.alertGroup, className, isToast ? styles.modifiers.toast : '')}
24-
{...props}
25-
>
26-
{Children.toArray(children).map((alert, index) => (
27-
<li
28-
className={css(styles.alertGroupItem, hasAnimations && styles.modifiers.offstageTop)}
29-
key={
30-
(alert as React.ReactElement<AlertProps>).props?.id ||
31-
`alertGroupItem-${(alert as React.ReactElement<AlertProps>).key}` ||
32-
index
33-
}
34-
>
35-
{alert}
36-
</li>
37-
))}
38-
{overflowMessage && (
39-
<li>
40-
<button onClick={onOverflowClick} className={css(styles.alertGroupOverflowButton)}>
41-
{overflowMessage}
42-
</button>
43-
</li>
44-
)}
45-
</ul>
46-
</AlertGroupContext.Provider>
47-
);
17+
}: AlertGroupProps) => {
18+
const [handleTransitionEnd, setHandleTransitionEnd] = useState(() => () => {});
19+
const updateTransitionEnd = (onTransitionEnd: React.Dispatch<React.SetStateAction<() => void>>) => {
20+
setHandleTransitionEnd(() => onTransitionEnd);
21+
};
22+
return (
23+
<AlertGroupContext.Provider value={{ hasAnimations, updateTransitionEnd }}>
24+
<ul
25+
role="list"
26+
aria-live={isLiveRegion ? 'polite' : null}
27+
aria-atomic={isLiveRegion ? false : null}
28+
className={css(styles.alertGroup, className, isToast ? styles.modifiers.toast : '')}
29+
{...props}
30+
>
31+
{Children.toArray(children).map((alert, index) => (
32+
<li
33+
onTransitionEnd={(event: React.TransitionEvent<HTMLLIElement>) => {
34+
if (!hasAnimations) {
35+
return;
36+
}
37+
38+
const prefersReducedMotion = !window.matchMedia('(prefers-reduced-motion: no-preference)')?.matches;
39+
if (
40+
// If a user has no motion preference, we want to target the grid template rows transition
41+
// so that the onClose is called after the "slide up" animation of other alerts finishes.
42+
// If they have motion preference, we don't need to check for a specific transition since only opacity should fire.
43+
(prefersReducedMotion || (!prefersReducedMotion && event.propertyName === 'grid-template-rows')) &&
44+
(event.target as HTMLElement).className.includes(styles.modifiers.offstageRight)
45+
) {
46+
handleTransitionEnd();
47+
}
48+
}}
49+
className={css(styles.alertGroupItem, hasAnimations && styles.modifiers.offstageTop)}
50+
key={
51+
(alert as React.ReactElement<AlertProps>).props?.id ||
52+
`alertGroupItem-${(alert as React.ReactElement<AlertProps>).key}` ||
53+
index
54+
}
55+
>
56+
{alert}
57+
</li>
58+
))}
59+
{overflowMessage && (
60+
<li>
61+
<button onClick={onOverflowClick} className={css(styles.alertGroupOverflowButton)}>
62+
{overflowMessage}
63+
</button>
64+
</li>
65+
)}
66+
</ul>
67+
</AlertGroupContext.Provider>
68+
);
69+
};
4870

4971
AlertGroupInline.displayName = 'AlertGroupInline';

packages/react-core/src/components/Alert/__tests__/AlertGroup.test.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen } from '@testing-library/react';
1+
import { render, screen, act, waitFor, fireEvent } from '@testing-library/react';
22
import userEvent from '@testing-library/user-event';
33

44
import { Alert } from '../../Alert';
@@ -65,13 +65,21 @@ test('Toast Alert Group contains expected modifier class', () => {
6565

6666
expect(screen.getByLabelText('group label')).toHaveClass('pf-m-toast');
6767
});
68-
6968
test('alertgroup closes when alerts are closed', async () => {
69+
window.matchMedia = (query) => ({
70+
matches: false,
71+
media: query,
72+
onchange: null,
73+
addListener: jest.fn(), // deprecated
74+
removeListener: jest.fn(), // deprecated
75+
addEventListener: jest.fn(),
76+
removeEventListener: jest.fn(),
77+
dispatchEvent: jest.fn()
78+
});
7079
const onClose = jest.fn();
7180
const user = userEvent.setup();
72-
7381
render(
74-
<AlertGroup hasAnimations={false} isToast appendTo={document.body}>
82+
<AlertGroup className="pf-v6-m-no-motion" isToast appendTo={document.body}>
7583
<Alert
7684
isLiveRegion
7785
title={'Test Alert'}
@@ -81,5 +89,6 @@ test('alertgroup closes when alerts are closed', async () => {
8189
);
8290

8391
await user.click(screen.getByLabelText('Close'));
92+
fireEvent.transitionEnd(screen.getByText('Test Alert').closest('.pf-v6-c-alert-group__item') as HTMLElement);
8493
expect(onClose).toHaveBeenCalled();
8594
});

packages/react-core/src/components/Alert/examples/Alert.md

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -478,13 +478,3 @@ You may add multiple alerts to an alert group at once. Click the "add alert coll
478478
```ts file="./AlertGroupMultipleDynamic.tsx"
479479

480480
```
481-
482-
### Alert group with animations
483-
484-
You can apply animations to alerts within an `<AlertGroup>` by passing the `hasAnimations` property. Doing so will animate alerts when added or removed from an `<AlertGroup>`. The following example shows both a toast and inline `<AlertGroup>` with animations applied.
485-
486-
When using animations, each alert must have a unique `id` or `key` passed to it.
487-
488-
```ts file="./AlertGroupAnimations.tsx"
489-
490-
```

packages/react-core/src/components/Alert/examples/AlertGroupAnimations.tsx

Lines changed: 0 additions & 76 deletions
This file was deleted.

0 commit comments

Comments
 (0)