Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useEffect, useState } from 'react';
import { Button, ButtonVariant, ButtonProps } from '../Button';
import AttentionBellIcon from '@patternfly/react-icons/dist/esm/icons/attention-bell-icon';
import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
import styles from '@patternfly/react-styles/css/components/Button/button';
import { css } from '@patternfly/react-styles';

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

export const NotificationBadge: React.FunctionComponent<NotificationBadgeProps> = ({
Expand All @@ -37,28 +47,46 @@ export const NotificationBadge: React.FunctionComponent<NotificationBadgeProps>
icon = <BellIcon />,
className,
isExpanded = false,
shouldNotify = false,
onAnimationEnd,
...props
}: NotificationBadgeProps) => {
const [isAnimating, setIsAnimating] = useState(shouldNotify);
const hasCount = count > 0;
const hasChildren = children !== undefined;
const isAttention = variant === NotificationBadgeVariant.attention;

const notificationIcon = isAttention ? attentionIcon : icon;
const notificationContent = hasChildren ? children : notificationIcon;
let notificationContent: React.ReactNode = null;

if (hasCount) {
notificationContent = count;
} else if (hasChildren) {
notificationContent = children;
}

const buttonClassName = isAnimating ? css(className, styles.modifiers.notify) : className;

useEffect(() => {
setIsAnimating(shouldNotify);
}, [shouldNotify]);

const [iconProp, notificationChild] = hasCount ? [notificationContent, count] : [undefined, notificationContent];
const handleAnimationEnd = (event: React.AnimationEvent<HTMLButtonElement>) => {
onAnimationEnd?.(event);
setIsAnimating(false);
};

return (
<Button
variant={ButtonVariant.stateful}
className={className}
className={buttonClassName}
aria-expanded={isExpanded}
state={variant}
isClicked={isExpanded}
icon={iconProp}
icon={notificationIcon}
onAnimationEnd={handleAnimationEnd}
{...props}
>
{notificationChild}
{notificationContent}
</Button>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NotificationBadge } from '../NotificationBadge';
import { render, screen } from '@testing-library/react';
import { render, screen, fireEvent } from '@testing-library/react';
import styles from '@patternfly/react-styles/css/components/Button/button';

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

expect(screen.getByRole('button')).not.toHaveClass('pf-m-clicked');
expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.clicked);
});

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

expect(screen.getByRole('button')).toHaveClass('pf-m-clicked');
expect(screen.getByRole('button')).toHaveClass(styles.modifiers.clicked);
});

test('Renders with .pf-m-notify when hasAnimation is passed in.', () => {
render(<NotificationBadge shouldNotify />);

expect(screen.getByRole('button')).toHaveClass(styles.modifiers.notify);
});

test('Removes the .pf-m-notify class when animation has ended.', async () => {
render(<NotificationBadge shouldNotify />);
fireEvent.animationEnd(screen.getByRole('button'));
await screen.findByRole('button');

expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.notify);
});
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ exports[`false NotificationBadge needs attention 1`] = `
data-ouia-safe="true"
type="button"
>
<span
class="pf-v6-c-button__icon pf-m-start"
>
<svg
aria-hidden="true"
class="pf-v6-svg"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 896 1024"
width="1em"
>
<path
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"
/>
</svg>
</span>
<span
class="pf-v6-c-button__text"
>
Expand All @@ -65,6 +82,23 @@ exports[`true NotificationBadge needs attention 1`] = `
data-ouia-safe="true"
type="button"
>
<span
class="pf-v6-c-button__icon pf-m-start"
>
<svg
aria-hidden="true"
class="pf-v6-svg"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 896 1024"
width="1em"
>
<path
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"
/>
</svg>
</span>
<span
class="pf-v6-c-button__text"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ cssPrefix: pf-v6-c-notification-badge
propComponents: ['NotificationBadge']
---

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

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

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

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

```ts file='./NotificationBadgeWithCount.tsx'
```

### With animation

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.

```ts file='./NotificationBadgeWithAnimation.tsx'
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useEffect, useState } from 'react';
import { NotificationBadge, NotificationBadgeVariant, Button, Stack, StackItem } from '@patternfly/react-core';

export const NotificationBadgeWithAnimation: React.FunctionComponent = () => {
const [count, setCount] = useState(0);
const [notify, setNotify] = useState(false);

useEffect(() => {
if (count > 0) {
setNotify(true);
}
}, [count]);

const onAnimationEnd = () => {
setNotify(false);
};

return (
<Stack hasGutter>
<StackItem>
<NotificationBadge
variant={NotificationBadgeVariant.read}
aria-label="Notification badge with read variant and no count"
count={count}
shouldNotify={notify}
onAnimationEnd={onAnimationEnd}
/>
<NotificationBadge
variant={NotificationBadgeVariant.unread}
aria-label="Notification badge animation with unread variant and count"
count={count}
shouldNotify={notify}
onAnimationEnd={onAnimationEnd}
/>
<NotificationBadge
variant={NotificationBadgeVariant.attention}
aria-label="Notification badge animation with attention variant and count"
count={count}
shouldNotify={notify}
onAnimationEnd={onAnimationEnd}
/>
</StackItem>
<StackItem>
<Button onClick={() => setCount((prevCount) => prevCount + 1)}>Add Notification</Button>
</StackItem>
</Stack>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import {
ToolbarGroup,
ToolbarContent
} from '@patternfly/react-core';
import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon';
import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon';
import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon';
Expand Down Expand Up @@ -184,9 +183,7 @@ export const NotificationDrawerBasic: React.FunctionComponent = () => {
onClick={(event: any) => onCloseNotificationDrawer(event)}
aria-label="Notifications"
isExpanded={isDrawerExpanded}
>
<BellIcon />
</NotificationBadge>
/>
</ToolbarItem>
<ToolbarGroup
variant="action-group-plain"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import {
ToolbarGroup,
ToolbarContent
} from '@patternfly/react-core';
import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon';
import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon';
import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon';
Expand Down Expand Up @@ -234,9 +233,7 @@ export const NotificationDrawerGrouped: React.FunctionComponent = () => {
onClick={(event: any) => onCloseNotificationDrawer(event)}
aria-label="Notifications"
isExpanded={isDrawerExpanded}
>
<BellIcon />
</NotificationBadge>
/>
</ToolbarItem>
<ToolbarGroup
variant="action-group-plain"
Expand Down