Skip to content
27 changes: 27 additions & 0 deletions docs/a11y-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# A11y Support
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is some documentation for the accessibility support


## Tested devices
* Google Chrome + ChromeVox
* Windows + Firefox + NVDA
* Windows + Firefox + JAWS

## What to expect

### Navigation
Tab and Shift + Tab are supported when navigating between tokens, it can't be
used to navigate menus.
Navigation inside the menus is entirely done with the arrow keys.

### Speech pattern
A Token should be spoken in its entirety adding a menu notification when the
dropdown menu is available (expanded and compressed)

The input itself should notify when the suggestions dropdown is available.

Inside the suggestions dropdown there are a number of lists with options.
The desired behaviour is that when focusing an option it is read out with its
position in the list and the name of the current list.

Inside the dropdown menu the behaviour is similar, it should read the name of
the option with the indication of the checked status of the menu item. The
status is *true* if it's the currently selected facet, it's *false* otherwhise.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"express": "^4.13.3",
"jest-cli": "^0.8.2",
"react": "^0.14.5",
"react-a11y": "^0.3.1",
"react-addons-test-utils": "^0.14.7",
"react-dom": "^0.14.5",
"rimraf": "^2.5.0",
Expand Down
35 changes: 29 additions & 6 deletions src/DropdownMenu.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import React from 'react';

const DropdownMenuSeparator = () => <hr />;
import { UNIQUE_ID } from './FacetedTokenInput';

const DropdownMenuSeparator = () => <hr aria-hidden="true" />;

const DropdownMenuItem = ({
selected,
suggestion,
sectionIndex,
index,
addToken,
setSelected
setSelected,
sectionTitle
}) => (
<li className={ selected ? 'active' : '' }>
<li
role="option"
className={ selected ? 'active' : '' }
id={ UNIQUE_ID + 'section_0' + sectionIndex + '_0' + index }
aria-labelledby={ UNIQUE_ID + sectionTitle + ' ' + UNIQUE_ID + 'section_0' + sectionIndex + '_0' + index + '_1' }
>
<a
id={ UNIQUE_ID + 'section_0' + sectionIndex + '_0' + index + '_1' }
role="button"
href="javascript: void 0"
onClick={ () => addToken(suggestion.result) }
onMouseMove={ () => setSelected({ sectionIndex, index }) }
Expand All @@ -22,9 +32,21 @@ const DropdownMenuItem = ({
);

const DropdownMenuSection = ({ section, sectionIndex, ...props }) => (
<ul>
<ul
role="listbox"
id={ UNIQUE_ID + 'section_0' + sectionIndex }
// different approach to read the section title
// aria-labelledby={ UNIQUE_ID + section.title }
>

{
section.title && <li key="header" className="header">
section.title &&
<li
key="header"
className="header"
aria-label={ section.title }
id={ UNIQUE_ID + section.title }
>
{ section.title }
</li>
}
Expand All @@ -41,13 +63,14 @@ const DropdownMenuSection = ({ section, sectionIndex, ...props }) => (
suggestion={ suggestion }
sectionIndex={ sectionIndex }
index={ index }
sectionTitle={ section.title }
/>
))}
</ul>
);

