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

Commit

Permalink
[terra-alert] Update alert roles for a11y (#3789)
Browse files Browse the repository at this point in the history
  • Loading branch information
trandrew1023 authored May 9, 2023
1 parent 2389a4e commit ef36e66
Show file tree
Hide file tree
Showing 27 changed files with 885 additions and 1,952 deletions.
1 change: 1 addition & 0 deletions packages/terra-alert/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Changed
* Added screen reader support to programmatically associate an alert's text to its dismiss button.
* Added text-wrapping at high magnification and narrow screen widths.
* Changed Alert role attribute with default of "status" for all types except critical alert type and added new optional `role` prop.

## 4.67.0 - (April 27, 2023)

Expand Down
20 changes: 16 additions & 4 deletions packages/terra-alert/src/Alert.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ const propTypes = {
* intl object programmatically imported through injectIntl from react-intl.
* */
intl: PropTypes.shape({ formatMessage: PropTypes.func }).isRequired,
/**
* The ARIA role attribute of the alert. If not provided, alert type _alert_ will default to role `"alert"`,
* all other alert types will use the role of `"status"`.
*/
role: PropTypes.string,
/**
* The title for the alert which will be bolded.
*/
Expand Down Expand Up @@ -119,6 +124,7 @@ const Alert = ({
customColorClass,
onDismiss,
intl,
role,
title,
type,
...customProps
Expand All @@ -127,6 +133,7 @@ const Alert = ({
const [isNarrow, setIsNarrow] = useState();

const defaultTitle = type === AlertTypes.CUSTOM ? '' : intl.formatMessage({ id: `Terra.alert.${type}` });
const defaultRole = type === AlertTypes.ALERT ? 'alert' : 'status';
const alertClassNames = classNames(
cx(
'alert-base',
Expand Down Expand Up @@ -173,10 +180,16 @@ const Alert = ({
);
}

const alertSectionClassName = cx('section', { 'section-custom': type === AlertTypes.CUSTOM });
const alertSectionClassName = cx('section', {
'section-custom': type === AlertTypes.CUSTOM,
});
const alertMessageContent = (
<div id={alertMessageId} className={alertSectionClassName}>
{(title || defaultTitle) && <strong id={alertTitleId} className={cx('title')}>{title || defaultTitle}</strong>}
{(title || defaultTitle) && (
<strong id={alertTitleId} className={cx('title')}>
{title || defaultTitle}
</strong>
)}
{children}
</div>
);
Expand All @@ -190,15 +203,14 @@ const Alert = ({
}
}}
>
<div role="alert" {...customProps} className={alertClassNames}>
<div role={role || defaultRole} {...customProps} className={alertClassNames}>
<div className={bodyClassNameForParent}>
{getAlertIcon(type, customIcon)}
{alertMessageContent}
</div>
{actionsSection}
</div>
</ResponsiveElement>

);
};

Expand Down
150 changes: 125 additions & 25 deletions packages/terra-alert/tests/jest/Alert.test.jsx
Original file line number Diff line number Diff line change
@@ -1,119 +1,219 @@
import React from 'react';
/* eslint-disable-next-line import/no-extraneous-dependencies */
import { shallowWithIntl, mountWithIntl } from 'terra-enzyme-intl';
import IconAlert from 'terra-icon/lib/icon/IconAlert';
import IconDiamondSymbol from 'terra-icon/lib/icon/IconDiamondSymbol';
import IconError from 'terra-icon/lib/icon/IconError';
import IconGapChecking from 'terra-icon/lib/icon/IconGapChecking';
import IconHelp from 'terra-icon/lib/icon/IconHelp';
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 Alert from '../../src/Alert';

const mockUUID = '00000000-0000-0000-0000-000000000000';
jest.mock('uuid', () => ({ v4: () => mockUUID }));

describe('Alert with no props', () => {
// Snapshot Tests
it('should render a default component', () => {
const wrapper = mountWithIntl(<Alert />);
expect(wrapper).toMatchSnapshot();
});
});

describe('Alert with role prop', () => {
it('should render a alert with provided role', () => {
const wrapper = shallowWithIntl(<Alert role="status" />).dive();

const alertDiv = wrapper.find('div.alert-base');
expect(alertDiv.prop('className')).toEqual('alert-base alert wide');
expect(alertDiv.prop('role')).toEqual('status');
expect(wrapper.find(IconAlert).length).toEqual(1);
expect(wrapper.find('.title').text()).toEqual('Terra.alert.alert');
expect(wrapper).toMatchSnapshot();
});
});

describe('Dismissible Alert that includes actions section', () => {
// Snapshot Tests
it('should render an alert component with a dismiss button', () => {
const wrapper = mountWithIntl(<Alert onDismiss={() => { }}>This is a test</Alert>);
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');
expect(wrapper).toMatchSnapshot();
});
});

describe('Alert of type alert with text content', () => {
// Snapshot Tests
it('should render an Alert component of type alert', () => {
const wrapper = mountWithIntl(<Alert type={Alert.Opts.Types.ALERT}>This is a test</Alert>);
const wrapper = shallowWithIntl(<Alert type={Alert.Opts.Types.ALERT}>This is a test</Alert>).dive();

const alertDiv = wrapper.find('div.alert-base');
expect(alertDiv.prop('className')).toEqual('alert-base alert wide');
expect(alertDiv.prop('role')).toEqual('alert');
expect(wrapper.find(IconAlert).length).toEqual(1);
expect(wrapper.find('.title').text()).toEqual('Terra.alert.alert');
expect(wrapper).toMatchSnapshot();
});
});

describe('Alert of type error with text content', () => {
// Snapshot Tests
it('should render an Alert component of type error', () => {
const wrapper = mountWithIntl(<Alert type={Alert.Opts.Types.ERROR}>This is an error.</Alert>);
const wrapper = shallowWithIntl(<Alert type={Alert.Opts.Types.ERROR}>This is an error.</Alert>).dive();

const alertDiv = wrapper.find('div.alert-base');
expect(alertDiv.prop('className')).toEqual('alert-base error wide');
expect(alertDiv.prop('role')).toEqual('status');
expect(wrapper.find(IconError).length).toEqual(1);
expect(wrapper.find('.title').text()).toEqual('Terra.alert.error');
expect(wrapper).toMatchSnapshot();
});
});

describe('Alert of type warning with text content', () => {
// Snapshot Tests
it('should render an Alert component of type warning', () => {
const wrapper = mountWithIntl(<Alert type={Alert.Opts.Types.WARNING}>This is an warning.</Alert>);
const wrapper = shallowWithIntl(<Alert type={Alert.Opts.Types.WARNING}>This is an warning.</Alert>).dive();

const alertDiv = wrapper.find('div.alert-base');
expect(alertDiv.prop('className')).toEqual('alert-base warning wide');
expect(alertDiv.prop('role')).toEqual('status');
expect(wrapper.find(IconWarning).length).toEqual(1);
expect(wrapper.find('.title').text()).toEqual('Terra.alert.warning');
expect(wrapper).toMatchSnapshot();
});
});

describe('Alert of type advisory with text content', () => {
// Snapshot Tests
it('should render an Alert component of type advisory', () => {
const wrapper = mountWithIntl(<Alert type={Alert.Opts.Types.ADVISORY}>This is an advisory alert.</Alert>);
const wrapper = shallowWithIntl(<Alert type={Alert.Opts.Types.ADVISORY}>This is an advisory alert.</Alert>).dive();

const alertDiv = wrapper.find('div.alert-base');
expect(alertDiv.prop('className')).toEqual('alert-base advisory wide');
expect(alertDiv.prop('role')).toEqual('status');
expect(wrapper.find('.title').text()).toEqual('Terra.alert.advisory');
expect(wrapper).toMatchSnapshot();
});
});

describe('Alert of type unsatisfied', () => {
it('should render an unsatisfied Alert', () => {
const wrapper = mountWithIntl(<Alert type={Alert.Opts.Types.UNSATISFIED}>This is an unsatisfied alert.</Alert>);
const wrapper = shallowWithIntl(<Alert type={Alert.Opts.Types.UNSATISFIED}>This is an unsatisfied alert.</Alert>).dive();

const alertDiv = wrapper.find('div.alert-base');
expect(alertDiv.prop('className')).toEqual('alert-base unsatisfied wide');
expect(alertDiv.prop('role')).toEqual('status');
expect(wrapper.find(IconGapChecking).length).toEqual(1);
expect(wrapper.find('.title').text()).toEqual('Terra.alert.unsatisfied');
expect(wrapper).toMatchSnapshot();
});
});

describe('Alert of type unverified', () => {
it('should render an unverified Alert', () => {
const wrapper = mountWithIntl(<Alert type={Alert.Opts.Types.UNVERIFIED}>This is an unverified alert.</Alert>);
const wrapper = shallowWithIntl(<Alert type={Alert.Opts.Types.UNVERIFIED}>This is an unverified alert.</Alert>).dive();

const alertDiv = wrapper.find('div.alert-base');
expect(alertDiv.prop('className')).toEqual('alert-base unverified wide');
expect(alertDiv.prop('role')).toEqual('status');
expect(wrapper.find(IconDiamondSymbol).length).toEqual(1);
expect(wrapper.find('.title').text()).toEqual('Terra.alert.unverified');
expect(wrapper).toMatchSnapshot();
});
});

describe('Alert of type info with text content', () => {
// Snapshot Tests
it('should render an Alert component of type info', () => {
const wrapper = mountWithIntl(<Alert type={Alert.Opts.Types.INFO}>This is an information alert.</Alert>);
const wrapper = shallowWithIntl(<Alert type={Alert.Opts.Types.INFO}>This is an information alert.</Alert>).dive();

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').text()).toEqual('Terra.alert.info');
expect(wrapper).toMatchSnapshot();
});
});

describe('Alert of type success with text content', () => {
// Snapshot Tests
it('should render an Alert component of type success', () => {
const wrapper = mountWithIntl(<Alert type={Alert.Opts.Types.SUCCESS}>This is a success alert.</Alert>);
const wrapper = shallowWithIntl(<Alert type={Alert.Opts.Types.SUCCESS}>This is a success alert.</Alert>).dive();

const alertDiv = wrapper.find('div.alert-base');
expect(alertDiv.prop('className')).toEqual('alert-base success wide');
expect(alertDiv.prop('role')).toEqual('status');
expect(wrapper.find(IconSuccess).length).toEqual(1);
expect(wrapper.find('.title').text()).toEqual('Terra.alert.success');
expect(wrapper).toMatchSnapshot();
});
});

describe('Alert of type custom with custom title and text content', () => {
// Snapshot Tests
it('should render an Alert component of type custom', () => {
const wrapper = mountWithIntl(<Alert type={Alert.Opts.Types.CUSTOM} title="Help!" customIcon={<IconHelp />} customColorClass="terra-alert-custom-orange-color">This is a custom alert.</Alert>);
const wrapper = shallowWithIntl(<Alert type={Alert.Opts.Types.CUSTOM} title="Help!" customIcon={<IconHelp />} customColorClass="terra-alert-custom-orange-color">This is a custom alert.</Alert>).dive();

const alertDiv = wrapper.find('div.alert-base');
expect(alertDiv.prop('className')).toEqual('alert-base custom wide terra-alert-custom-orange-color');
expect(alertDiv.prop('role')).toEqual('status');
expect(wrapper.find(IconHelp).length).toEqual(1);
expect(wrapper.find('.title').text()).toEqual('Help!');
expect(wrapper).toMatchSnapshot();
});
});

describe('Alert of type info with custom title and HTML content', () => {
// Snapshot Tests
it('should render an Alert component of type info with custom title and HTML content', () => {
const wrapper = mountWithIntl(<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>);

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 . . .');
expect(wrapper).toMatchSnapshot();
});
});

describe('Alert of type success with an action button text content', () => {
// Snapshot Tests
it('should render an Alert component of type success with an action button', () => {
const wrapper = mountWithIntl(<Alert type={Alert.Opts.Types.SUCCESS} action={<Button text="Action" variant={Button.Opts.Variants.EMPHASIS} onClick={() => { }} />}>This is a success alert.</Alert>);
const mockOnClick = jest.fn();
const wrapper = shallowWithIntl(<Alert type={Alert.Opts.Types.SUCCESS} action={<Button text="Action" variant={Button.Opts.Variants.EMPHASIS} onClick={mockOnClick} />}>This is a success alert.</Alert>).dive();

const alertDiv = wrapper.find('div.alert-base');
expect(alertDiv.prop('className')).toEqual('alert-base success wide');
expect(alertDiv.prop('role')).toEqual('status');
expect(wrapper.find(IconSuccess).length).toEqual(1);
expect(wrapper.find('.title').text()).toEqual('Terra.alert.success');
expect(wrapper.find(Button).length).toEqual(1);
expect(wrapper.find(Button).prop('text')).toEqual('Action');
expect(wrapper.find(Button).prop('onClick')).toEqual(mockOnClick);
expect(wrapper.find(Button).prop('variant')).toEqual('emphasis');
expect(wrapper).toMatchSnapshot();
});
});

describe('Dismissable Alert of type custom with action button, custom title and text content', () => {
// Snapshot Tests
it('should render an Alert component of type custom with an action button', () => {
const wrapper = mountWithIntl(<Alert type={Alert.Opts.Types.CUSTOM} onDismiss={() => { }} title="Help!" customIcon={<IconHelp />} customColorClass="terra-alert-custom-orange-color" action={<Button text="Action" variant={Button.Opts.Variants.EMPHASIS} onClick={() => { }} />}>This is a custom alert.</Alert>);
const mockOnClick = jest.fn();
const mockOnDismiss = jest.fn();
const wrapper = shallowWithIntl(<Alert type={Alert.Opts.Types.CUSTOM} onDismiss={mockOnDismiss} title="Help!" customIcon={<IconHelp />} customColorClass="terra-alert-custom-orange-color" action={<Button text="Action" variant={Button.Opts.Variants.EMPHASIS} onClick={mockOnClick} />}>This is a custom alert.</Alert>).dive();

const alertDiv = wrapper.find('div.alert-base');
expect(alertDiv.prop('className')).toEqual('alert-base custom wide terra-alert-custom-orange-color');
expect(alertDiv.prop('role')).toEqual('status');
expect(wrapper.find(IconHelp).length).toEqual(1);
expect(wrapper.find('.title').text()).toEqual('Help!');
const buttons = wrapper.find(Button);
expect(buttons.length).toEqual(2);
// action button
expect(buttons.at(0).prop('text')).toEqual('Action');
expect(buttons.at(0).prop('onClick')).toEqual(mockOnClick);
expect(buttons.at(0).prop('variant')).toEqual('emphasis');
// dismiss button
expect(buttons.at(1).prop('text')).toEqual('Terra.alert.dismiss');
expect(buttons.at(1).prop('onClick')).toEqual(mockOnDismiss);
expect(buttons.at(1).prop('variant')).toEqual('neutral');
expect(wrapper).toMatchSnapshot();
});
});
Expand Down
Loading

0 comments on commit ef36e66

Please sign in to comment.