Skip to content

Commit 37ad42b

Browse files
authored
feat(NotificationBadge): Add animation for notify (#11675)
* feat(NotificationBadge): Add animation for notify * update snapshot for draft * added review comments, tests, example * address review comments * update example description in md * add new prop for handling animation end
1 parent 6609550 commit 37ad42b

7 files changed

Lines changed: 145 additions & 19 deletions

File tree

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

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { useEffect, useState } from 'react';
12
import { Button, ButtonVariant, ButtonProps } from '../Button';
23
import AttentionBellIcon from '@patternfly/react-icons/dist/esm/icons/attention-bell-icon';
34
import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
5+
import styles from '@patternfly/react-styles/css/components/Button/button';
6+
import { css } from '@patternfly/react-styles';
47

58
export enum NotificationBadgeVariant {
69
read = 'read',
@@ -27,6 +30,13 @@ export interface NotificationBadgeProps extends Omit<ButtonProps, 'variant'> {
2730
isExpanded?: boolean;
2831
/** Determines the variant of the notification badge. */
2932
variant?: NotificationBadgeVariant | 'read' | 'unread' | 'attention';
33+
/** Flag indicating whether the notification badge animation should be triggered. Each
34+
* time this prop is true, the animation will be triggered a single time.
35+
*/
36+
shouldNotify?: boolean;
37+
/** Callback for when the animation of the notification badge icon ends.
38+
*/
39+
onAnimationEnd?: (event: React.AnimationEvent<HTMLButtonElement>) => void;
3040
}
3141

3242
export const NotificationBadge: React.FunctionComponent<NotificationBadgeProps> = ({
@@ -37,28 +47,46 @@ export const NotificationBadge: React.FunctionComponent<NotificationBadgeProps>
3747
icon = <BellIcon />,
3848
className,
3949
isExpanded = false,
50+
shouldNotify = false,
51+
onAnimationEnd,
4052
...props
4153
}: NotificationBadgeProps) => {
54+
const [isAnimating, setIsAnimating] = useState(shouldNotify);
4255
const hasCount = count > 0;
4356
const hasChildren = children !== undefined;
4457
const isAttention = variant === NotificationBadgeVariant.attention;
45-
4658
const notificationIcon = isAttention ? attentionIcon : icon;
47-
const notificationContent = hasChildren ? children : notificationIcon;
59+
let notificationContent: React.ReactNode = null;
60+
61+
if (hasCount) {
62+
notificationContent = count;
63+
} else if (hasChildren) {
64+
notificationContent = children;
65+
}
66+
67+
const buttonClassName = isAnimating ? css(className, styles.modifiers.notify) : className;
68+
69+
useEffect(() => {
70+
setIsAnimating(shouldNotify);
71+
}, [shouldNotify]);
4872

49-
const [iconProp, notificationChild] = hasCount ? [notificationContent, count] : [undefined, notificationContent];
73+
const handleAnimationEnd = (event: React.AnimationEvent<HTMLButtonElement>) => {
74+
onAnimationEnd?.(event);
75+
setIsAnimating(false);
76+
};
5077

5178
return (
5279
<Button
5380
variant={ButtonVariant.stateful}
54-
className={className}
81+
className={buttonClassName}
5582
aria-expanded={isExpanded}
5683
state={variant}
5784
isClicked={isExpanded}
58-
icon={iconProp}
85+
icon={notificationIcon}
86+
onAnimationEnd={handleAnimationEnd}
5987
{...props}
6088
>
61-
{notificationChild}
89+
{notificationContent}
6290
</Button>
6391
);
6492
};

packages/react-core/src/components/NotificationBadge/__tests__/NotificationBadge.test.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NotificationBadge } from '../NotificationBadge';
2-
import { render, screen } from '@testing-library/react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import styles from '@patternfly/react-styles/css/components/Button/button';
34

45
Object.values([true, false]).forEach((attentionVariant) => {
56
test(`${attentionVariant} NotificationBadge needs attention`, () => {
@@ -32,11 +33,25 @@ test('Renders with aria-expanded="true" when isExpanded is passed in.', () => {
3233
test('Does not render with .pf-m-clicked when isExpanded is not passed in.', () => {
3334
render(<NotificationBadge />);
3435

35-
expect(screen.getByRole('button')).not.toHaveClass('pf-m-clicked');
36+
expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.clicked);
3637
});
3738

3839
test('Renders with .pf-m-clicked when isExpanded is passed in.', () => {
3940
render(<NotificationBadge isExpanded />);
4041

41-
expect(screen.getByRole('button')).toHaveClass('pf-m-clicked');
42+
expect(screen.getByRole('button')).toHaveClass(styles.modifiers.clicked);
43+
});
44+
45+
test('Renders with .pf-m-notify when hasAnimation is passed in.', () => {
46+
render(<NotificationBadge shouldNotify />);
47+
48+
expect(screen.getByRole('button')).toHaveClass(styles.modifiers.notify);
49+
});
50+
51+
test('Removes the .pf-m-notify class when animation has ended.', async () => {
52+
render(<NotificationBadge shouldNotify />);
53+
fireEvent.animationEnd(screen.getByRole('button'));
54+
await screen.findByRole('button');
55+
56+
expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.notify);
4257
});

packages/react-core/src/components/NotificationBadge/__tests__/__snapshots__/NotificationBadge.test.tsx.snap

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,23 @@ exports[`false NotificationBadge needs attention 1`] = `
4646
data-ouia-safe="true"
4747
type="button"
4848
>
49+
<span
50+
class="pf-v6-c-button__icon pf-m-start"
51+
>
52+
<svg
53+
aria-hidden="true"
54+
class="pf-v6-svg"
55+
fill="currentColor"
56+
height="1em"
57+
role="img"
58+
viewBox="0 0 896 1024"
59+
width="1em"
60+
>
61+
<path
62+
d="M448,0 C465.333333,0 480.333333,6.33333333 493,19 C505.666667,31.6666667 512,46.6666667 512,64 L512,106 L514.23,106.45 C587.89,121.39 648.48,157.24 696,214 C744,271.333333 768,338.666667 768,416 C768,500 780,568.666667 804,622 C818.666667,652.666667 841.333333,684 872,716 C873.773676,718.829136 875.780658,721.505113 878,724 C890,737.333333 896,752.333333 896,769 C896,785.666667 890,800.333333 878,813 C866,825.666667 850.666667,832 832,832 L63.3,832 C44.9533333,831.84 29.8533333,825.506667 18,813 C6,800.333333 0,785.666667 0,769 C0,752.333333 6,737.333333 18,724 L24,716 L25.06,714.9 C55.1933333,683.28 77.5066667,652.313333 92,622 C116,568.666667 128,500 128,416 C128,338.666667 152,271.333333 200,214 C248,156.666667 309.333333,120.666667 384,106 L384,63.31 C384.166667,46.27 390.5,31.5 403,19 C415.666667,6.33333333 430.666667,0 448,0 Z M576,896 L576,897.08 C575.74,932.6 563.073333,962.573333 538,987 C512.666667,1011.66667 482.666667,1024 448,1024 C413.333333,1024 383.333333,1011.66667 358,987 C332.666667,962.333333 320,932 320,896 L576,896 Z M475,192 L421,192 C400.565464,192 384,208.565464 384,229 L384,539 C384,559.434536 400.565464,576 421,576 L475,576 C495.434536,576 512,559.434536 512,539 L512,229 C512,208.565464 495.434536,192 475,192 Z M448,640 C412.653776,640 384,668.653776 384,704 C384,739.346224 412.653776,768 448,768 C483.346224,768 512,739.346224 512,704 C512,668.653776 483.346224,640 448,640 Z"
63+
/>
64+
</svg>
65+
</span>
4966
<span
5067
class="pf-v6-c-button__text"
5168
>
@@ -65,6 +82,23 @@ exports[`true NotificationBadge needs attention 1`] = `
6582
data-ouia-safe="true"
6683
type="button"
6784
>
85+
<span
86+
class="pf-v6-c-button__icon pf-m-start"
87+
>
88+
<svg
89+
aria-hidden="true"
90+
class="pf-v6-svg"
91+
fill="currentColor"
92+
height="1em"
93+
role="img"
94+
viewBox="0 0 896 1024"
95+
width="1em"
96+
>
97+
<path
98+
d="M448,0 C465.333333,0 480.333333,6.33333333 493,19 C505.666667,31.6666667 512,46.6666667 512,64 L512,106 L514.23,106.45 C587.89,121.39 648.48,157.24 696,214 C744,271.333333 768,338.666667 768,416 C768,500 780,568.666667 804,622 C818.666667,652.666667 841.333333,684 872,716 C873.773676,718.829136 875.780658,721.505113 878,724 C890,737.333333 896,752.333333 896,769 C896,785.666667 890,800.333333 878,813 C866,825.666667 850.666667,832 832,832 L63.3,832 C44.9533333,831.84 29.8533333,825.506667 18,813 C6,800.333333 0,785.666667 0,769 C0,752.333333 6,737.333333 18,724 L24,716 L25.06,714.9 C55.1933333,683.28 77.5066667,652.313333 92,622 C116,568.666667 128,500 128,416 C128,338.666667 152,271.333333 200,214 C248,156.666667 309.333333,120.666667 384,106 L384,63.31 C384.166667,46.27 390.5,31.5 403,19 C415.666667,6.33333333 430.666667,0 448,0 Z M576,896 L576,897.08 C575.74,932.6 563.073333,962.573333 538,987 C512.666667,1011.66667 482.666667,1024 448,1024 C413.333333,1024 383.333333,1011.66667 358,987 C332.666667,962.333333 320,932 320,896 L576,896 Z M475,192 L421,192 C400.565464,192 384,208.565464 384,229 L384,539 C384,559.434536 400.565464,576 421,576 L475,576 C495.434536,576 512,559.434536 512,539 L512,229 C512,208.565464 495.434536,192 475,192 Z M448,640 C412.653776,640 384,668.653776 384,704 C384,739.346224 412.653776,768 448,768 C483.346224,768 512,739.346224 512,704 C512,668.653776 483.346224,640 448,640 Z"
99+
/>
100+
</svg>
101+
</span>
68102
<span
69103
class="pf-v6-c-button__text"
70104
>

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ cssPrefix: pf-v6-c-notification-badge
55
propComponents: ['NotificationBadge']
66
---
77

8-
import { useState } from 'react';
8+
import { useEffect, useState } from 'react';
99
import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
1010

1111
## Examples
@@ -18,7 +18,7 @@ The `isExpanded` property is also passed in to:
1818

1919
- set the `aria-expanded` attribute on the notification badge,
2020
- apply visual styling on the notification badge, and
21-
- to indiciate that a notification drawer is expanded, for use-cases as seen in our [notification drawer react demos](/components/notification-drawer/react-demos).
21+
- to indicate that a notification drawer is expanded, for use-cases as seen in our [notification drawer react demos](/components/notification-drawer/react-demos).
2222

2323
```ts file='./NotificationBadgeBasic.tsx'
2424
```
@@ -29,3 +29,10 @@ You can display a number within the notification badge by passing in the `count`
2929

3030
```ts file='./NotificationBadgeWithCount.tsx'
3131
```
32+
33+
### With animation
34+
35+
To animate the icon within the notification badge, use the `shouldNotify` property. In this example, the notification badge icon will only animate if the notification count is greater than 0.
36+
37+
```ts file='./NotificationBadgeWithAnimation.tsx'
38+
```
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useEffect, useState } from 'react';
2+
import { NotificationBadge, NotificationBadgeVariant, Button, Stack, StackItem } from '@patternfly/react-core';
3+
4+
export const NotificationBadgeWithAnimation: React.FunctionComponent = () => {
5+
const [count, setCount] = useState(0);
6+
const [notify, setNotify] = useState(false);
7+
8+
useEffect(() => {
9+
if (count > 0) {
10+
setNotify(true);
11+
}
12+
}, [count]);
13+
14+
const onAnimationEnd = () => {
15+
setNotify(false);
16+
};
17+
18+
return (
19+
<Stack hasGutter>
20+
<StackItem>
21+
<NotificationBadge
22+
variant={NotificationBadgeVariant.read}
23+
aria-label="Notification badge with read variant and no count"
24+
count={count}
25+
shouldNotify={notify}
26+
onAnimationEnd={onAnimationEnd}
27+
/>
28+
<NotificationBadge
29+
variant={NotificationBadgeVariant.unread}
30+
aria-label="Notification badge animation with unread variant and count"
31+
count={count}
32+
shouldNotify={notify}
33+
onAnimationEnd={onAnimationEnd}
34+
/>
35+
<NotificationBadge
36+
variant={NotificationBadgeVariant.attention}
37+
aria-label="Notification badge animation with attention variant and count"
38+
count={count}
39+
shouldNotify={notify}
40+
onAnimationEnd={onAnimationEnd}
41+
/>
42+
</StackItem>
43+
<StackItem>
44+
<Button onClick={() => setCount((prevCount) => prevCount + 1)}>Add Notification</Button>
45+
</StackItem>
46+
</Stack>
47+
);
48+
};

packages/react-core/src/demos/NotificationDrawer/examples/NotificationDrawerBasic.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ import {
4545
ToolbarGroup,
4646
ToolbarContent
4747
} from '@patternfly/react-core';
48-
import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
4948
import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon';
5049
import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon';
5150
import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon';
@@ -184,9 +183,7 @@ export const NotificationDrawerBasic: React.FunctionComponent = () => {
184183
onClick={(event: any) => onCloseNotificationDrawer(event)}
185184
aria-label="Notifications"
186185
isExpanded={isDrawerExpanded}
187-
>
188-
<BellIcon />
189-
</NotificationBadge>
186+
/>
190187
</ToolbarItem>
191188
<ToolbarGroup
192189
variant="action-group-plain"

packages/react-core/src/demos/NotificationDrawer/examples/NotificationDrawerGrouped.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ import {
4747
ToolbarGroup,
4848
ToolbarContent
4949
} from '@patternfly/react-core';
50-
import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
5150
import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon';
5251
import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon';
5352
import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon';
@@ -234,9 +233,7 @@ export const NotificationDrawerGrouped: React.FunctionComponent = () => {
234233
onClick={(event: any) => onCloseNotificationDrawer(event)}
235234
aria-label="Notifications"
236235
isExpanded={isDrawerExpanded}
237-
>
238-
<BellIcon />
239-
</NotificationBadge>
236+
/>
240237
</ToolbarItem>
241238
<ToolbarGroup
242239
variant="action-group-plain"

0 commit comments

Comments
 (0)