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

[terra-action-header] Add support to allow users to set A11y label to action buttons. #3645

Merged
merged 10 commits into from
Aug 2, 2022
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
2 changes: 2 additions & 0 deletions packages/terra-action-header/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Changelog

## Unreleased

* Changed
* Replaced `terra-button` with `IconButton`
* Added props to provide accessibility label to action buttons.

* Breaking Changes
* Renamed `title` prop as `text` to avoid confusion with HTML attribute `title`.
Expand Down
4 changes: 3 additions & 1 deletion packages/terra-action-header/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@
"dependencies": {
"@cerner/terra-docs": "^1.7.1",
"classnames": "^2.2.5",
"lodash.uniqueid": "^4.0.1",
"prop-types": "^15.5.8",
"terra-button": "^3.61.0",
"terra-mixins": "^1.40.0",
"terra-theme-context": "^1.0.0"
"terra-theme-context": "^1.0.0",
"terra-visually-hidden-text": "^2.35.0"
},
"scripts": {
"compile": "babel --root-mode upward src --out-dir lib --copy-files",
Expand Down
35 changes: 32 additions & 3 deletions packages/terra-action-header/src/ActionHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import classNames from 'classnames/bind';
import { IconButton } from 'terra-button';
import { injectIntl } from 'react-intl';
import ThemeContext from 'terra-theme-context';
import uniqueid from 'lodash.uniqueid';
import ActionHeaderContainer from './_ActionHeaderContainer';
import styles from './ActionHeader.module.scss';

const cx = classNames.bind(styles);

const propTypes = {
/**
* Accessibility label for Back button. To be used with onBack prop.
*/
backButtonA11yLabel: PropTypes.string,
/**
* Displays a single terra `Collapsible Menu View` (_Not provided by `Action Header`_) child element on the right end of the header.
*/
Expand All @@ -24,6 +29,10 @@ const propTypes = {
* Changing 'level' will not visually change the style of the content.
*/
level: PropTypes.oneOf([1, 2, 3, 4, 5, 6]),
/**
* Accessibility label for Next button. To be used with onNext prop
*/
nextButtonA11yLabel: PropTypes.string,
/**
* Callback function for when the close button is clicked.
* On small viewports, this will be triggered by a back button if onBack is not set.
Expand Down Expand Up @@ -59,6 +68,10 @@ const propTypes = {
* Callback function for when the previous button is clicked. The previous-next button group will display if either this or onNext is set but the button for the one not set will be disabled.
*/
onPrevious: PropTypes.func,
/**
* Accessibility label for Previous button. To be used with onPrevious prop.
*/
prevButtonA11yLabel: PropTypes.string,
/**
* Text to be displayed as the title in the header bar.
*/
Expand All @@ -75,6 +88,10 @@ const defaultProps = {
onNext: undefined,
onPrevious: undefined,
children: undefined,
backButtonA11yLabel: undefined,
nextButtonA11yLabel: undefined,
prevButtonA11yLabel: undefined,

};

const ActionHeader = ({
Expand All @@ -88,10 +105,18 @@ const ActionHeader = ({
onPrevious,
onNext,
children,
backButtonA11yLabel,
nextButtonA11yLabel,
prevButtonA11yLabel,
...customProps
}) => {
const theme = React.useContext(ThemeContext);

const buttonId = uniqueid();
const closeButtonId = `terra-action-header-close-button-${buttonId}`;
const maximizeButtonId = `terra-action-header-maximize-button-${buttonId}`;
const minimizeButtonId = `terra-action-header-minimize-button-${buttonId}`;

const closeButton = onClose
? (
<IconButton
Expand All @@ -103,6 +128,7 @@ const ActionHeader = ({
text={intl.formatMessage({ id: 'Terra.actionHeader.close' })}
onClick={onClose}
variant={IconButton.Opts.Variants.UTILITY}
aria-describedby={closeButtonId}
/>
)
: null;
Expand All @@ -114,7 +140,7 @@ const ActionHeader = ({
isIconOnly
icon={<span className={cx(['header-icon', 'back'])} />}
iconType={IconButton.Opts.IconTypes.INFORMATIVE}
text={intl.formatMessage({ id: 'Terra.actionHeader.back' })}
text={backButtonA11yLabel || intl.formatMessage({ id: 'Terra.actionHeader.back' })}
onClick={onBack}
variant={IconButton.Opts.Variants.UTILITY}
/>
Expand All @@ -134,6 +160,7 @@ const ActionHeader = ({
text={intl.formatMessage({ id: 'Terra.actionHeader.maximize' })}
onClick={onMaximize}
variant={IconButton.Opts.Variants.UTILITY}
aria-describedby={maximizeButtonId}
/>
);
} else if (onMinimize) {
Expand All @@ -147,6 +174,7 @@ const ActionHeader = ({
text={intl.formatMessage({ id: 'Terra.actionHeader.minimize' })}
onClick={onMinimize}
variant={IconButton.Opts.Variants.UTILITY}
aria-describedby={minimizeButtonId}
/>
);
}
Expand All @@ -161,7 +189,7 @@ const ActionHeader = ({
isIconOnly
icon={<span className={cx(['header-icon', 'previous'])} />}
iconType={IconButton.Opts.IconTypes.INFORMATIVE}
text={intl.formatMessage({ id: 'Terra.actionHeader.previous' })}
text={prevButtonA11yLabel || intl.formatMessage({ id: 'Terra.actionHeader.previous' })}
onClick={onPrevious}
isDisabled={onPrevious === undefined}
variant={IconButton.Opts.Variants.UTILITY}
Expand All @@ -172,7 +200,7 @@ const ActionHeader = ({
isIconOnly
icon={<span className={cx(['header-icon', 'next'])} />}
iconType={IconButton.Opts.IconTypes.INFORMATIVE}
text={intl.formatMessage({ id: 'Terra.actionHeader.next' })}
text={nextButtonA11yLabel || intl.formatMessage({ id: 'Terra.actionHeader.next' })}
onClick={onNext}
isDisabled={onNext === undefined}
variant={IconButton.Opts.Variants.UTILITY}
Expand Down Expand Up @@ -200,6 +228,7 @@ const ActionHeader = ({
text={text}
endContent={rightButtons}
level={level}
id={buttonId}
>
{children}
</ActionHeaderContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,8 @@
}
}
/* stylelint-enable selector-max-compound-selectors */

.hidden-label {
height: 0;
}
}
25 changes: 23 additions & 2 deletions packages/terra-action-header/src/_ActionHeaderContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import classNamesBind from 'classnames/bind';
import ThemeContext from 'terra-theme-context';
import VisuallyHiddenText from 'terra-visually-hidden-text';
import { injectIntl } from 'react-intl';
import styles from './ActionHeaderContainer.module.scss';

const cx = classNamesBind.bind(styles);
Expand All @@ -14,6 +16,12 @@ const propTypes = {
*/
children: PropTypes.element,

/**
* @private
* The intl object to be injected for translations.
*/
intl: PropTypes.shape({ formatMessage: PropTypes.func }),

/**
* Content to be displayed at the start of the header, placed before the title.
*/
Expand Down Expand Up @@ -43,14 +51,26 @@ const defaultProps = {
};

const ActionHeaderContainer = ({
children, text, startContent, endContent, level, ...customProps
children, text, startContent, endContent, level, intl, ...customProps
}) => {
const theme = React.useContext(ThemeContext);

const content = React.Children.map(children, child => (
React.cloneElement(child, { className: cx(['flex-collapse', children.props.className]) })
));

const closeButtonId = `terra-action-header-close-button-${customProps.id}`;
const maximizeButtonId = `terra-action-header-maximize-button-${customProps.id}`;
const minimizeButtonId = `terra-action-header-minimize-button-${customProps.id}`;

const visuallyHiddenComponent = (
<>
<VisuallyHiddenText aria-hidden id={closeButtonId} text={intl.formatMessage({ id: 'Terra.actionHeader.close.description' }, { text })} />
<VisuallyHiddenText aria-hidden id={maximizeButtonId} text={intl.formatMessage({ id: 'Terra.actionHeader.maximize.description' }, { text })} />
<VisuallyHiddenText aria-hidden id={minimizeButtonId} text={intl.formatMessage({ id: 'Terra.actionHeader.minimize.description' }, { text })} />
</>
);

let titleElement;
if (text && level) {
const HeaderElement = `h${level}`;
Expand All @@ -71,11 +91,12 @@ const ActionHeaderContainer = ({
</div>
{content}
{endContent && <div className={cx('flex-end')}>{endContent}</div>}
<div className={cx('hidden-label')}>{visuallyHiddenComponent}</div>
</div>
);
};

ActionHeaderContainer.propTypes = propTypes;
ActionHeaderContainer.defaultProps = defaultProps;

export default ActionHeaderContainer;
export default injectIntl(ActionHeaderContainer);
12 changes: 6 additions & 6 deletions packages/terra-action-header/tests/jest/ActionHeader.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('ActionHeader', () => {
});

it('should render an action header with back and close buttons and title', () => {
const actionHeader = <ActionHeader level={1} title="Action Header" onBack={() => {}} onClose={() => {}} />;
const actionHeader = <ActionHeader level={1} title="Action Header" onBack={() => {}} onClose={() => {}} backButtonA11yLabel="Go Back" closeButtonA11yLabel="Close Modal" />;
const wrapper = shallowWithIntl(actionHeader).dive();
expect(wrapper).toMatchSnapshot();
});
Expand Down Expand Up @@ -63,31 +63,31 @@ describe('ActionHeader', () => {
});

it('should render an action header with maximize button and title', () => {
const actionHeader = <ActionHeader level={1} title="Action Header" onMaximize={() => {}} />;
const actionHeader = <ActionHeader level={1} title="Action Header" onMaximize={() => {}} maximizeButtonA11yLabel="maximize modal" />;
const wrapper = shallowWithIntl(actionHeader).dive();
expect(wrapper).toMatchSnapshot();
});

it('should render an action header with minimize button and title', () => {
const actionHeader = <ActionHeader level={1} title="Action Header" onMinimize={() => {}} />;
const actionHeader = <ActionHeader level={1} title="Action Header" onMinimize={() => {}} minimizeButtonA11yLabel="minimize modal" />;
const wrapper = shallowWithIntl(actionHeader).dive();
expect(wrapper).toMatchSnapshot();
});

it('should render an action header with next and previous buttons and title', () => {
const actionHeader = <ActionHeader level={1} title="Action Header" onNext={() => {}} onPrevious={() => {}} />;
const actionHeader = <ActionHeader level={1} title="Action Header" onNext={() => {}} onPrevious={() => {}} nextButtonA11yLabel="Go to Next Page" prevButtonA11yLabel="Go to Previous Page" />;
const wrapper = shallowWithIntl(actionHeader).dive();
expect(wrapper).toMatchSnapshot();
});

it('should render an action header with title, enabled next button, and disabled previous button', () => {
const actionHeader = <ActionHeader level={1} title="Action Header" onNext={() => {}} />;
const actionHeader = <ActionHeader level={1} title="Action Header" onNext={() => {}} nextButtonA11yLabel="Go to Next Page" />;
const wrapper = shallowWithIntl(actionHeader).dive();
expect(wrapper).toMatchSnapshot();
});

it('should render an action header with title, enabled previous button, and disabled next button', () => {
const actionHeader = <ActionHeader level={1} title="Action Header" onPrevious={() => {}} />;
const actionHeader = <ActionHeader level={1} title="Action Header" onPrevious={() => {}} prevButtonA11yLabel="Go to Previous Page" />;
const wrapper = shallowWithIntl(actionHeader).dive();
expect(wrapper).toMatchSnapshot();
});
Expand Down
Loading