diff --git a/packages/react-core/src/components/NotificationBadge/NotificationBadge.tsx b/packages/react-core/src/components/NotificationBadge/NotificationBadge.tsx index b8b34f2f4f0..c703aaef1dd 100644 --- a/packages/react-core/src/components/NotificationBadge/NotificationBadge.tsx +++ b/packages/react-core/src/components/NotificationBadge/NotificationBadge.tsx @@ -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', @@ -27,6 +30,13 @@ export interface NotificationBadgeProps extends Omit { 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) => void; } export const NotificationBadge: React.FunctionComponent = ({ @@ -37,28 +47,46 @@ export const NotificationBadge: React.FunctionComponent icon = , 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) => { + onAnimationEnd?.(event); + setIsAnimating(false); + }; return ( ); }; diff --git a/packages/react-core/src/components/NotificationBadge/__tests__/NotificationBadge.test.tsx b/packages/react-core/src/components/NotificationBadge/__tests__/NotificationBadge.test.tsx index 928d82dedc7..ad4a2922647 100644 --- a/packages/react-core/src/components/NotificationBadge/__tests__/NotificationBadge.test.tsx +++ b/packages/react-core/src/components/NotificationBadge/__tests__/NotificationBadge.test.tsx @@ -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`, () => { @@ -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(); - 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(); - 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(); + + expect(screen.getByRole('button')).toHaveClass(styles.modifiers.notify); +}); + +test('Removes the .pf-m-notify class when animation has ended.', async () => { + render(); + fireEvent.animationEnd(screen.getByRole('button')); + await screen.findByRole('button'); + + expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.notify); }); diff --git a/packages/react-core/src/components/NotificationBadge/__tests__/__snapshots__/NotificationBadge.test.tsx.snap b/packages/react-core/src/components/NotificationBadge/__tests__/__snapshots__/NotificationBadge.test.tsx.snap index f4e0dabe556..ff9aa023b4d 100644 --- a/packages/react-core/src/components/NotificationBadge/__tests__/__snapshots__/NotificationBadge.test.tsx.snap +++ b/packages/react-core/src/components/NotificationBadge/__tests__/__snapshots__/NotificationBadge.test.tsx.snap @@ -46,6 +46,23 @@ exports[`false NotificationBadge needs attention 1`] = ` data-ouia-safe="true" type="button" > + + + @@ -65,6 +82,23 @@ exports[`true NotificationBadge needs attention 1`] = ` data-ouia-safe="true" type="button" > + + + diff --git a/packages/react-core/src/components/NotificationBadge/examples/NotificationBadge.md b/packages/react-core/src/components/NotificationBadge/examples/NotificationBadge.md index 4d0d3b7a662..4512fc011b4 100644 --- a/packages/react-core/src/components/NotificationBadge/examples/NotificationBadge.md +++ b/packages/react-core/src/components/NotificationBadge/examples/NotificationBadge.md @@ -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 @@ -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' ``` @@ -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' +``` \ No newline at end of file diff --git a/packages/react-core/src/components/NotificationBadge/examples/NotificationBadgeWithAnimation.tsx b/packages/react-core/src/components/NotificationBadge/examples/NotificationBadgeWithAnimation.tsx new file mode 100644 index 00000000000..0e9074148e0 --- /dev/null +++ b/packages/react-core/src/components/NotificationBadge/examples/NotificationBadgeWithAnimation.tsx @@ -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 ( + + + + + + + + + + + ); +}; diff --git a/packages/react-core/src/demos/NotificationDrawer/examples/NotificationDrawerBasic.tsx b/packages/react-core/src/demos/NotificationDrawer/examples/NotificationDrawerBasic.tsx index ff49e4a0e62..da7d3c1434d 100644 --- a/packages/react-core/src/demos/NotificationDrawer/examples/NotificationDrawerBasic.tsx +++ b/packages/react-core/src/demos/NotificationDrawer/examples/NotificationDrawerBasic.tsx @@ -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'; @@ -184,9 +183,7 @@ export const NotificationDrawerBasic: React.FunctionComponent = () => { onClick={(event: any) => onCloseNotificationDrawer(event)} aria-label="Notifications" isExpanded={isDrawerExpanded} - > - - + /> { onClick={(event: any) => onCloseNotificationDrawer(event)} aria-label="Notifications" isExpanded={isDrawerExpanded} - > - - + />