Skip to content

Commit 9cebdda

Browse files
author
Eric Olkowski
committed
Refactored transitionend handling
1 parent 9121bdf commit 9cebdda

File tree

8 files changed

+87
-186
lines changed

8 files changed

+87
-186
lines changed

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

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

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

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

183161
if (hasAnimations) {
184-
window.addEventListener('transitionend', handleOnTransitionEnd);
162+
updateTransitionEnd(() => {
163+
setIsDismissed(true);
164+
});
165+
} else {
166+
setIsDismissed(true);
185167
}
186-
187-
return () => {
188-
window.removeEventListener('transitionend', handleOnTransitionEnd);
189-
};
190-
}, [hasAnimations, shouldDismiss, isDismissed]);
168+
}, [shouldDismiss, isDismissed]);
191169

192170
React.useEffect(() => {
193171
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
@@ -27,48 +27,20 @@ export const AlertActionCloseButton: React.FunctionComponent<AlertActionCloseBut
2727
variantLabel,
2828
...props
2929
}: AlertActionCloseButtonProps) => {
30-
const [shouldDismissOnTransition, setShouldDismissOnTransition] = React.useState(false);
3130
const closeButtonRef = React.useRef(null);
32-
const { hasAnimations } = React.useContext(AlertGroupContext);
31+
const { hasAnimations, updateTransitionEnd } = React.useContext(AlertGroupContext);
3332
const { offstageRight } = alertGroupStyles.modifiers;
3433

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

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

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

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
{React.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] = React.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+
{React.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,6 +1,6 @@
11
import * as React from 'react';
22

3-
import { render, screen } from '@testing-library/react';
3+
import { render, screen, act, waitFor, fireEvent } from '@testing-library/react';
44
import userEvent from '@testing-library/user-event';
55

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

6868
expect(screen.getByLabelText('group label')).toHaveClass('pf-m-toast');
6969
});
70-
7170
test('alertgroup closes when alerts are closed', async () => {
71+
window.matchMedia = (query) => ({
72+
matches: false,
73+
media: query,
74+
onchange: null,
75+
addListener: jest.fn(), // deprecated
76+
removeListener: jest.fn(), // deprecated
77+
addEventListener: jest.fn(),
78+
removeEventListener: jest.fn(),
79+
dispatchEvent: jest.fn()
80+
});
7281
const onClose = jest.fn();
7382
const user = userEvent.setup();
74-
7583
render(
76-
<AlertGroup hasAnimations={false} isToast appendTo={document.body}>
84+
<AlertGroup className="pf-v6-m-no-motion" isToast appendTo={document.body}>
7785
<Alert
7886
isLiveRegion
7987
title={'Test Alert'}
@@ -83,5 +91,6 @@ test('alertgroup closes when alerts are closed', async () => {
8391
);
8492

8593
await user.click(screen.getByLabelText('Close'));
94+
fireEvent.transitionEnd(screen.getByText('Test Alert').closest('.pf-v6-c-alert-group__item') as HTMLElement);
8695
expect(onClose).toHaveBeenCalled();
8796
});

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)