Skip to content

Commit

Permalink
CJL-46 add aria and keyboard support to accordion components
Browse files Browse the repository at this point in the history
CJL-46 add aria and keyboard support to MultiSelect

- refactor to use Checkbox
- add classnames dependency
- add generateUID util

CJL-46 use unique id and classnames for accordion items

CJL-46 refactor pagination using classnames

CJL-46 refactor pagination for aria and keyboard accessibility

CJL-46 check if first/last before changing page

fixes keyboard page changes

CJL-46 add aria and keyboard accessibility to avatar menu

CJL-46 update Tag component

- add size, optionLabel and onOptionClick props
- use onOptionClick to distinguish click on button vs the tag
- use optionLabel for aria label
- use size prop and append to classname (was limited to just ‘sm’ before)
- remove small prop
- use classnames
- update Tag readme with props and example

CJL-46 use Tag component in SearchMultiSelect, refactor using classnames

CJL-46 add aria label to pagination buttons since text is hidden

CJL-46 add prop classname to accordions

CJL-46 refactor Accordion using classnames

CJL-46 fix nested accordion story

CJL-46 update eslint to warn for unused variables

CJL-46 correct default tag size in story

CJL-46 use class to fix vertical align instead on inline style in Pagination

CJL-46 add classname to root class in MultiSelect

CJL-46 add option label to Tag story

CJL-46 add constants for charCodes

CJL-46 add notes about keyboard accessibility in readme
  • Loading branch information
