Skip to content
This repository has been archived by the owner on May 24, 2024. It is now read-only.

[terra-alert] Update notification banner for screen reader experience #3908

Merged
merged 9 commits into from
Sep 22, 2023
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
3 changes: 3 additions & 0 deletions packages/terra-alert/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Added
* Added visually hidden default title for screen readers to read with custom titles.

## 4.80.0 - (September 21, 2023)

* Changed
Expand Down
1 change: 1 addition & 0 deletions packages/terra-alert/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"terra-icon": "^3.57.1",
"terra-responsive-element": "^5.37.0",
"terra-theme-context": "^1.0.0",
"terra-visually-hidden-text": "^2.36.0",
"uuid": "3.4.0"
},
"scripts": {
Expand Down
2 changes: 2 additions & 0 deletions packages/terra-alert/src/Alert.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid';

import ResponsiveElement from 'terra-responsive-element';
import Button from 'terra-button';
import VisuallyHiddenText from 'terra-visually-hidden-text';
import {
IconAlert,
IconError,
Expand Down Expand Up @@ -202,6 +203,7 @@ const Alert = ({
<div id={alertMessageId} className={alertSectionClassName}>
{(title || defaultTitle) && (
<strong id={alertTitleId} className={cx('title')}>
{title && defaultTitle && <VisuallyHiddenText text={`${defaultTitle},`} />}
{title || defaultTitle}
</strong>
)}
Expand Down
131 changes: 108 additions & 23 deletions packages/terra-alert/tests/jest/Alert.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import IconInformation from 'terra-icon/lib/icon/IconInformation';
import IconSuccess from 'terra-icon/lib/icon/IconSuccess';
import IconWarning from 'terra-icon/lib/icon/IconWarning';
import Button from 'terra-button';
import VisuallyHiddenText from 'terra-visually-hidden-text';
import { v4 as uuidv4 } from 'uuid';
import Alert from '../../src/Alert';

Expand Down Expand Up @@ -66,17 +67,34 @@ describe('Alert with props', () => {
expect(alert.prop('disableAlertActionFocus')).toEqual(true);
expect(wrapper).toMatchSnapshot();
});

it('should render an alert with provided title', () => {
const wrapper = shallowWithIntl(<Alert title="Custom Title" />).dive();
const alertTitle = wrapper.find('.title');

expect(alertTitle.prop('children')).toEqual(
[
<VisuallyHiddenText text="Terra.alert.alert," />,
'Custom Title',
],
);
expect(alertTitle.text()).toEqual('<VisuallyHiddenText />Custom Title');
expect(wrapper).toMatchSnapshot();
});
});

describe('Dismissible Alert that includes actions section', () => {
it('should render an alert component with a dismiss button', () => {
const mockOnDismiss = jest.fn();
const wrapper = shallowWithIntl(<Alert onDismiss={mockOnDismiss}>This is a test</Alert>).dive();

expect(wrapper.find(Button).length).toEqual(1);
expect(wrapper.find(Button).prop('text')).toEqual('Terra.alert.dismiss');
expect(wrapper.find(Button).prop('onClick')).toEqual(mockOnDismiss);
expect(wrapper.find(Button).prop('variant')).toEqual('neutral');
const dismissButton = wrapper.find(Button);
expect(wrapper.find('.title').text()).toEqual('Terra.alert.alert');
expect(dismissButton.length).toEqual(1);
expect(dismissButton.prop('text')).toEqual('Terra.alert.dismiss');
expect(dismissButton.prop('onClick')).toEqual(mockOnDismiss);
expect(dismissButton.prop('variant')).toEqual('neutral');
expect(dismissButton.prop('aria-describedby')).toEqual('alert-title-00000000-0000-0000-0000-000000000000');
expect(wrapper).toMatchSnapshot();
});
});
Expand Down Expand Up @@ -199,11 +217,20 @@ describe('Alert of type custom with custom title and text content', () => {

describe('Alert of type info with custom title and HTML content', () => {
it('should render an Alert component of type info with custom title and HTML content', () => {
const wrapper = shallowWithIntl(<Alert type={Alert.Opts.Types.INFO} title="Gettysburg Address"><span>Four score and seven years ago . . .</span></Alert>);
const wrapper = shallowWithIntl(<Alert type={Alert.Opts.Types.INFO} title="Gettysburg Address"><span>Four score and seven years ago . . .</span></Alert>).dive();

expect(wrapper.prop('title')).toEqual('Gettysburg Address');
expect(wrapper.prop('type')).toEqual('info');
expect(wrapper.find('span').text()).toEqual('Four score and seven years ago . . .');
const alertDiv = wrapper.find('div.alert-base');
expect(alertDiv.prop('className')).toEqual('alert-base info wide');
expect(alertDiv.prop('role')).toEqual('status');
expect(wrapper.find(IconInformation).length).toEqual(1);
expect(wrapper.find('.title').prop('children')).toEqual(
[
<VisuallyHiddenText text="Terra.alert.info," />,
'Gettysburg Address',
],
);
expect(wrapper.find('.title').text()).toEqual('<VisuallyHiddenText />Gettysburg Address');
expect(wrapper.find('.section').find('span').text()).toEqual('Four score and seven years ago . . .');
expect(wrapper).toMatchSnapshot();
});
});
Expand All @@ -226,7 +253,7 @@ describe('Alert of type success with an action button text content', () => {
});
});

describe('Dismissable Alert of type custom with action button, custom title and text content', () => {
describe('Dismissible Alert of type custom with action button, custom title and text content', () => {
it('should render an Alert component of type custom with an action button', () => {
const mockOnClick = jest.fn();
const mockOnDismiss = jest.fn();
Expand All @@ -251,10 +278,10 @@ describe('Dismissable Alert of type custom with action button, custom title and
});
});

describe('Dismissible Alert', () => {
describe('Notifications titles', () => {
let wrapper;

describe('Custom Alert with no title prop', () => {
describe('Custom notification with no title prop', () => {
beforeEach(() => {
wrapper = shallowWithIntl(
<Alert
Expand All @@ -266,18 +293,23 @@ describe('Dismissible Alert', () => {
</Alert>,
).dive();
});
it('should set the alert message ID', () => {
it('should set the notification message ID', () => {
const alertContent = wrapper.find('.section');
expect(alertContent.prop('id')).toEqual('alert-message-00000000-0000-0000-0000-000000000000');
});

it('should set the dismiss button aria-describedby to the alert description', () => {
it('should have no title', () => {
const alertTitle = wrapper.find('.title');
expect(alertTitle.length).toEqual(0);
});

it('should set the dismiss button aria-describedby to the notification description', () => {
const dismissButton = wrapper.find('Button');
expect(dismissButton.prop('aria-describedby')).toEqual('alert-message-00000000-0000-0000-0000-000000000000');
});
});

describe('Custom Alert with custom title', () => {
describe('Custom notification with custom title', () => {
beforeEach(() => {
wrapper = shallowWithIntl(
<Alert
Expand All @@ -301,13 +333,19 @@ describe('Dismissible Alert', () => {
expect(alertTitle.prop('id')).toEqual('alert-title-00000000-0000-0000-0000-000000000000');
});

it('should set the alert title', () => {
const alertTitle = wrapper.find('.title');
expect(alertTitle.prop('children')).toEqual(['', 'Help!']);
expect(alertTitle.text()).toEqual('Help!');
});

it('should set the dismiss button aria-describedby to the alert title', () => {
const dismissButton = wrapper.find('Button');
expect(dismissButton.prop('aria-describedby')).toEqual('alert-title-00000000-0000-0000-0000-000000000000');
});
});

describe('Success Alert with no title prop', () => {
describe('Success notification with no title prop', () => {
beforeEach(() => {
wrapper = shallowWithIntl(
<Alert
Expand All @@ -319,23 +357,29 @@ describe('Dismissible Alert', () => {
).dive();
});

it('should set the alert message ID', () => {
it('should set the notification message ID', () => {
const alertContent = wrapper.find('.section');
expect(alertContent.prop('id')).toEqual('alert-message-00000000-0000-0000-0000-000000000000');
});

it('should set the alert title ID', () => {
it('should set the notification title ID', () => {
const alertTitle = wrapper.find('.title');
expect(alertTitle.prop('id')).toEqual('alert-title-00000000-0000-0000-0000-000000000000');
});

it('should set the dismiss button aria-describedby to the alert title', () => {
it('should use the default title', () => {
const alertTitle = wrapper.find('.title');
expect(alertTitle.prop('children')).toEqual([undefined, 'Terra.alert.success']);
expect(alertTitle.text()).toEqual('Terra.alert.success');
});

it('should set the dismiss button aria-describedby to the notification title', () => {
const dismissButton = wrapper.find('Button');
expect(dismissButton.prop('aria-describedby')).toEqual('alert-title-00000000-0000-0000-0000-000000000000');
});
});

describe('Success Alert with blank title', () => {
describe('Success notification with blank title', () => {
beforeEach(() => {
wrapper = shallowWithIntl(
<Alert
Expand All @@ -350,20 +394,61 @@ describe('Dismissible Alert', () => {

it('should use the default title', () => {
const alertTitle = wrapper.find('.title');
expect(alertTitle.prop('children')).toEqual('Terra.alert.success');
expect(alertTitle.prop('children')).toEqual(['', 'Terra.alert.success']);
expect(alertTitle.text()).toEqual('Terra.alert.success');
});

it('should set the alert message ID', () => {
it('should set the notification message ID', () => {
const alertContent = wrapper.find('.section');
expect(alertContent.prop('id')).toEqual('alert-message-00000000-0000-0000-0000-000000000000');
});

it('should set the alert title ID', () => {
it('should set the notification title ID', () => {
const alertTitle = wrapper.find('.title');
expect(alertTitle.prop('id')).toEqual('alert-title-00000000-0000-0000-0000-000000000000');
});

it('should set the dismiss button aria-describedby to the alert title', () => {
it('should set the dismiss button aria-describedby to the notification title', () => {
const dismissButton = wrapper.find('Button');
expect(dismissButton.prop('aria-describedby')).toEqual('alert-title-00000000-0000-0000-0000-000000000000');
});
});

describe('Success notification with title', () => {
beforeEach(() => {
wrapper = shallowWithIntl(
<Alert
type={Alert.Opts.Types.SUCCESS}
title="Custom Success Title"
onDismiss={() => { }}
>
This is a success alert.
</Alert>,
).dive();
});

it('should use the custom title', () => {
const alertTitle = wrapper.find('.title');
expect(alertTitle.prop('children')).toEqual(
[
<VisuallyHiddenText text="Terra.alert.success," />,
'Custom Success Title',
],
);
expect(alertTitle.text()).toEqual('<VisuallyHiddenText />Custom Success Title');
});

it('should set the notification message ID', () => {
const alertContent = wrapper.find('.section');
expect(alertContent.prop('id')).toEqual('alert-message-00000000-0000-0000-0000-000000000000');
});

it('should set the notification title ID', () => {
const alertTitle = wrapper.find('.title');
expect(alertTitle.prop('id')).toEqual('alert-title-00000000-0000-0000-0000-000000000000');
});

it('should set the dismiss button aria-describedby to the notification title', () => {
const dismissButton = wrapper.find('Button');
expect(dismissButton.prop('aria-describedby')).toEqual('alert-title-00000000-0000-0000-0000-000000000000');
});
Expand Down
Loading
Loading