diff --git a/packages/terra-action-header/CHANGELOG.md b/packages/terra-action-header/CHANGELOG.md index d532b20712b..9881c5609c6 100644 --- a/packages/terra-action-header/CHANGELOG.md +++ b/packages/terra-action-header/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +* Added + * Added `text` prop as a replacement of `title` prop to avoid confusion with HTML attribute `title`. + * Added Ally labels for action header navigation buttons. + * Added translations for back, next and previous button. + * Changed * Updated jest snapshots for button changes. diff --git a/packages/terra-action-header/package.json b/packages/terra-action-header/package.json index d36ced7a706..2bfa421419e 100644 --- a/packages/terra-action-header/package.json +++ b/packages/terra-action-header/package.json @@ -29,10 +29,12 @@ "dependencies": { "@cerner/terra-docs": "^1.9.0", "classnames": "^2.2.5", + "lodash.uniqueid": "^4.0.1", "prop-types": "^15.5.8", "terra-button": "^3.64.0", "terra-mixins": "^1.40.0", - "terra-theme-context": "^1.0.0" + "terra-theme-context": "^1.0.0", + "terra-visually-hidden-text": "2.36.0" }, "scripts": { "compile": "babel --root-mode upward src --out-dir lib --copy-files", diff --git a/packages/terra-action-header/src/ActionHeader.jsx b/packages/terra-action-header/src/ActionHeader.jsx index e8840973d26..5d4e83ddc30 100644 --- a/packages/terra-action-header/src/ActionHeader.jsx +++ b/packages/terra-action-header/src/ActionHeader.jsx @@ -4,6 +4,7 @@ import classNames from 'classnames/bind'; import Button, { ButtonVariants } 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'; @@ -20,8 +21,10 @@ const propTypes = { */ intl: PropTypes.shape({ formatMessage: PropTypes.func }), /** + * ![IMPORTANT](https://badgen.net/badge/UX/Accessibility/blue) * Optionally sets the heading level. One of `1`, `2`, `3`, `4`, `5`, `6`. Default `level=1`. This helps screen readers to announce appropriate heading levels. * Changing 'level' will not visually change the style of the content. + * `level` should be specified explicitly to allow screen readers to identify headers consistently. */ level: PropTypes.oneOf([1, 2, 3, 4, 5, 6]), /** @@ -33,6 +36,10 @@ const propTypes = { * Callback function for when the back button is clicked. The back button will not display if this is not set. */ onBack: PropTypes.func, + /** + * Accessibility label for Back button. To be used with onBack prop. + */ + backButtonA11yLabel: PropTypes.string, /** * Callback function for when the expand button is clicked. * The expand button will not display if this is not set or on small viewports. @@ -55,19 +62,32 @@ const propTypes = { * Callback function for when the next button is clicked. The previous-next button group will display if either this or onPrevious is set but the button for the one not set will be disabled. */ onNext: PropTypes.func, + /** + * Accessibility label for Next button. To be used with onNext prop + */ + nextButtonA11yLabel: PropTypes.string, /** * 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, + /** + * ![IMPORTANT](https://badgen.net/badge/UX/Accessibility/blue) * Text to be displayed as the title in the header bar. */ + text: PropTypes.string, + /** + * ![IMPORTANT](https://badgen.net/badge/prop/deprecated/red) + * title prop has been deperecated and will be removed on next major version relase. Replace the `title` prop with `text` prop. + */ title: PropTypes.string, }; const defaultProps = { - title: undefined, - level: 1, + text: undefined, onClose: undefined, onBack: undefined, onMaximize: undefined, @@ -75,9 +95,13 @@ const defaultProps = { onNext: undefined, onPrevious: undefined, children: undefined, + backButtonA11yLabel: undefined, + prevButtonA11yLabel: undefined, + nextButtonA11yLabel: undefined, }; const ActionHeader = ({ + text, title, intl, level, @@ -88,9 +112,16 @@ const ActionHeader = ({ onPrevious, onNext, children, + backButtonA11yLabel, + prevButtonA11yLabel, + nextButtonA11yLabel, ...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 ? ( @@ -102,6 +133,7 @@ const ActionHeader = ({ text={intl.formatMessage({ id: 'Terra.actionHeader.close' })} onClick={onClose} variant={ButtonVariants.UTILITY} + aria-describedby={closeButtonId} /> ) : null; @@ -112,7 +144,7 @@ const ActionHeader = ({ data-terra-action-header="back-button" isIconOnly icon={} - text={intl.formatMessage({ id: 'Terra.actionHeader.back' })} + text={backButtonA11yLabel || intl.formatMessage({ id: 'Terra.actionHeader.back' })} onClick={onBack} variant={ButtonVariants.UTILITY} /> @@ -131,6 +163,7 @@ const ActionHeader = ({ text={intl.formatMessage({ id: 'Terra.actionHeader.maximize' })} onClick={onMaximize} variant={ButtonVariants.UTILITY} + aria-describedby={maximizeButtonId} /> ); } else if (onMinimize) { @@ -143,6 +176,7 @@ const ActionHeader = ({ text={intl.formatMessage({ id: 'Terra.actionHeader.minimize' })} onClick={onMinimize} variant={ButtonVariants.UTILITY} + aria-describedby={minimizeButtonId} /> ); } @@ -156,7 +190,7 @@ const ActionHeader = ({ data-terra-action-header="previous-button" isIconOnly icon={} - text={intl.formatMessage({ id: 'Terra.actionHeader.previous' })} + text={prevButtonA11yLabel || intl.formatMessage({ id: 'Terra.actionHeader.previous' })} onClick={onPrevious} isDisabled={onPrevious === undefined} variant={ButtonVariants.UTILITY} @@ -166,7 +200,7 @@ const ActionHeader = ({ data-terra-action-header="next-button" isIconOnly icon={} - text={intl.formatMessage({ id: 'Terra.actionHeader.next' })} + text={nextButtonA11yLabel || intl.formatMessage({ id: 'Terra.actionHeader.next' })} onClick={onNext} isDisabled={onNext === undefined} variant={ButtonVariants.UTILITY} @@ -186,14 +220,22 @@ const ActionHeader = ({ : null; const rightButtons = closeButton ?
{closeButton}
: null; - + if (title) { + // eslint-disable-next-line no-console + console.warn('`title` prop has been renamed to `text`. please update all the refernces of `title` prop to use prop `text`.'); // to be removed on next major version release. + } + if (!level) { + // eslint-disable-next-line no-console + console.warn('Default heading level may not appropriate has it would fail to convey context of heading in a site / application where it is used. Heading level should be set explicitly depending on the position of header in site / application to allow screen readers to identify headers consistently.'); // to be removed on next major version release. + } return ( {children} diff --git a/packages/terra-action-header/src/ActionHeaderContainer.module.scss b/packages/terra-action-header/src/ActionHeaderContainer.module.scss index 700b6e15b1a..bc5e3dbe4d0 100644 --- a/packages/terra-action-header/src/ActionHeaderContainer.module.scss +++ b/packages/terra-action-header/src/ActionHeaderContainer.module.scss @@ -70,4 +70,8 @@ } } /* stylelint-enable selector-max-compound-selectors */ + + .hidden-label { + height: 0; + } } diff --git a/packages/terra-action-header/src/_ActionHeaderContainer.jsx b/packages/terra-action-header/src/_ActionHeaderContainer.jsx index 578a005a3a7..6761e24bcc8 100644 --- a/packages/terra-action-header/src/_ActionHeaderContainer.jsx +++ b/packages/terra-action-header/src/_ActionHeaderContainer.jsx @@ -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); @@ -22,7 +24,7 @@ const propTypes = { /** * Text to be displayed as the title in the header bar. */ - title: PropTypes.string, + text: PropTypes.string, /** * Content to be displayed at the end of the header. @@ -34,16 +36,21 @@ const propTypes = { * Changing 'level' will not visually change the style of the content. */ level: PropTypes.oneOf([1, 2, 3, 4, 5, 6]).isRequired, + /** + * @private + * The intl object to be injected for translations. + */ + intl: PropTypes.shape({ formatMessage: PropTypes.func }), }; const defaultProps = { - title: undefined, + text: undefined, startContent: undefined, endContent: undefined, }; const ActionHeaderContainer = ({ - children, title, startContent, endContent, level, ...customProps + children, text, startContent, endContent, level, intl, ...customProps }) => { const theme = React.useContext(ThemeContext); const HeaderElement = `h${level}`; @@ -52,10 +59,22 @@ const ActionHeaderContainer = ({ React.cloneElement(child, { className: cx(['flex-collapse', children.props.className]) }) )); - const titleElement = title ? ( + 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 = ( + <> + + + + + ); + + const titleElement = text ? (
- {title} + {text}
) : undefined; @@ -68,6 +87,7 @@ const ActionHeaderContainer = ({ {content} {endContent &&
{endContent}
} +
{visuallyHiddenComponent}
); }; @@ -75,4 +95,4 @@ const ActionHeaderContainer = ({ ActionHeaderContainer.propTypes = propTypes; ActionHeaderContainer.defaultProps = defaultProps; -export default ActionHeaderContainer; +export default injectIntl(ActionHeaderContainer); diff --git a/packages/terra-action-header/tests/jest/__snapshots__/ActionHeader.test.jsx.snap b/packages/terra-action-header/tests/jest/__snapshots__/ActionHeader.test.jsx.snap index 522ba7bd47d..82c6fbaba19 100644 --- a/packages/terra-action-header/tests/jest/__snapshots__/ActionHeader.test.jsx.snap +++ b/packages/terra-action-header/tests/jest/__snapshots__/ActionHeader.test.jsx.snap @@ -1,21 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ActionHeader correctly applies the theme context className 1`] = ` -
-
+
-
-
-
-

- Action Header -

-
-
-
+ } + id="14" + intl={ + Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object {}, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": null, + "now": [Function], + "onError": [Function], + "textComponent": "span", + "timeZone": null, + } + } + level={1} + startContent={
-
- + } + text="Action Header" +/> `; exports[`ActionHeader should render a default action header 1`] = ` - `; exports[`ActionHeader should render an action header with back and close buttons and title 1`] = ` -