jscottsmith authored and Eder Sanchez committed Mar 10, 2018
1 parent 7a69ca3 commit 662554c
Show file tree
Hide file tree
Showing 17 changed files with 547 additions and 295 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"no-multi-str": 1,
"key-spacing": [1, { "afterColon": true }],
"no-var": 1,
"no-unused-vars": 0,
"no-unused-vars": [1, { "vars": "local", "args": "after-used" }],
"no-alert": 0,
"no-lone-blocks": 0,
"react/display-name": [1, { "ignoreTranspilerName": false }],
Expand Down
26 changes: 26 additions & 0 deletions _stories/atoms/Tag/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,27 @@
The `<Tag>` component is used to indicate active or selected items, filters or options. Refer to [this](http://design-prototypes.gumgum.com/black-tie/documentation/#icons-btl) page for icon names.

**Example**:

```jsx
<Tag
context="primary"
hasOption
onOptionClick={() => {}}
optionIcon="bt-times"
size="sm"
text="Sample Tag"
/>
```

| **prop name** | **description** |
| ------------- | ----------------------------------------------------------------------------------------------------------------------- |
| className | Class to add to the root element {string} {required} |
| context | Any of \`normal, primary, success, warning, danger\` or leave unset to get default appearance {string} {defaults to ''} |
| hasOption | Indicate if the tag has an option button {boolean} {defaults to false} |
| onClick | Click handler for the root element {function} |
| onOptionClick | Click handler for the option button element {function} |
| optionIcon | Icon name for the option button {string} {defaults to 'bt-times'} |
| optionLabel | Aria label for the option button {string} {defaults to 'Remove tag'} |
| size | Any of \`sm, xs\` {string} |
| style | Styles to add to the root element {object} |
| text | Tag display text {string} |
18 changes: 13 additions & 5 deletions _stories/atoms/Tag/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,24 @@ const contextOptions = {
danger: 'danger'
};

const sizeOptions = {
sm: 'sm',
xs: 'xs',
'': 'default'
};

const component = () => (
<Tag
className={text('Classes', '')}
context={select('Context', contextOptions, 'normal')}
text={text('Text', 'Sample Text')}
hasOption={boolean('Has Option', false)}
optionIcon={text('Option Icon', 'bt-times')}
small={boolean('Small', false)}
hasOption={boolean('Has Option', true)}
onClick={action('tag_click')}
className={text('Classes', '')}
onOptionClick={action('option_click')}
optionIcon={text('Option Icon', 'bt-times')}
optionLabel={text('Option Label', 'Delete Tag')}
size={select('Size', sizeOptions, 'normal')}
style={object('Style', {})}
text={text('Text', 'Sample Text')}
/>
);

Expand Down
41 changes: 22 additions & 19 deletions _stories/molecules/Accordion/README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
The `<Accordion>` component is a collapsible container for holding related elements. Nest `<AccordionItem>` components inside `<Accordion>` for each drawer. Nest `<AccordionItemContent>` in each `<AccordionItem>` if you would like to display listed information within an open `<AccordionItem>`. Each `<Accordion>` related component accepts a className and otherProps so you are able to further customize the component.

*Accordion example*:
_Accordion example_:

You can use `<Accordion>` to display any information that you pass in, such as:

```
<Accordion>
<AccordionItem label="Controls">
<FormGroup>
<FormGroupLabel text="Name" />
<TextInput name="name" />
</FormGroup>
<FormGroup>
<FormGroupLabel text="Name" />
<TextInput name="name" />
</FormGroup>
</AccordionItem>
</Accordion>
```


You can also use `<Accordion>` to display information in a list form, in the case you want to list items in rows. Example:

```
<Accordion>
<AccordionItem label="Settings">
<AccordionItemContent>
<Column md="6">
Setting 1
</Column>
<Column md=6>
<Button>Save</Button>
</Column>
<Column md="6">
Setting 1
</Column>
<Column md=6>
<Button>Save</Button>
</Column>
</AccordionItemContent>
<AccordionItemContent>
<Column md="6">
Setting 2
</Column>
<Column md=6>
<Button>Save</Button>
</Column>
<Column md="6">
Setting 2
</Column>
<Column md=6>
<Button>Save</Button>
</Column>
</AccordionItemContent>
</AccordionItem>
</Accordion>
```

**Keyboard Accessibility:**

When an accordion is in focus you can toggle it expanded/collapsed with the spacebar or enter keys.
16 changes: 13 additions & 3 deletions _stories/molecules/Accordion/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { boolean, select, text, object } from '@storybook/addon-knobs';
import { select, text } from '@storybook/addon-knobs';
import readme from './README.md';

import Accordion from '../../../components/molecules/Accordion';
Expand All @@ -23,10 +23,20 @@ const component = () => (
context={select('Context', contextOptions, '')}
className={text('Class', '')}>
<AccordionItem label={text('Item Label 1', 'Item 1')}>
<AccordionItemContent>Content</AccordionItemContent>
<AccordionItemContent>Content 1</AccordionItemContent>
</AccordionItem>
<AccordionItem label={text('Item Label 2', 'Item 2')}>
<AccordionItemContent>Content</AccordionItemContent>
<AccordionItemContent>Content 2</AccordionItemContent>
</AccordionItem>
<AccordionItem label={text('Item Label 3', 'Item 3')}>
<AccordionItemContent>Content 3</AccordionItemContent>
<Accordion
size={select('Size', sizeOptions, '')}
context={select('Context', contextOptions, '')}>
<AccordionItem label={text('Nested Item Label 1', 'Nested Item 1')}>
<AccordionItemContent>Nested Content</AccordionItemContent>
</AccordionItem>
</Accordion>
</AccordionItem>
</Accordion>
);
Expand Down
6 changes: 6 additions & 0 deletions _stories/molecules/Avatar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The avatar can include a dropdown menu, which is meant to be shown when the avat
For the menu contents, you must pass in `menuOptions` (an array of objects of names and paths for the menu), and a `optionCallback` that will be called when the option is clicked. If you want to have a login option, you can include that in the `optionCallback`.

**Example**:

```
state {
avatarOpen: false
Expand Down Expand Up @@ -34,6 +35,7 @@ return(
```

**Options Format**:

```
const AvatarMenu = [
{ name: 'Change Password', path: '/account/change-password' },
Expand All @@ -43,3 +45,7 @@ const AvatarMenu = [
{ name: 'Logout', path: 'logout' }
];
```

**Keyboard Accessibility:**

When the avatar is in focus you can toggle the related menu opened/closed with the spacebar or enter keys.
21 changes: 13 additions & 8 deletions _stories/molecules/Pagination/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ The `<Pagination>` component creates a set of indicators to show how many pages
When a page change is detected, the `onChange` prop will be called with the next page number as its only argument.

**Example**:

```
state = {
activePage: 7,
Expand Down Expand Up @@ -33,11 +34,15 @@ render() {

**Props**:

**prop name** | **description**
----------------|------------
activePage | Current active page {number} {defaults to 1}
lastPage | Total number of pages {number} {required}
boundaries | Whether or not to always show the start and end pages {boolean} {defaults to false}
justify | Whether or not to take the whole available width of the container {boolean} {defaults to false}
size | Any of \`xl, lg, sm, xs\` or leave unset to get default size {string} {defaults to ''}
onChange | Callback to run when changing a page. Receives an object with next page and offsets {function} {required}
| **prop name** | **description** |
| ------------- | --------------------------------------------------------------------------------------------------------- |
| activePage | Current active page {number} {defaults to 1} |
| lastPage | Total number of pages {number} {required} |
| boundaries | Whether or not to always show the start and end pages {boolean} {defaults to false} |
| justify | Whether or not to take the whole available width of the container {boolean} {defaults to false} |
| size | Any of \`xl, lg, sm, xs\` or leave unset to get default size {string} {defaults to ''} |
| onChange | Callback to run when changing a page. Receives an object with next page and offsets {function} {required} |

**Keyboard Accessibility:**

When pagination is in focus, you can use the arrow key to navigate to the next (arrow right and arrow down) and previous (arrow up and arrow left) pages. You can also focus on the next/previous button and "press" them with the spacebar or enter keys.
86 changes: 63 additions & 23 deletions components/atoms/AccordionItem.jsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,54 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import trimString from '../utils/trimString';
import cx from 'classnames';
import generateUID from '../utils/generateUID';
import charCodes from '../../constants/charCodes';

class AccordionItem extends Component {
constructor() {
super();
this.uid = generateUID(this);
}

state = {
open: false
isOpen: false
};

toggleOpen = () => {
this.setState(({ open }) => ({ open: !open }));
toggleOpen = event => {
const { type, charCode } = event;
event.stopPropagation(); // don't want nested accordions to propagate events

if (
type === 'keypress' &&
(charCode === charCodes.SPACEBAR || charCode === charCodes.ENTER)
) {
event.preventDefault(); // prevents page scroll from space key
this.setState(({ isOpen }) => ({ isOpen: !isOpen }));
} else if (type === 'click') {
this.setState(({ isOpen }) => ({ isOpen: !isOpen }));
}
};

render() {
const { size, context, className, children } = this.props;
const baseClass = 'gds-accordion__item';
const contextClass = context ? `${baseClass}--${context}` : '';
const activeClass = this.state.open ? `${baseClass}--active` : '';
const classNames = trimString(`${baseClass} ${contextClass} ${activeClass} ${className}`);
const { size, context, className, children, label } = this.props;
const { isOpen } = this.state;

const titleBaseClass = 'gds-accordion__item-title';
const titleSizeClass = size ? `${titleBaseClass}--${size}` : '';
const titleClass = trimString(`${titleBaseClass} ${titleSizeClass}`);
const rootClass = cx('gds-accordion__item', className, {
[`gds-accordion__item--${context}`]: context,
'gds-accordion__item--active': isOpen
});

const iconBaseClass = 'gds-accordion__item-icon';
const iconSizeClass = size ? `${iconBaseClass}--${size}` : '';
const iconClass = trimString(`${iconBaseClass} ${iconSizeClass}`);
const titleClass = cx('gds-accordion__item-title', {
[`gds-accordion__item-title--${size}`]: size
});

const childBaseClass = 'gds-accordion__child-items';
const childSizeClass = size ? `${childBaseClass}--${size}` : '';
const childClass = trimString(`${childBaseClass} ${childSizeClass}`);
const iconClass = cx('gds-accordion__item-icon', {
[`gds-accordion__item-icon--${size}`]: size
});

const childClass = cx('gds-accordion__child-items', {
[`gds-accordion__child-items--${size}`]: size
});

const newChildren = React.Children.map(children, child => {
return React.cloneElement(child, {
Expand All @@ -37,17 +57,37 @@ class AccordionItem extends Component {
});
});

const regionId = `AccordionItem-region-${this.uid}`;
const labelId = `AccordionItem-label-${this.uid}`;

return (
<li className={classNames} onClick={this.toggleOpen}>
<h4 className={titleClass}>{this.props.label}</h4>
<i className={iconClass} />
<ul className={childClass}>{newChildren}</ul>
<li
aria-pressed={isOpen}
aria-controls={regionId}
aria-expanded={isOpen}
className={rootClass}
onClick={this.toggleOpen}
onKeyPress={this.toggleOpen}
role="button"
tabIndex={0}>
<h4 id={labelId} className={titleClass}>
{label}
</h4>
<span className={iconClass} />
<ul
aria-labelledby={labelId}
aria-hidden={!isOpen}
className={childClass}
id={regionId}
role="region">
{newChildren}
</ul>
</li>
);
}
}

AccordionItem.displayName = 'Accordion Item';
AccordionItem.displayName = 'AccordionItem';

AccordionItem.defaultProps = {
className: '',
Expand Down
Loading

0 comments on commit 662554c

Please sign in to comment.