From 63de33a8fc9960f32c121db8e81f45e0ba32bdb6 Mon Sep 17 00:00:00 2001 From: Marcos Kolodny Date: Fri, 9 Aug 2024 13:21:25 +0200 Subject: [PATCH] feat(Popover, Snackbar, Callout, Chip, Dialog, Cards): allow customizable close button label (#1193) --- src/__tests__/callout-test.tsx | 26 +++++++++++++- src/__tests__/chip-test.tsx | 17 +++++++++ src/__tests__/data-card-test.tsx | 20 +++++++++++ src/__tests__/dialog-test.tsx | 14 ++++++++ src/__tests__/display-data-card-test.tsx | 20 +++++++++++ src/__tests__/display-media-card-test.tsx | 21 +++++++++++ src/__tests__/media-card-test.tsx | 21 +++++++++++ src/__tests__/naked-card-test.tsx | 21 +++++++++++ src/__tests__/popover-test.tsx | 13 +++++++ src/__tests__/poster-card-test.tsx | 22 ++++++++++++ src/__tests__/snackbar-test.tsx | 26 ++++++++++++++ src/callout.tsx | 4 ++- src/card.tsx | 44 +++++++++++++++++++---- src/chip.tsx | 5 +-- src/dialog.tsx | 6 +++- src/popover.tsx | 3 ++ src/snackbar.tsx | 18 ++++++++-- src/tooltip.tsx | 6 +++- 18 files changed, 293 insertions(+), 14 deletions(-) diff --git a/src/__tests__/callout-test.tsx b/src/__tests__/callout-test.tsx index 20fe0e98f4..c265637bc6 100644 --- a/src/__tests__/callout-test.tsx +++ b/src/__tests__/callout-test.tsx @@ -4,7 +4,7 @@ import {ThemeContextProvider, Callout} from '..'; import userEvent from '@testing-library/user-event'; import {makeTheme} from './test-utils'; -test('renders an accesible and clossable Callout', async () => { +test('renders an accesible and closable Callout', async () => { const handleCloseSpy = jest.fn(); render( @@ -22,3 +22,27 @@ test('renders an accesible and clossable Callout', async () => { expect(handleCloseSpy).toHaveBeenCalledTimes(1); }); + +test('renders an accesible and closable Callout with custom close button label', async () => { + const handleCloseSpy = jest.fn(); + render( + + + + ); + + const callout = screen.getByRole('region'); + expect(callout).toBeInTheDocument(); + + const closeButton = within(callout).getByRole('button', {name: 'custom close label'}); + expect(closeButton).toBeInTheDocument(); + + await userEvent.click(closeButton); + + expect(handleCloseSpy).toHaveBeenCalledTimes(1); +}); diff --git a/src/__tests__/chip-test.tsx b/src/__tests__/chip-test.tsx index 9729675af4..87962ed629 100644 --- a/src/__tests__/chip-test.tsx +++ b/src/__tests__/chip-test.tsx @@ -20,6 +20,23 @@ test('Chip can be closed', async () => { expect(closeSpy).toHaveBeenCalledTimes(1); }); +test('Chip can be closed when using custom close label', async () => { + const closeSpy = jest.fn(); + render( + + + some text + + + ); + + const closeButton = screen.getByRole('button', {name: 'custom close label'}); + + await userEvent.click(closeButton); + + expect(closeSpy).toHaveBeenCalledTimes(1); +}); + test('Chip can be clicked', async () => { const clickSpy = jest.fn(); render( diff --git a/src/__tests__/data-card-test.tsx b/src/__tests__/data-card-test.tsx index abbe4b39d5..2056c341b0 100644 --- a/src/__tests__/data-card-test.tsx +++ b/src/__tests__/data-card-test.tsx @@ -6,6 +6,7 @@ import ThemeContextProvider from '../theme-context-provider'; import Tag from '../tag'; import Stack from '../stack'; import {Text2} from '../text'; +import userEvent from '@testing-library/user-event'; test('DataCard "href" label', async () => { render( @@ -72,3 +73,22 @@ test('DataCard "onPress" label', async () => { await screen.findByRole('button', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); }); + +test('DataCard onClose custom label', async () => { + const closeSpy = jest.fn(); + + render( + + + + ); + + const closeButton = await screen.findByRole('button', {name: 'custom close label'}); + await userEvent.click(closeButton); + expect(closeSpy).toHaveBeenCalledTimes(1); +}); diff --git a/src/__tests__/dialog-test.tsx b/src/__tests__/dialog-test.tsx index 1a2902d5c1..59664e53ca 100644 --- a/src/__tests__/dialog-test.tsx +++ b/src/__tests__/dialog-test.tsx @@ -22,6 +22,7 @@ const confirmProps = { cancelText: 'Nope!', onAccept: onAcceptSpy, onCancel: onCancelSpy, + closeButtonLabel: 'custom close label', }; let savedAlert: (params: any) => void | null = () => { @@ -291,3 +292,16 @@ test('when webview bridge is available nativeConfirm is shown', async () => { }); }); }); + +test('dialog close button is accessible', async () => { + render( + + + + ); + + const dialogButton = await screen.findByRole('button', {name: 'Dialog'}); + await userEvent.click(dialogButton); + + await screen.findByRole('button', {name: 'custom close label'}); +}); diff --git a/src/__tests__/display-data-card-test.tsx b/src/__tests__/display-data-card-test.tsx index 02e5e647e2..49e33ffc06 100644 --- a/src/__tests__/display-data-card-test.tsx +++ b/src/__tests__/display-data-card-test.tsx @@ -6,6 +6,7 @@ import ThemeContextProvider from '../theme-context-provider'; import Tag from '../tag'; import Stack from '../stack'; import {Text2} from '../text'; +import userEvent from '@testing-library/user-event'; test('DisplayDataCard "href" label', async () => { render( @@ -72,3 +73,22 @@ test('DisplayDataCard "onPress" label', async () => { await screen.findByRole('button', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); }); + +test('DisplayDataCard onClose custom label', async () => { + const closeSpy = jest.fn(); + + render( + + + + ); + + const closeButton = await screen.findByRole('button', {name: 'custom close label'}); + await userEvent.click(closeButton); + expect(closeSpy).toHaveBeenCalledTimes(1); +}); diff --git a/src/__tests__/display-media-card-test.tsx b/src/__tests__/display-media-card-test.tsx index 21297fdf22..da8d978d22 100644 --- a/src/__tests__/display-media-card-test.tsx +++ b/src/__tests__/display-media-card-test.tsx @@ -6,6 +6,7 @@ import ThemeContextProvider from '../theme-context-provider'; import Tag from '../tag'; import Stack from '../stack'; import {Text2} from '../text'; +import userEvent from '@testing-library/user-event'; test('DisplayMediaCard "href" label', async () => { render( @@ -75,3 +76,23 @@ test('DisplayMediaCard "onPress" label', async () => { await screen.findByRole('button', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); }); + +test('DisplayMediaCard onClose custom label', async () => { + const closeSpy = jest.fn(); + + render( + + + + ); + + const closeButton = await screen.findByRole('button', {name: 'custom close label'}); + await userEvent.click(closeButton); + expect(closeSpy).toHaveBeenCalledTimes(1); +}); diff --git a/src/__tests__/media-card-test.tsx b/src/__tests__/media-card-test.tsx index a57128d562..35776af9de 100644 --- a/src/__tests__/media-card-test.tsx +++ b/src/__tests__/media-card-test.tsx @@ -7,6 +7,7 @@ import Tag from '../tag'; import Stack from '../stack'; import Image from '../image'; import {Text2} from '../text'; +import userEvent from '@testing-library/user-event'; test('MediaCard "href" label', async () => { render( @@ -85,3 +86,23 @@ test('MediaCard "onPress" label', async () => { name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', }); }); + +test('MediaCard onClose custom label', async () => { + const closeSpy = jest.fn(); + + render( + + } + /> + + ); + + const closeButton = await screen.findByRole('button', {name: 'custom close label'}); + await userEvent.click(closeButton); + expect(closeSpy).toHaveBeenCalledTimes(1); +}); diff --git a/src/__tests__/naked-card-test.tsx b/src/__tests__/naked-card-test.tsx index ce8ce22384..4cdce0b345 100644 --- a/src/__tests__/naked-card-test.tsx +++ b/src/__tests__/naked-card-test.tsx @@ -7,6 +7,7 @@ import Tag from '../tag'; import Stack from '../stack'; import Image from '../image'; import {Text2} from '../text'; +import userEvent from '@testing-library/user-event'; test('NakedCard "href" label', async () => { render( @@ -86,6 +87,26 @@ test('NakedCard "onPress" label', async () => { }); }); +test('NakedCard onClose custom label', async () => { + const closeSpy = jest.fn(); + + render( + + } + /> + + ); + + const closeButton = await screen.findByRole('button', {name: 'custom close label'}); + await userEvent.click(closeButton); + expect(closeSpy).toHaveBeenCalledTimes(1); +}); + test('SmallNakedCard "href" label', async () => { render( diff --git a/src/__tests__/popover-test.tsx b/src/__tests__/popover-test.tsx index 39f142d8d9..779bf0182c 100644 --- a/src/__tests__/popover-test.tsx +++ b/src/__tests__/popover-test.tsx @@ -140,3 +140,16 @@ test('popover - uncontrolled', async () => { expect(screen.queryByText('Content')).not.toBeInTheDocument(); }); }); + +test('popover- close button label is customizable', async () => { + render(); + + const target = screen.getByText('Press me!'); + + // Initially closed, the button is not visible + expect(screen.queryByLabelText('custom close label')).not.toBeInTheDocument(); + + // Opened after click on target, the button is visible + fireEvent.click(target); + expect(screen.getByLabelText('custom close label')).toBeInTheDocument(); +}); diff --git a/src/__tests__/poster-card-test.tsx b/src/__tests__/poster-card-test.tsx index dc25739c5b..bca1d5ef10 100644 --- a/src/__tests__/poster-card-test.tsx +++ b/src/__tests__/poster-card-test.tsx @@ -5,6 +5,7 @@ import {render, screen} from '@testing-library/react'; import ThemeContextProvider from '../theme-context-provider'; import Stack from '../stack'; import {Text2} from '../text'; +import userEvent from '@testing-library/user-event'; test('PosterCard "href" label', async () => { render( @@ -83,3 +84,24 @@ test('PosterCard "onPress" label', async () => { name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', }); }); + +test('PosterCard onClose custom label', async () => { + const closeSpy = jest.fn(); + + render( + + {}} + isInverse + title="Title" + description="Description" + /> + + ); + + const closeButton = await screen.findByRole('button', {name: 'custom close label'}); + await userEvent.click(closeButton); + expect(closeSpy).toHaveBeenCalledTimes(1); +}); diff --git a/src/__tests__/snackbar-test.tsx b/src/__tests__/snackbar-test.tsx index dfa692b613..0f4e8cd95f 100644 --- a/src/__tests__/snackbar-test.tsx +++ b/src/__tests__/snackbar-test.tsx @@ -77,6 +77,32 @@ test('Snackbar with dismiss button', async () => { }); }); +test('Snackbar with dismiss button and custom label', async () => { + const onCloseSpy = jest.fn(); + + render( + + + + ); + expect(screen.getByText('Some message')).toBeInTheDocument(); + const dismissButton = await screen.findByRole('button', {name: 'custom close label'}); + expect(dismissButton).toBeInTheDocument(); + + await userEvent.click(dismissButton); + + await waitFor(() => { + expect(onCloseSpy).toHaveBeenCalledWith({action: 'DISMISS'}); + }); +}); + test('Snackbar does not have dismiss button by default', async () => { render( diff --git a/src/callout.tsx b/src/callout.tsx index f41d3a0de3..a9a9dce916 100644 --- a/src/callout.tsx +++ b/src/callout.tsx @@ -25,6 +25,7 @@ type Props = { titleAs?: HeadingType; description: string; onClose?: () => void; + closeButtonLabel?: string; icon?: React.ReactElement; button?: RendersNullableElement; secondaryButton?: RendersNullableElement; @@ -41,6 +42,7 @@ const Callout: React.FC = ({ description, icon, onClose, + closeButtonLabel, button, secondaryButton, buttonLink, @@ -105,7 +107,7 @@ const Callout: React.FC = ({ bleedRight Icon={IconCloseRegular} onPress={onClose} - aria-label={texts.closeButtonLabel} + aria-label={closeButtonLabel ?? texts.closeButtonLabel} /> )} diff --git a/src/card.tsx b/src/card.tsx index 6b875de331..5479755687 100644 --- a/src/card.tsx +++ b/src/card.tsx @@ -84,13 +84,17 @@ export type CardAction = { trackingEvent?: TrackingEvent | ReadonlyArray; } & ExclusifyUnion; -const useTopActions = (actions?: ReadonlyArray, onClose?: () => void) => { +const useTopActions = ( + actions?: ReadonlyArray, + onClose?: () => void, + closeButtonLabel?: string +) => { const {texts} = useTheme(); const finalActions = actions ? [...actions] : []; if (onClose) { finalActions.push({ - label: texts.closeButtonLabel, + label: closeButtonLabel ?? texts.closeButtonLabel, onPress: onClose, Icon: IconCloseRegular, }); @@ -104,6 +108,7 @@ const CardActionTypeContext = React.createContext<'default' | 'inverse' | 'media type CardActionsGroupProps = { actions?: ReadonlyArray; onClose?: () => void; + closeButtonLabel?: string; padding?: number; type?: 'default' | 'inverse' | 'media'; }; @@ -151,8 +156,9 @@ export const CardActionsGroup = ({ padding = 16, onClose, type = 'default', + closeButtonLabel, }: CardActionsGroupProps): JSX.Element => { - const finalActions = useTopActions(actions, onClose); + const finalActions = useTopActions(actions, onClose, closeButtonLabel); const hasActions = finalActions.length > 0; return hasActions ? ( @@ -520,6 +526,7 @@ interface MediaCardBaseProps { dataAttributes?: DataAttributes; 'aria-label'?: string; onClose?: () => void; + closeButtonLabel?: string; } type MediaCardProps = MediaCardBaseProps & @@ -553,6 +560,7 @@ export const MediaCard = React.forwardRef( dataAttributes, 'aria-label': ariaLabelProp, onClose, + closeButtonLabel, ...touchableProps }, ref @@ -621,7 +629,12 @@ export const MediaCard = React.forwardRef( - + ); } @@ -649,6 +662,7 @@ export const NakedCard = React.forwardRef( dataAttributes, 'aria-label': ariaLabelProp, onClose, + closeButtonLabel, ...touchableProps }, ref @@ -722,7 +736,12 @@ export const NakedCard = React.forwardRef( )} - + ); } @@ -856,6 +875,7 @@ interface DataCardBaseProps { dataAttributes?: DataAttributes; 'aria-label'?: string; onClose?: () => void; + closeButtonLabel?: string; } type DataCardProps = DataCardBaseProps & @@ -888,6 +908,7 @@ export const DataCard = React.forwardRef( dataAttributes, 'aria-label': ariaLabelProp, onClose, + closeButtonLabel, aspectRatio, ...touchableProps }, @@ -968,7 +989,12 @@ export const DataCard = React.forwardRef( - + ); } @@ -1149,6 +1175,7 @@ interface CommonDisplayCardProps { icon?: React.ReactElement; actions?: ReadonlyArray; onClose?: () => void; + closeButtonLabel?: string; dataAttributes?: DataAttributes; headline?: React.ReactComponentElement; pretitle?: string; @@ -1225,6 +1252,7 @@ const DisplayCard = React.forwardRef( button, secondaryButton, onClose, + closeButtonLabel, actions, buttonLink, dataAttributes, @@ -1402,6 +1430,7 @@ const DisplayCard = React.forwardRef( @@ -1441,6 +1470,7 @@ interface PosterCardBaseProps { icon?: React.ReactElement; actions?: ReadonlyArray; onClose?: () => void; + closeButtonLabel?: string; dataAttributes?: DataAttributes; headline?: string | RendersNullableElement; pretitle?: string; @@ -1495,6 +1525,7 @@ export const PosterCard = React.forwardRef( ['aria-label']: ariaLabelProp = deprecatedAriaLabel, actions, onClose, + closeButtonLabel, icon, headline, pretitle, @@ -1702,6 +1733,7 @@ export const PosterCard = React.forwardRef( void; + closeButtonLabel?: string; } interface ToggleChipProps extends SimpleChipProps { @@ -37,7 +38,7 @@ type ClickableChipProps = TouchableComponentProps; const Chip: React.FC = (props: ChipProps) => { - const {Icon, children, id, dataAttributes, active, badge, onClose} = props; + const {Icon, children, id, dataAttributes, active, badge, onClose, closeButtonLabel} = props; const {texts, isDarkMode, textPresets} = useTheme(); const overAlternative = useThemeVariant() === 'alternative'; @@ -83,7 +84,7 @@ const Chip: React.FC = (props: ChipProps) => { width: pxToRem(24), height: pxToRem(24), }} - aria-label={texts.closeButtonLabel} + aria-label={closeButtonLabel ?? texts.closeButtonLabel} onPress={() => onClose()} > diff --git a/src/dialog.tsx b/src/dialog.tsx index 2812e27e52..7679af5a5e 100644 --- a/src/dialog.tsx +++ b/src/dialog.tsx @@ -32,6 +32,7 @@ interface BaseDialogProps { acceptText?: string; onAccept?: () => void; destructive?: boolean; + closeButtonLabel?: string; /** @deprecated this does nothing */ forceWeb?: boolean; /** @deprecated this does nothing */ @@ -392,7 +393,10 @@ const ModalDialog = (props: ModalDialogProps): JSX.Element => {
void; + closeButtonLabel?: string; position?: Position; width?: number; trackingEvent?: TrackingEvent | ReadonlyArray; @@ -33,6 +34,7 @@ const Popover: React.FC = ({ extra, children, onClose = () => {}, + closeButtonLabel, dataAttributes, trackingEvent, title, @@ -72,6 +74,7 @@ const Popover: React.FC = ({ hasPointerInteractionOnly delay={false} onClose={onClose} + closeButtonLabel={closeButtonLabel} trackingEvent={trackingEvent} dataAttributes={{'component-name': 'Popover', ...dataAttributes}} {...props} diff --git a/src/snackbar.tsx b/src/snackbar.tsx index 6ff589c65f..1204b93302 100644 --- a/src/snackbar.tsx +++ b/src/snackbar.tsx @@ -26,6 +26,7 @@ const DEFAULT_DURATION_WITH_BUTTON = 10000; export type Props = { buttonText?: string; buttonAccessibilityLabel?: string; + closeButtonLabel?: string; duration?: number; message: string; onClose?: SnackbarCloseHandler; @@ -45,6 +46,7 @@ const SnackbarComponent = React.forwardRef( message, buttonText, buttonAccessibilityLabel, + closeButtonLabel, duration, onClose, type, @@ -170,7 +172,7 @@ const SnackbarComponent = React.forwardRef( onPress={() => { close({action: 'DISMISS'}); }} - aria-label={texts.closeButtonLabel} + aria-label={closeButtonLabel ?? texts.closeButtonLabel} className={styles.dismissButton[hasLongButton ? 'topRight' : 'centered']} style={{display: 'flex', width: 32, height: 32}} > @@ -192,6 +194,7 @@ const Snackbar = React.forwardRef( message, buttonText, buttonAccessibilityLabel, + closeButtonLabel, duration, onClose: onCloseProp = () => {}, type = 'INFORMATIVE', @@ -219,6 +222,7 @@ const Snackbar = React.forwardRef( duration: duration === Infinity ? 'PERSISTENT' : undefined, buttonText, buttonAccessibilityLabel, + closeButtonLabel, type, withDismiss, }) @@ -234,7 +238,16 @@ const Snackbar = React.forwardRef( isOpenRef.current = false; }); } - }, [buttonAccessibilityLabel, buttonText, duration, message, renderNative, type, withDismiss]); + }, [ + buttonAccessibilityLabel, + closeButtonLabel, + buttonText, + duration, + message, + renderNative, + type, + withDismiss, + ]); if (renderNative) { return null; @@ -247,6 +260,7 @@ const Snackbar = React.forwardRef( duration={duration} buttonText={buttonText} buttonAccessibilityLabel={buttonAccessibilityLabel} + closeButtonLabel={closeButtonLabel} type={type} onClose={onCloseRef.current} withDismiss={withDismiss} diff --git a/src/tooltip.tsx b/src/tooltip.tsx index 63109e8972..aa57bc6165 100644 --- a/src/tooltip.tsx +++ b/src/tooltip.tsx @@ -154,6 +154,7 @@ type BaseTooltipProps = { open?: boolean; hasPointerInteractionOnly?: boolean; onClose?: () => void; + closeButtonLabel?: string; trackingEvent?: TrackingEvent | ReadonlyArray; }; @@ -167,6 +168,7 @@ export const BaseTooltip: React.FC = ({ centerContent, open, onClose, + closeButtonLabel, hasPointerInteractionOnly = false, trackingEvent, }) => { @@ -575,7 +577,9 @@ export const BaseTooltip: React.FC = ({ onClose(); }} trackingEvent={trackingEvent} - aria-label={texts.modalClose} + aria-label={ + closeButtonLabel ?? texts.closeButtonLabel + } Icon={IconCloseRegular} small />