Skip to content

Commit

Permalink
added accessibility folder, added helper fucntions for aria messages …
Browse files Browse the repository at this point in the history
…for focusedValue, focusedOption and results context
  • Loading branch information
gwyneplaine committed Jun 19, 2018
1 parent 4da6217 commit b3697d1
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 77 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"emotion": "^9.1.2",
"prop-types": "^15.6.0",
"raf": "^3.4.0",
"react-aria-live": "^2.0.2",
"react-input-autosize": "^2.2.1",
"react-transition-group": "^2.2.1"
},
Expand Down
101 changes: 33 additions & 68 deletions src/Select.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import React, { Component, type ElementRef, type Node } from 'react';

import { createFilter } from './filters';
import { DummyInput, ScrollBlock, ScrollCaptor } from './internal/index';
import {
valueFocusAriaMessage,
optionFocusAriaMessage,
resultsAriaMessage,
valueEventAriaMessage,
instructionsAriaMessage,
type InstructionsContext,
type ValueEventContext,
} from './accessibility';

import {
classNames,
Expand Down Expand Up @@ -54,34 +63,6 @@ type FormatOptionLabelMeta = {
selectValue: ValueType,
};

type InstructionsData = { event: string, context?: InstructionsContext};
type InstructionsContext = { isSearchable?: boolean, isMulti?: boolean };
type ValueEventData = {event: string, context: ValueEventContext};
type ValueEventContext = { value: string };

const instructions = (event, context?: InstructionsContext = {}) => {
const { isSearchable, isMulti } = context;
switch (event) {
case 'menu':
return 'Use Up and Down to choose options, press Backspace to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.';
case 'value':
return `Select is focused ${ isSearchable ? ',type to refine list' : '' }, press Down to open the menu, ${ isMulti ? ' press left to focus selected values' : '' }`;
case 'input':
return 'Use left and right to toggle between focused values, press Enter to remove the currently focused value';
}
};

const valueEvent = (event, context: ValueEventContext) => {
const { value } = context;
switch (event) {
case 'deselect-option':
case 'pop-value':
case 'remove-value':
return `option ${value}, deselected.`;
case 'select-option':
return `option ${value}, selected.`;
}
};

export type Props = {
/* Aria label (for assistive tech) */
Expand Down Expand Up @@ -251,7 +232,7 @@ export const defaultProps = {
pageSize: 5,
placeholder: 'Select...',
screenReaderStatus: ({ count }: { count: number }) =>
`${count} result${count !== 1 ? 's' : ''} available.`,
`${count} result${count !== 1 ? 's' : ''} available`,
styles: {},
tabIndex: '0',
tabSelectsValue: true,
Expand All @@ -267,18 +248,10 @@ type State = {
ariaLiveContext: string,
inputIsHidden: boolean,
isFocused: boolean,
instructions: string,
feedback: string,
focusedOption: OptionType | null,
focusedValue: OptionType | null,
menuOptions: MenuOptions,
selectValue: OptionsType,
a11yState: {
selection?: string,
valueFocus?: string,
optionFocus?: string,
instructions?: string
},
};

type ElRef = ElementRef<*>;
Expand Down Expand Up @@ -312,9 +285,6 @@ export default class Select extends Component<Props, State> {
isFocused: false,
menuOptions: { render: [], focusable: [] },
selectValue: [],
instructions: '',
feedback: '',
a11yState: {},
};
constructor(props: Props) {
super(props);
Expand Down Expand Up @@ -352,7 +322,6 @@ export default class Select extends Component<Props, State> {
const menuOptions = this.buildMenuOptions(nextProps, selectValue);
const focusedValue = this.getNextFocusedValue(selectValue);
const focusedOption = this.getNextFocusedOption(menuOptions.focusable);
// this.getNextAnnouncement(nextProps, this.props, focusedOption);
this.setState({ menuOptions, selectValue, focusedOption, focusedValue });
}
// some updates should toggle the state of the input visibility
Expand Down Expand Up @@ -412,13 +381,11 @@ export default class Select extends Component<Props, State> {
// ==============================

onMenuOpen() {
// TODO: remove this, as instructions are explicitly to do with focus / pseudo focus changes.
this.props.onMenuOpen();
}
onMenuClose() {
const { isSearchable, isMulti } = this.props;
// TODO: remove this, as instructions are explicitly to do with focus / pseudo focus changes.
this.announceAriaLiveContext({ event: 'input', context: { isSearchable, isMulti }});
this.announceAriaLiveContext({ event: 'input', context: { isSearchable, isMulti } });
this.onInputChange('', { action: 'menu-close' });
this.props.onMenuClose();
}
Expand Down Expand Up @@ -464,9 +431,9 @@ export default class Select extends Component<Props, State> {
this.setState({
focusedValue: null,
focusedOption: menuOptions.focusable[openAtIndex],
}, () => {
this.announceAriaLiveContext({ event: 'menu' });
});

this.announceAriaLiveContext({ event: 'menu' });
}
focusValue(direction: 'previous' | 'next') {
const { isMulti, isSearchable } = this.props;
Expand Down Expand Up @@ -677,10 +644,10 @@ export default class Select extends Component<Props, State> {

return nextFocusedOption;
}
getOptionLabel(data: OptionType): string {
getOptionLabel = (data: OptionType): string => {
return this.props.getOptionLabel(data);
}
getOptionValue(data: OptionType): string {
getOptionValue = (data: OptionType): string => {
return this.props.getOptionValue(data);
}
getStyles = (key: string, props: {}): {} => {
Expand All @@ -707,14 +674,14 @@ export default class Select extends Component<Props, State> {
// ==============================
// Helpers
// ==============================
announceAriaLiveSelection = (data: ValueEventData) => {
announceAriaLiveSelection = ({ event, context }: { event: string, context: ValueEventContext }) => {
this.setState({
ariaLiveSelection: valueEvent(data.event, data.context),
ariaLiveSelection: valueEventAriaMessage(event, context),
});
}
announceAriaLiveContext = (data: InstructionsData) => {
announceAriaLiveContext = ({ event, context }: { event: string, context?: InstructionsContext }) => {
this.setState({
ariaLiveContext: instructions(data.event, data.context),
ariaLiveContext: instructionsAriaMessage(event, { ...context, label: this.props['aria-label'] }),
});
};

Expand Down Expand Up @@ -929,7 +896,6 @@ export default class Select extends Component<Props, State> {
this.setState({
focusedValue: null,
isFocused: false,
a11yState: {},
});
};
onOptionHover = (focusedOption: OptionType) => {
Expand Down Expand Up @@ -1151,9 +1117,9 @@ export default class Select extends Component<Props, State> {
const { ariaLiveContext, selectValue, focusedValue, focusedOption } = this.state;
const { options, menuIsOpen, inputValue, screenReaderStatus } = this.props;
return [
focusedValue ?`value ${this.getOptionLabel(focusedValue)} focused, ${selectValue.indexOf(focusedValue) + 1} of ${selectValue.length}`: null,
(focusedOption && menuIsOpen) ? `option ${this.getOptionLabel(focusedOption)} focused, ${options.indexOf(focusedOption) + 1} of ${options.length}` : null,
inputValue ? `${screenReaderStatus({ count: this.countOptions() })} for search term ${inputValue}` : null,
focusedValue ? valueFocusAriaMessage({ focusedValue, getOptionLabel: this.getOptionLabel, selectValue }) : null,
(focusedOption && menuIsOpen) ? optionFocusAriaMessage({ focusedOption, getOptionLabel: this.getOptionLabel, options }) : null,
inputValue ? resultsAriaMessage({ inputValue, screenReaderMessage: screenReaderStatus({ count: this.countOptions() }) }) : null,
ariaLiveContext
].join(' ');
}
Expand Down Expand Up @@ -1542,6 +1508,16 @@ export default class Select extends Component<Props, State> {
}
}

renderLiveRegion () {
if (!this.state.isFocused) return null;
return (
<A11yText aria-live="assertive">
<p id="aria-selection-event">&nbsp;{this.state.ariaLiveSelection}</p>
<p id="aria-context">&nbsp;{this.constructAriaLiveMessage()}</p>
</A11yText>
);
}

render() {
const {
Control,
Expand All @@ -1566,18 +1542,7 @@ export default class Select extends Component<Props, State> {
isDisabled={isDisabled}
isFocused={isFocused}
>
<span style={{
position: 'fixed',
height: '300px',
zIndex: 9999,
top: 0,
left: 0,
}}>
<A11yText aria-live="assertive">
<p>&nbsp;{this.state.ariaLiveSelection}</p>
<p>&nbsp;{this.constructAriaLiveMessage()}</p>
</A11yText>
</span>
{this.renderLiveRegion()}
<Control
{...commonProps}
innerProps={{
Expand Down
30 changes: 30 additions & 0 deletions src/accessibility/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export type InstructionsContext = { isSearchable?: boolean, isMulti?: boolean, label?: string };
export type ValueEventContext = { value: string };

export const instructionsAriaMessage = (event, context?: InstructionsContext = {}) => {
const { isSearchable, isMulti, label } = context;
switch (event) {
case 'menu':
return 'Use Up and Down to choose options, press Backspace to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.';
case 'input':
return `${label ? label : 'Select'} is focused ${ isSearchable ? ',type to refine list' : '' }, press Down to open the menu, ${ isMulti ? ' press left to focus selected values' : '' }`;
case 'value':
return 'Use left and right to toggle between focused values, press Enter to remove the currently focused value';
}
};

export const valueEventAriaMessage = (event, context: ValueEventContext) => {
const { value } = context;
switch (event) {
case 'deselect-option':
case 'pop-value':
case 'remove-value':
return `option ${value}, deselected.`;
case 'select-option':
return `option ${value}, selected.`;
}
};

export const valueFocusAriaMessage = ({ focusedValue, getOptionLabel, selectValue }) => `value ${getOptionLabel(focusedValue)} focused, ${selectValue.indexOf(focusedValue) + 1} of ${selectValue.length}.`;
export const optionFocusAriaMessage = ({ focusedOption, getOptionLabel, options }) => `option ${getOptionLabel(focusedOption)} focused, ${options.indexOf(focusedOption) + 1} of ${options.length}.`;
export const resultsAriaMessage = ({ inputValue, screenReaderMessage }) => `${screenReaderMessage} for search term ${inputValue}.`;
13 changes: 5 additions & 8 deletions src/primitives.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,14 @@ export const Input = createPrimitive('input');
export const A11yText = (props: any) => (
<span
css={{
display: 'block',
zIndex: 9999,
border: 0,
// clip: 'rect(1px, 1px, 1px, 1px)',
height: '100px',
// width: '200px',
position: 'relative',
// overflow: 'hidden',
clip: 'rect(1px, 1px, 1px, 1px)',
height: 1,
width: 1,
position: 'absolute',
overflow: 'hidden',
padding: 0,
// top:0,
// left:0,
whiteSpace: 'nowrap',
backgroundColor: 'red',
color: 'blue',
Expand Down

0 comments on commit b3697d1

Please sign in to comment.