const DropdownMenu = ({ sections, ...props }) => (
<div className="dropdown input-dropdown">
<div className="dropdown input-dropdown" id="suggestions_box" >
{ sections.map((section, sectionIndex) => (
<DropdownMenuSection
{ ...props }
Expand Down
170 changes: 164 additions & 6 deletions src/FacetedTokenInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import DropdownMenu from './DropdownMenu';

import onLeftRight from './onLeftRight';

// if react-a11y is not used delete line 11 and 12
import a11y from 'react-a11y';
import ReactDOM from 'react-dom';

import {
BACKSPACE,
ENTER,
Expand All @@ -29,10 +33,22 @@ import {
isBackward
} from './key-utils';

// Abilitate accessibility test only in development mode
// a11y(React, { ReactDOM: ReactDOM });

export const DIRECTION_NONE = 'none';
export const DIRECTION_BACKWARD = 'backward';
export const DIRECTION_FORWARD = 'forward';

export const UNIQUE_ID = 'fti_';

let tokensLength = 0;
let tempFacet = '';
let tempDescription = '';
let supportText = '';
let tempToken = [];

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are variables that are needed for the a11ySupport method


const INPUT_STYLE = {
font: 'inherit',
lineHeight: 'inherit',
Expand All @@ -45,14 +61,25 @@ const INPUT_STYLE = {

const INPUT_SPY_WRAPPER_STYLE = {
position: 'absolute',
visibility: 'hidden',
top: '0px',
left: '0px',
width: '100%',
height: '0px',
overflow: 'hidden'
};

const A11Y_HELPER_STYLE = {
position: 'absolute',
height: '0px',
width: '0px',
margin: '0px',
padding: '0px',
overflow: 'hidden',
top: '0px',
bottom: '0px',
fontSize: '0px'
};

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adds styles for the helpers.

const INPUT_SPY_STYLE = {
display: 'block',
whiteSpace: 'pre',
Expand Down Expand Up @@ -102,7 +129,9 @@ export default class FacetedTokenInput extends Component {
searchText,
showDropDown,
focused,
textDirection
textDirection,
selectedSectionIndex,
selectedIndex
} = this.state;

const facetedTokenInputClass = classNames('compound-input', {
Expand All @@ -119,11 +148,30 @@ export default class FacetedTokenInput extends Component {
onFocus={ event => this.onFocus(event) }
onBlur={ event => this.onBlur(event) }
>

{ tokens.map(this.renderToken, this) }


{ /*
I'm an experiment to delete tokens in mobile
<a
style={ { width: '20px', background: 'pink', textAlign: 'center', display: (tokens.length) ? 'inline-block' : 'none' } }
onClick={ event => this.onBackspace() }
>
{ 'X' }
</a>
*/ }

<input
key="input"
ref="input"
role="combobox"
aria-expanded={ showDropDown }
aria-autocomplete="list"
aria-owns="suggestions_box"
aria-activedescendant={ UNIQUE_ID + 'section_0' + selectedSectionIndex + '_0' + selectedIndex }
aria-label="input"
aria-describedby="number_tokens"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aria-describedby="number_tokens"

this will read the number of tokens in the input

style={ INPUT_STYLE }
className="compound-input-field"
placeholder={ tokens.length ? '' : placeholder }
Expand All @@ -149,10 +197,105 @@ export default class FacetedTokenInput extends Component {
{ this.props.children }

{ showDropDown && this.renderDropdown() }



{ /* start test code */ }

{ /*
the following div needs to be hidden if this code is going into the
master branch (see A11Y_HELPER_STYLE)
style={ A11Y_HELPER_STYLE }
*/ }
<div
id="a11ysupport"
role="log"
aria-live="polite"
aria-relevant="all"
aria-atomic={ true }
style={ A11Y_HELPER_STYLE }
>

{ /*
<span id="tagsupport">
simply add all the tokens that exist in the input

<p></p>

this adds only the last token, it doesn't solve the current
problem since the additions count from the second one onwards

(tokens.length) ? this.a11ySupport(tokens[tokens.length - 1], tokens.length - 1) : ''

GOOGLE
<div aria-live="polite" aria-atomic="true">
pierpaolo.ramon aggiunto. Premi backspace per rimuovere il destinatario.
</div>


{ tokens.map(this.a11ySupport, this) }
</span>
*/ }

{ this.a11ySupport() }
</div>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this container is a live region and should read addition and removals of tokens.
aria-atomic is set to true because the entire phrase needs to be read and not only whats get changed


{ /*
this adds a string that thanks to aria-describedby present in the
input reads out the number of tokens present in the input in addition
to the specifics of the input
*/ }
<div id="number_tokens" style={ A11Y_HELPER_STYLE }>
{ tokens.length }
{ ' tokens in input' }
</div>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this regulate the exact phrase that gets read to specify the number of tokens in the input


{ /* end test code */ }



</div>
);
}


// start experimental method

// this should add a node that should be detected by aria-live="true"
a11ySupport() {
const { tokens } = this.state;
const { facet, description } = this.props.renderToken(tokens[tokens.length - 1] || []);

if (tokens.length === tokensLength + 1) {
supportText = 'added';
tempFacet = facet;
tempDescription = description;
tempToken[tokens.length - 1] = { facet, description };
}
else if (tokens.length === tokensLength - 1) {
supportText = 'deleted';
tempFacet = tempToken[tokens.length].facet;
tempDescription = tempToken[tokens.length].description;
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if a token is added I need the support text to be 'added' and I need to store the token in a temporary array.
If a token id removed I need to change the text to 'deleted' and I need to get the facet and description of the deleted token

tokensLength = tokens.length;

return (
<span
key={ 'test' + tokensLength }
>
{ tempFacet }
{ ' ' }
{ tempDescription }
{ ' ' }
{ supportText }
</span>
);
}

// end experimental method


renderDropdown() {
const { dropdownSections } = this.props;

Expand Down Expand Up @@ -223,7 +366,9 @@ export default class FacetedTokenInput extends Component {
if (noSelection || this.isInTokenSelection(tokens.length)) {
this.refs.input.focus();
}
else if (tokenSelectionDirection === DIRECTION_NONE) {
else if (tokenSelectionDirection === DIRECTION_NONE &&
this.refs['token' + tokenSelectionStart]) {

this.refs['token' + tokenSelectionStart].focus();
}
else {
Expand Down Expand Up @@ -272,9 +417,19 @@ export default class FacetedTokenInput extends Component {

const searchText = this.refs.input.value;

if (searchText) {
this.setState({
showDropDown: true
});
}
else {
this.setState({
showDropDown: false
});
}

this.setState({
searchText: searchText,
showDropDown: true,
selectedSectionIndex: -1,
selectedIndex: -1
});
Expand Down Expand Up @@ -425,7 +580,8 @@ export default class FacetedTokenInput extends Component {
tokenSelectionStart
} = this.state;

const keyDirection = isForward(event, dir || textDirection) ? DIRECTION_FORWARD
const keyDirection = isForward(event, dir || textDirection)
? DIRECTION_FORWARD
: isBackward(event, dir || textDirection) ? DIRECTION_BACKWARD
: DIRECTION_NONE;

Expand Down Expand Up @@ -521,7 +677,9 @@ export default class FacetedTokenInput extends Component {
});
}

if (event.target !== this.refs.input) {
if (event.target !== this.refs.input ||
(event.target === this.refs.input && !searchText)) {

event.preventDefault();
}
}
Expand Down
Loading