diff --git a/src/components/NotificationSystem/NotificationSystem.utils.test.tsx b/src/components/NotificationSystem/NotificationSystem.utils.test.tsx index 3689034a7..34227e6d9 100644 --- a/src/components/NotificationSystem/NotificationSystem.utils.test.tsx +++ b/src/components/NotificationSystem/NotificationSystem.utils.test.tsx @@ -78,10 +78,13 @@ describe('NotificationSystem utils', () => { let spies; - const setup = () => { + const setup = ({ screenReaderAnnouncerIsRegistered = true } = {}) => { spies = { autoClose: jest.spyOn(utils, 'calculateAutoClose'), getContainerID: jest.spyOn(utils, 'getContainerID'), + isRegistered: jest + .spyOn(ScreenReaderAnnouncer, 'isRegistered') + .mockReturnValue(screenReaderAnnouncerIsRegistered), announce: jest.spyOn(ScreenReaderAnnouncer, 'announce').mockReturnValue(), }; }; @@ -100,6 +103,7 @@ describe('NotificationSystem utils', () => { }); expect(spies.autoClose).toHaveBeenCalledWith(options); expect(spies.getContainerID).toHaveBeenCalledWith(notificationSystemId, ATTENTION.MEDIUM); + expect(spies.isRegistered).not.toHaveBeenCalled(); expect(spies.announce).not.toHaveBeenCalled(); }); @@ -124,13 +128,14 @@ describe('NotificationSystem utils', () => { }); expect(spies.autoClose).toHaveBeenCalledWith(options); expect(spies.getContainerID).toHaveBeenCalledWith(notificationSystemId, ATTENTION.MEDIUM); + expect(spies.isRegistered).not.toHaveBeenCalled(); expect(spies.announce).toHaveBeenCalledWith( { body: screenReaderAnnouncement }, notificationSystemId ); }); - it('should announce the screenReaderAnnouncement using the announcerIdentity, if provided', () => { + it('should announce the screenReaderAnnouncement using the announcerIdentity, if provided and registered', () => { setup(); const options = { @@ -152,21 +157,54 @@ describe('NotificationSystem utils', () => { }); expect(spies.autoClose).toHaveBeenCalledWith(options); expect(spies.getContainerID).toHaveBeenCalledWith(notificationSystemId, ATTENTION.MEDIUM); + expect(spies.isRegistered).toHaveBeenCalledWith(announcerIdentity); expect(spies.announce).toHaveBeenCalledWith( { body: screenReaderAnnouncement }, announcerIdentity ); }); + + it('should announce the screenReaderAnnouncement using the notificationSystemId, if announcerIdentity not registered', () => { + setup({ screenReaderAnnouncerIsRegistered: false }); + + const options = { + toastId, + onClose, + autoClose, + notificationSystemId, + attention, + screenReaderAnnouncement, + announcerIdentity, + }; + expect(notify(
, options)).toBe(toastId); + + expect(toast).toHaveBeenCalledWith(
, { + autoClose: autoClose, + containerId: 'test_containerbla_medium_notification_container', + onClose: onClose, + toastId: toastId, + }); + expect(spies.autoClose).toHaveBeenCalledWith(options); + expect(spies.getContainerID).toHaveBeenCalledWith(notificationSystemId, ATTENTION.MEDIUM); + expect(spies.isRegistered).toHaveBeenCalledWith(announcerIdentity); + expect(spies.announce).toHaveBeenCalledWith( + { body: screenReaderAnnouncement }, + notificationSystemId + ); + }); }); describe('update', () => { let toastId; let spies; - const setup = () => { + const setup = ({ screenReaderAnnouncerIsRegistered = true } = {}) => { toastId = notify(
, { notificationSystemId: 'id' }); spies = { getContainerID: jest.spyOn(utils, 'getContainerID'), + isRegistered: jest + .spyOn(ScreenReaderAnnouncer, 'isRegistered') + .mockReturnValue(screenReaderAnnouncerIsRegistered), announce: jest.spyOn(ScreenReaderAnnouncer, 'announce').mockReturnValue(), }; }; @@ -186,6 +224,7 @@ describe('NotificationSystem utils', () => { render:

, }); expect(spies.getContainerID).toHaveBeenCalledWith('id', 'medium'); + expect(spies.isRegistered).not.toHaveBeenCalled(); expect(spies.announce).not.toHaveBeenCalled(); }); @@ -205,10 +244,11 @@ describe('NotificationSystem utils', () => { render:

, }); expect(spies.getContainerID).toHaveBeenCalledWith('id', 'medium'); + expect(spies.isRegistered).not.toHaveBeenCalled(); expect(spies.announce).toHaveBeenCalledWith({ body: 'some screenreader announcement' }, 'id'); }); - it('should announce the screenReaderAnnouncement using the announcerIdentity, if provided', () => { + it('should announce the screenReaderAnnouncement using the announcerIdentity, if provided and registered', () => { setup(); update(toastId, { @@ -225,11 +265,33 @@ describe('NotificationSystem utils', () => { render:

, }); expect(spies.getContainerID).toHaveBeenCalledWith('id', 'medium'); + expect(spies.isRegistered).toHaveBeenCalledWith('some_announcer_id'); expect(spies.announce).toHaveBeenCalledWith( { body: 'some screenreader announcement' }, 'some_announcer_id' ); }); + + it('should announce the screenReaderAnnouncement using the notificationSystemId, if announcerIdentity not registered', () => { + setup({ screenReaderAnnouncerIsRegistered: false }); + + update(toastId, { + toastId: 'new', + render:

, + attention: ATTENTION.MEDIUM, + notificationSystemId: 'id', + screenReaderAnnouncement: 'some screenreader announcement', + announcerIdentity: 'some_announcer_id', + }); + expect(toast.update).toHaveBeenCalledWith(toastId, { + containerId: 'id_medium_notification_container', + toastId: 'new', + render:

, + }); + expect(spies.getContainerID).toHaveBeenCalledWith('id', 'medium'); + expect(spies.isRegistered).toHaveBeenCalledWith('some_announcer_id'); + expect(spies.announce).toHaveBeenCalledWith({ body: 'some screenreader announcement' }, 'id'); + }); }); describe('dismiss', () => { diff --git a/src/components/NotificationSystem/NotificationSystem.utils.ts b/src/components/NotificationSystem/NotificationSystem.utils.ts index dc2299dcf..58a1659c4 100644 --- a/src/components/NotificationSystem/NotificationSystem.utils.ts +++ b/src/components/NotificationSystem/NotificationSystem.utils.ts @@ -46,13 +46,20 @@ export const notify = (content: ToastContent, options: NotifyOptionsType): Id => attention, onClose, role, - announcerIdentity, + announcerIdentity: announcerIdentityFromOptions, } = options; if (screenReaderAnnouncement) { - ScreenReaderAnnouncer.announce( - { body: screenReaderAnnouncement }, - announcerIdentity || notificationSystemId - ); + let announcerIdentity = notificationSystemId; + if (announcerIdentityFromOptions) { + if (ScreenReaderAnnouncer.isRegistered(announcerIdentityFromOptions)) { + announcerIdentity = announcerIdentityFromOptions; + } else { + console.warn( + `ScreenReaderAnnouncer with identity ${announcerIdentityFromOptions} is not registered, falling back to ${notificationSystemId}` + ); + } + } + ScreenReaderAnnouncer.announce({ body: screenReaderAnnouncement }, announcerIdentity); } return toast(content, { toastId: toastId, @@ -74,14 +81,21 @@ export const update = (toastId: Id, options: UpdateOptionsType): void => { notificationSystemId, attention, screenReaderAnnouncement, - announcerIdentity, + announcerIdentity: announcerIdentityFromOptions, ...updateOptions } = options; if (screenReaderAnnouncement) { - ScreenReaderAnnouncer.announce( - { body: screenReaderAnnouncement }, - announcerIdentity || notificationSystemId - ); + let announcerIdentity = notificationSystemId; + if (announcerIdentityFromOptions) { + if (ScreenReaderAnnouncer.isRegistered(announcerIdentityFromOptions)) { + announcerIdentity = announcerIdentityFromOptions; + } else { + console.warn( + `ScreenReaderAnnouncer with identity ${announcerIdentityFromOptions} is not registered, falling back to ${notificationSystemId}` + ); + } + } + ScreenReaderAnnouncer.announce({ body: screenReaderAnnouncement }, announcerIdentity); } toast.update(toastId, { ...updateOptions, diff --git a/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.tsx b/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.tsx index d9f9f2b66..f5690b57a 100644 --- a/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.tsx +++ b/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.tsx @@ -11,6 +11,7 @@ import { AnnouncerProps, CompoundProps, ScreenReaderAnnouncerAnnounce, + ScreenReaderAnnouncerIsRegistered, } from './ScreenReaderAnnouncer.types'; const registry: Record = {}; @@ -37,6 +38,9 @@ const deregister = (identity: string) => { delete registry[identity]; }; +const isRegistered: ScreenReaderAnnouncerIsRegistered = (announcerIdentity) => + !!registry[announcerIdentity]; + /** * Announce a message via a screen reader. * To allow for multiple announcers to exist concurrently, an announcer identity can be provided. @@ -146,3 +150,4 @@ const ScreenReaderAnnouncer: FC & CompoundProps = ({ export default ScreenReaderAnnouncer; ScreenReaderAnnouncer.announce = announce; +ScreenReaderAnnouncer.isRegistered = isRegistered; diff --git a/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.types.ts b/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.types.ts index 927f7c443..ea13844e4 100644 --- a/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.types.ts +++ b/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.types.ts @@ -3,6 +3,7 @@ import { ReactNode } from 'react'; type Level = 'assertive' | 'polite'; export interface CompoundProps { announce: ScreenReaderAnnouncerAnnounce; + isRegistered: ScreenReaderAnnouncerIsRegistered; } type AnnounceOptions = { @@ -29,6 +30,7 @@ type AnnounceOptions = { timeout?: number; }; type ScreenReaderAnnouncerAnnounce = (options: AnnounceOptions, announcerIdentity?: string) => void; +type ScreenReaderAnnouncerIsRegistered = (announcerIdentity: string) => boolean; type Announce = (options: AnnounceOptions) => void; type Clear = (options: { messageIdentity: string }) => void; type Message = { @@ -50,6 +52,7 @@ type AnnouncerProps = { identity?: string }; export type { ScreenReaderAnnouncerAnnounce, + ScreenReaderAnnouncerIsRegistered, Level, AnnounceOptions, Announce, diff --git a/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.unit.test.tsx b/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.unit.test.tsx index 144be9aec..a3cb20d93 100644 --- a/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.unit.test.tsx +++ b/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.unit.test.tsx @@ -404,5 +404,17 @@ describe('', () => { // This would error if the announcement timer was not cleared advanceTimers(delay); }); + + it('isRegistered returns correctly', () => { + setup(); + + if (announcerIdentity) { + expect(ScreenReaderAnnouncer.isRegistered(announcerIdentity)).toBe(true); + expect(ScreenReaderAnnouncer.isRegistered('default')).toBe(false); + } else { + expect(ScreenReaderAnnouncer.isRegistered('custom-identity')).toBe(false); + expect(ScreenReaderAnnouncer.isRegistered('default')).toBe(true); + } + }); }); }); diff --git a/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.unit.test.tsx.snap b/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.unit.test.tsx.snap index f84dc5b0e..f9402551d 100644 --- a/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.unit.test.tsx.snap +++ b/src/components/ScreenReaderAnnouncer/ScreenReaderAnnouncer.unit.test.tsx.snap @@ -1500,6 +1500,16 @@ exports[` Announcer identity: custom-identity errors wh

`; +exports[` Announcer identity: custom-identity isRegistered returns correctly 1`] = ` +
+
+
+`; + exports[` Announcer identity: undefined announces react node with default configuration 1`] = `
Announcer identity: undefined errors when reg />
`; + +exports[` Announcer identity: undefined isRegistered returns correctly 1`] = ` +
+
+
+`;