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
Expand Up @@ -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(),
};
};
Expand All @@ -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();
});

Expand All @@ -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 = {
Expand All @@ -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(<div />, options)).toBe(toastId);

expect(toast).toHaveBeenCalledWith(<div />, {
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(<div />, { notificationSystemId: 'id' });
spies = {
getContainerID: jest.spyOn(utils, 'getContainerID'),
isRegistered: jest
.spyOn(ScreenReaderAnnouncer, 'isRegistered')
.mockReturnValue(screenReaderAnnouncerIsRegistered),
announce: jest.spyOn(ScreenReaderAnnouncer, 'announce').mockReturnValue(),
};
};
Expand All @@ -186,6 +224,7 @@ describe('NotificationSystem utils', () => {
render: <p />,
});
expect(spies.getContainerID).toHaveBeenCalledWith('id', 'medium');
expect(spies.isRegistered).not.toHaveBeenCalled();
expect(spies.announce).not.toHaveBeenCalled();
});

Expand All @@ -205,10 +244,11 @@ describe('NotificationSystem utils', () => {
render: <p />,
});
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, {
Expand All @@ -225,11 +265,33 @@ describe('NotificationSystem utils', () => {
render: <p />,
});
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: <p />,
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: <p />,
});
expect(spies.getContainerID).toHaveBeenCalledWith('id', 'medium');
expect(spies.isRegistered).toHaveBeenCalledWith('some_announcer_id');
expect(spies.announce).toHaveBeenCalledWith({ body: 'some screenreader announcement' }, 'id');
});
});

describe('dismiss', () => {
Expand Down
34 changes: 24 additions & 10 deletions src/components/NotificationSystem/NotificationSystem.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
AnnouncerProps,
CompoundProps,
ScreenReaderAnnouncerAnnounce,
ScreenReaderAnnouncerIsRegistered,
} from './ScreenReaderAnnouncer.types';

const registry: Record<string, { announce: Announce }> = {};
Expand All @@ -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.
Expand Down Expand Up @@ -146,3 +150,4 @@ const ScreenReaderAnnouncer: FC<AnnouncerProps> & CompoundProps = ({
export default ScreenReaderAnnouncer;

ScreenReaderAnnouncer.announce = announce;
ScreenReaderAnnouncer.isRegistered = isRegistered;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ReactNode } from 'react';
type Level = 'assertive' | 'polite';
export interface CompoundProps {
announce: ScreenReaderAnnouncerAnnounce;
isRegistered: ScreenReaderAnnouncerIsRegistered;
}

type AnnounceOptions = {
Expand All @@ -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 = {
Expand All @@ -50,6 +52,7 @@ type AnnouncerProps = { identity?: string };

export type {
ScreenReaderAnnouncerAnnounce,
ScreenReaderAnnouncerIsRegistered,
Level,
AnnounceOptions,
Announce,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,5 +404,17 @@ describe('<ScreenReaderAnnouncer />', () => {
// 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);
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,16 @@ exports[`<ScreenReaderAnnouncer /> Announcer identity: custom-identity errors wh
</div>
`;

exports[`<ScreenReaderAnnouncer /> Announcer identity: custom-identity isRegistered returns correctly 1`] = `
<div>
<div
class="md-screen-reader-announcer-wrapper"
data-testid="screen-reader-announcer"
style="clip-path: inset(50%); height: 1px; overflow: hidden; position: absolute; width: 1px; white-space: nowrap;"
/>
</div>
`;

exports[`<ScreenReaderAnnouncer /> Announcer identity: undefined announces react node with default configuration 1`] = `
<div>
<div
Expand Down Expand Up @@ -2999,3 +3009,13 @@ exports[`<ScreenReaderAnnouncer /> Announcer identity: undefined errors when reg
/>
</div>
`;

exports[`<ScreenReaderAnnouncer /> Announcer identity: undefined isRegistered returns correctly 1`] = `
<div>
<div
class="md-screen-reader-announcer-wrapper"
data-testid="screen-reader-announcer"
style="clip-path: inset(50%); height: 1px; overflow: hidden; position: absolute; width: 1px; white-space: nowrap;"
/>
</div>
`;
Loading