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

[Terra-Button-Group] Button group A11y Updates #3731

Merged
merged 11 commits into from
Feb 27, 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-action-header/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Changed
* Updated jest snapshots for button changes.

## 2.77.0 - (February 16, 2023)

* Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ exports[`ActionHeader correctly applies the theme context className 1`] = `
className="header-icon maximize"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -58,7 +57,6 @@ exports[`ActionHeader correctly applies the theme context className 1`] = `
className="header-icon close"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -96,7 +94,6 @@ exports[`ActionHeader should render an action header with back and close buttons
className="header-icon close"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand All @@ -122,7 +119,6 @@ exports[`ActionHeader should render an action header with back and close buttons
className="header-icon back"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -155,7 +151,6 @@ exports[`ActionHeader should render an action header with back button and title
className="header-icon back"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -186,7 +181,6 @@ exports[`ActionHeader should render an action header with close button and title
className="header-icon close"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand All @@ -213,7 +207,6 @@ exports[`ActionHeader should render an action header with custom button and titl
title="Action Header"
>
<Button
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -252,7 +245,6 @@ exports[`ActionHeader should render an action header with maximize button and ti
className="header-icon maximize"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -285,7 +277,6 @@ exports[`ActionHeader should render an action header with minimize button and ti
className="header-icon minimize"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand All @@ -311,7 +302,6 @@ exports[`ActionHeader should render an action header with multiple custom button
>
<span>
<Button
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand All @@ -323,7 +313,6 @@ exports[`ActionHeader should render an action header with multiple custom button
variant="neutral"
/>
<Button
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -357,7 +346,6 @@ exports[`ActionHeader should render an action header with next and previous butt
className="header-icon previous"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand All @@ -376,7 +364,6 @@ exports[`ActionHeader should render an action header with next and previous butt
className="header-icon next"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -422,7 +409,6 @@ exports[`ActionHeader should render an action header with title, enabled next bu
className="header-icon previous"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={true}
Expand All @@ -440,7 +426,6 @@ exports[`ActionHeader should render an action header with title, enabled next bu
className="header-icon next"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -477,7 +462,6 @@ exports[`ActionHeader should render an action header with title, enabled previou
className="header-icon previous"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand All @@ -496,7 +480,6 @@ exports[`ActionHeader should render an action header with title, enabled previou
className="header-icon next"
/>
}
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={true}
Expand Down
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

* Changed
* Updated jest snapshots for button changes.

## 4.65.0 - (February 16, 2023)

* Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,6 @@ exports[`Alert of type success with an action button text content should render
<InjectIntl(Alert)
action={
<Button
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -797,7 +796,6 @@ exports[`Alert of type success with an action button text content should render
<Alert
action={
<Button
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -903,7 +901,6 @@ exports[`Alert of type success with an action button text content should render
className="actions"
>
<Button
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -1574,7 +1571,6 @@ exports[`Dismissable Alert of type custom with action button, custom title and t
<InjectIntl(Alert)
action={
<Button
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -1627,7 +1623,6 @@ exports[`Dismissable Alert of type custom with action button, custom title and t
<Alert
action={
<Button
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -1741,7 +1736,6 @@ exports[`Dismissable Alert of type custom with action button, custom title and t
className="actions actions-custom"
>
<Button
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -1776,7 +1770,6 @@ exports[`Dismissable Alert of type custom with action button, custom title and t
</button>
</Button>
<Button
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down Expand Up @@ -1944,7 +1937,6 @@ exports[`Dismissible Alert that includes actions section should render an alert
className="actions"
>
<Button
iconType="decorative"
isBlock={false}
isCompact={false}
isDisabled={false}
Expand Down
6 changes: 6 additions & 0 deletions packages/terra-button-group/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

* Added
* Added support for decoratove icons and multi-selection.

* Fixed
* Fixed keyboard navigation feature and fixes to convey selection states.

## 3.63.0 - (February 16, 2023)

* Changed
Expand Down
52 changes: 49 additions & 3 deletions packages/terra-button-group/src/ButtonGroup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import classNamesBind from 'classnames/bind';
import ThemeContext from 'terra-theme-context';
import { KEY_RIGHT, KEY_LEFT } from 'keycode-js';
import ButtonGroupButton from './ButtonGroupButton';
import ButtonGroupUtils from './ButtonGroupUtils';
import styles from './ButtonGroup.module.scss';
Expand All @@ -20,6 +21,11 @@ const propTypes = {
*/
isBlock: PropTypes.bool,

/**
* Whether or not it is a multi select button group.
*/
isMultiSelect: PropTypes.bool,

/**
* Callback function when the state changes. Parameters are (event, key).
*/
Expand All @@ -35,12 +41,14 @@ const defaultProps = {
children: [],
isBlock: false,
selectedKeys: [],
isMultiSelect: false,
};

class ButtonGroup extends React.Component {
constructor(props) {
super(props);
this.handleOnChange = this.handleOnChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}

handleOnChange(event, key) {
Expand All @@ -49,6 +57,38 @@ class ButtonGroup extends React.Component {
}
}

handleKeyDown(event, idx) {
const allBtns = this.btnGrpRef.querySelectorAll('[data-terra-button-group-button]');
let key = idx;

if (event.keyCode === KEY_RIGHT && allBtns[key + 1]) {
key += 1;
while (allBtns[key] && allBtns[key].hasAttribute('disabled')) {
key += 1;
}
if (allBtns[key]) allBtns[key].focus();
}

if (event.keyCode === KEY_LEFT && allBtns[key - 1]) {
key -= 1;
while (allBtns[key] && allBtns[key].hasAttribute('disabled')) {
key -= 1;
}
if (allBtns[key]) allBtns[key].focus();
}
}

wrapKeyDown(item, idx) {
const { onKeyDown } = item.props;
return (event) => {
this.handleKeyDown(event, idx);

if (onKeyDown) {
onKeyDown(event);
}
};
}

wrapOnClick(item) {
const { onClick } = item.props;
return (event) => {
Expand All @@ -64,6 +104,7 @@ class ButtonGroup extends React.Component {
const {
children,
isBlock,
isMultiSelect,
onChange,
selectedKeys,
...customProps
Expand All @@ -81,19 +122,24 @@ class ButtonGroup extends React.Component {
);

const allButtons = children ? [] : undefined;
// eslint-disable-next-line no-nested-ternary
const btnRole = onChange ? isMultiSelect ? 'checkbox' : 'radio' : 'button';

React.Children.forEach(children, (child) => {
React.Children.forEach(children, (child, index) => {
const isSelected = selectedKeys.indexOf(child.key) > -1;
const cloneChild = React.cloneElement(child, {
role: btnRole,
onClick: this.wrapOnClick(child),
onKeyDown: this.wrapKeyDown(child, index),
className: cx([{ 'is-selected': isSelected && !child.props.isDisabled }, child.props.className]),
'aria-pressed': child.props.isDisabled ? null : isSelected,
'aria-pressed': btnRole === 'button' && !child.props.isDisabled ? isSelected : undefined,
'aria-checked': btnRole !== 'button' && !child.props.isDisabled ? isSelected : undefined,
});
allButtons.push(cloneChild);
});

return (
<div {...customProps} className={buttonGroupClassNames}>
<div {...customProps} ref={(btnGrpRef) => { this.btnGrpRef = btnGrpRef; }} role={btnRole === 'radio' ? 'radiogroup' : 'group'} className={buttonGroupClassNames}>
{allButtons}
</div>
);
Expand Down
3 changes: 2 additions & 1 deletion packages/terra-button-group/src/ButtonGroupButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class ButtonGroupButton extends React.Component {
handleKeyUp(event) {
// Apply focus styles for keyboard navigation.
// The onFocus event doesn't get triggered in some browsers, hence, the focus state needs to be managed here.
if (event.nativeEvent.keyCode === KeyCode.KEY_TAB) {
if (event.nativeEvent.keyCode === KeyCode.KEY_TAB || event.nativeEvent.keyCode === KeyCode.KEY_LEFT || event.nativeEvent.keyCode === KeyCode.KEY_RIGHT) {
this.setState({ focused: true });
this.shouldShowFocus = true;
}
Expand Down Expand Up @@ -140,6 +140,7 @@ class ButtonGroupButton extends React.Component {
onFocus={this.handleFocus}
variant={Button.Opts.Variants.NEUTRAL}
className={buttonClassName}
data-terra-button-group-button
/>
);
}
Expand Down
22 changes: 22 additions & 0 deletions packages/terra-button-group/tests/jest/ButtonGroup.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,28 @@ it('should select a button', () => {
expect(buttonGroup).toMatchSnapshot();
});

it('should apply correct role for multiSelect button group', () => {
const onChange = jest.fn();
const buttonGroup = shallow((
<ButtonGroup isMultiSelect onChange={onChange}>
{button1}
{button2}
</ButtonGroup>
));
expect(buttonGroup).toMatchSnapshot();
});

it('should apply correct role for single select button group', () => {
const onChange = jest.fn();
const buttonGroup = shallow((
<ButtonGroup onChange={onChange}>
{button1}
{button2}
</ButtonGroup>
));
expect(buttonGroup).toMatchSnapshot();
});

it('correctly applies the theme context className', () => {
const buttonGroup = mount(
<ThemeContextProvider theme={{ className: 'orion-fusion-theme' }}>
Expand Down
Loading