From 5b1aa6c03ec94b46b79641b041939b17fa93bc9e Mon Sep 17 00:00:00 2001 From: Denis Kashkovsky Date: Wed, 16 Jan 2019 17:11:47 +0200 Subject: [PATCH 1/8] Issue #3352: Aria messages cannot be localized and one of the messages contains wrong statement --- src/Select.js | 60 ++++++++++++----- .../__snapshots__/Async.test.js.snap | 65 +++++++++++++++++++ .../__snapshots__/AsyncCreatable.test.js.snap | 65 +++++++++++++++++++ .../__snapshots__/Select.test.js.snap | 56 ++++++++++++++++ .../__snapshots__/StateManaged.test.js.snap | 9 +++ 5 files changed, 237 insertions(+), 18 deletions(-) diff --git a/src/Select.js b/src/Select.js index 365055bf5b..3bbbf9efcf 100644 --- a/src/Select.js +++ b/src/Select.js @@ -74,6 +74,21 @@ type FormatOptionLabelMeta = { inputValue: string, selectValue: ValueType, }; +export type Accessibility = { + valueFocusAriaMessage: (args: { + focusedValue?: OptionType, + getOptionLabel: (data: OptionType) => string, + selectValue: OptionsType | Array + }) => string, + optionFocusAriaMessage: (args: { + focusedOption: OptionType, + getOptionLabel: (data: OptionType) => string, + options: OptionsType + }) => string, + resultsAriaMessage: (args: { inputValue: string, screenReaderMessage: string }) => string, + valueEventAriaMessage: (event: any, context: ValueEventContext) => string, + instructionsAriaMessage: (event: any, context?: InstructionsContext) => string +}; export type Props = { /* Aria label (for assistive tech) */ @@ -244,6 +259,8 @@ export type Props = { tabSelectsValue: boolean, /* The value of the select; reflected by the selected option */ value: ValueType, + /* Custom ARIA message functions */ + accessibility?: Accessibility }; export const defaultProps = { @@ -284,6 +301,13 @@ export const defaultProps = { styles: {}, tabIndex: '0', tabSelectsValue: true, + accessibility: { + valueFocusAriaMessage, + optionFocusAriaMessage, + resultsAriaMessage, + valueEventAriaMessage, + instructionsAriaMessage + }, }; type MenuOptions = { @@ -809,7 +833,7 @@ export default class Select extends Component { context: ValueEventContext, }) => { this.setState({ - ariaLiveSelection: valueEventAriaMessage(event, context), + ariaLiveSelection: this.props.accessibility ? this.props.accessibility.valueEventAriaMessage(event, context) : '', }); }; announceAriaLiveContext = ({ @@ -820,10 +844,10 @@ export default class Select extends Component { context?: InstructionsContext, }) => { this.setState({ - ariaLiveContext: instructionsAriaMessage(event, { + ariaLiveContext: this.props.accessibility ? this.props.accessibility.instructionsAriaMessage(event, { ...context, label: this.props['aria-label'], - }), + }) : '', }); }; @@ -1354,30 +1378,30 @@ export default class Select extends Component { focusedValue, focusedOption, } = this.state; - const { options, menuIsOpen, inputValue, screenReaderStatus } = this.props; + const { options, menuIsOpen, inputValue, screenReaderStatus, accessibility } = this.props; // An aria live message representing the currently focused value in the select. - const focusedValueMsg = focusedValue - ? valueFocusAriaMessage({ - focusedValue, - getOptionLabel: this.getOptionLabel, - selectValue, - }) + const focusedValueMsg = focusedValue && accessibility + ? accessibility.valueFocusAriaMessage({ + focusedValue, + getOptionLabel: this.getOptionLabel, + selectValue, + }) : ''; // An aria live message representing the currently focused option in the select. const focusedOptionMsg = - focusedOption && menuIsOpen - ? optionFocusAriaMessage({ - focusedOption, - getOptionLabel: this.getOptionLabel, - options, - }) + focusedOption && menuIsOpen && accessibility + ? accessibility.optionFocusAriaMessage({ + focusedOption, + getOptionLabel: this.getOptionLabel, + options, + }) : ''; // An aria live message representing the set of focusable results and current searchterm/inputvalue. - const resultsMsg = resultsAriaMessage({ + const resultsMsg = accessibility ? accessibility.resultsAriaMessage({ inputValue, screenReaderMessage: screenReaderStatus({ count: this.countOptions() }), - }); + }) : ''; return `${focusedValueMsg} ${focusedOptionMsg} ${resultsMsg} ${ariaLiveContext}`; } diff --git a/src/__tests__/__snapshots__/Async.test.js.snap b/src/__tests__/__snapshots__/Async.test.js.snap index 00ab8cc588..954d6d5e01 100644 --- a/src/__tests__/__snapshots__/Async.test.js.snap +++ b/src/__tests__/__snapshots__/Async.test.js.snap @@ -18,6 +18,15 @@ exports[`defaults - snapshot 1`] = ` options={Array []} > snapshot 1`] = ` { + return `custom aria option focus message: ${getOptionLabel(focusedOption)}`; + } + }} + id="select-example" + className="basic-single" + classNamePrefix="select" + name="color" + options={colourOptions} + /> + + ); + } +} diff --git a/src/Select.js b/src/Select.js index 528d61f8d6..a6d8f78707 100644 --- a/src/Select.js +++ b/src/Select.js @@ -21,6 +21,8 @@ import { instructionsAriaMessage, type InstructionsContext, type ValueEventContext, + type ValueEventType, + type InstructionEventType, } from './accessibility/index'; import { @@ -74,11 +76,27 @@ type FormatOptionLabelMeta = { inputValue: string, selectValue: ValueType, }; -export type Accessibility = { +export type AccessibilityProp = { + valueFocusAriaMessage?: (args: { + focusedValue: OptionType, + getOptionLabel: (data: OptionType) => string, + selectValue: OptionsType + }) => string, + optionFocusAriaMessage?: (args: { + focusedOption: OptionType, + getOptionLabel: (data: OptionType) => string, + options: OptionsType + }) => string, + resultsAriaMessage?: (args: { inputValue: string, screenReaderMessage: string }) => string, + valueEventAriaMessage?: (event: ValueEventType, context: ValueEventContext) => string, + instructionsAriaMessage?: (event: InstructionEventType, context?: InstructionsContext) => string +}; + +export type AccessibilityConfig = { valueFocusAriaMessage: (args: { focusedValue: OptionType, getOptionLabel: (data: OptionType) => string, - selectValue: OptionsType | Array + selectValue: OptionsType }) => string, optionFocusAriaMessage: (args: { focusedOption: OptionType, @@ -86,9 +104,9 @@ export type Accessibility = { options: OptionsType }) => string, resultsAriaMessage: (args: { inputValue: string, screenReaderMessage: string }) => string, - valueEventAriaMessage: (event: any, context: ValueEventContext) => string, - instructionsAriaMessage: (event: any, context?: InstructionsContext) => string -}; + valueEventAriaMessage: (event: ValueEventType, context: ValueEventContext) => string, + instructionsAriaMessage: (event: InstructionEventType, context?: InstructionsContext) => string +} export type Props = { /* Aria label (for assistive tech) */ @@ -260,7 +278,7 @@ export type Props = { /* The value of the select; reflected by the selected option */ value: ValueType, /* Custom ARIA message functions */ - accessibility?: Accessibility + accessibility?: AccessibilityProp }; export const defaultProps = { @@ -347,7 +365,7 @@ export default class Select extends Component { // Misc. Instance Properties // ------------------------------ - + accessibility: AccessibilityConfig; blockOptionHover: boolean = false; clearFocusValueOnUpdate: boolean = false; commonProps: any; // TODO @@ -388,6 +406,7 @@ export default class Select extends Component { super(props); const { value } = props; this.cacheComponents = memoizeOne(this.cacheComponents, isEqual).bind(this); + this.accessibility = this.getAccessibilityConfig(props.accessibility); this.cacheComponents(props.components); this.instancePrefix = 'react-select-' + (this.props.instanceId || ++instanceId); @@ -415,6 +434,7 @@ export default class Select extends Component { const { options, value, inputValue } = this.props; // re-cache custom components this.cacheComponents(nextProps.components); + this.accessibility = this.getAccessibilityConfig(nextProps.accessibility); // rebuild the menu options if ( nextProps.value !== value || @@ -825,29 +845,39 @@ export default class Select extends Component { // ============================== // Helpers // ============================== + getAccessibilityConfig (accessibilityObj?: AccessibilityProp): AccessibilityConfig { + return { + valueFocusAriaMessage, + optionFocusAriaMessage, + resultsAriaMessage, + valueEventAriaMessage, + instructionsAriaMessage, + ...accessibilityObj, + }; + }; announceAriaLiveSelection = ({ event, context, }: { - event: string, + event: ValueEventType, context: ValueEventContext, }) => { this.setState({ - ariaLiveSelection: this.props.accessibility ? this.props.accessibility.valueEventAriaMessage(event, context) : '', + ariaLiveSelection: this.accessibility.valueEventAriaMessage(event, context), }); }; announceAriaLiveContext = ({ event, context, }: { - event: string, + event: InstructionEventType, context?: InstructionsContext, }) => { this.setState({ - ariaLiveContext: this.props.accessibility ? this.props.accessibility.instructionsAriaMessage(event, { + ariaLiveContext: this.accessibility.instructionsAriaMessage(event, { ...context, label: this.props['aria-label'], - }) : '', + }), }); }; @@ -1378,11 +1408,11 @@ export default class Select extends Component { focusedValue, focusedOption, } = this.state; - const { options, menuIsOpen, inputValue, screenReaderStatus, accessibility } = this.props; + const { options, menuIsOpen, inputValue, screenReaderStatus } = this.props; // An aria live message representing the currently focused value in the select. - const focusedValueMsg = focusedValue && accessibility - ? accessibility.valueFocusAriaMessage({ + const focusedValueMsg = focusedValue && this.accessibility + ? this.accessibility.valueFocusAriaMessage({ focusedValue, getOptionLabel: this.getOptionLabel, selectValue, @@ -1390,18 +1420,17 @@ export default class Select extends Component { : ''; // An aria live message representing the currently focused option in the select. const focusedOptionMsg = - focusedOption && menuIsOpen && accessibility - ? accessibility.optionFocusAriaMessage({ + focusedOption && menuIsOpen ? this.accessibility.optionFocusAriaMessage({ focusedOption, getOptionLabel: this.getOptionLabel, options, }) : ''; // An aria live message representing the set of focusable results and current searchterm/inputvalue. - const resultsMsg = accessibility ? accessibility.resultsAriaMessage({ + const resultsMsg = this.accessibility.resultsAriaMessage({ inputValue, screenReaderMessage: screenReaderStatus({ count: this.countOptions() }), - }) : ''; + }); return `${focusedValueMsg} ${focusedOptionMsg} ${resultsMsg} ${ariaLiveContext}`; } @@ -1811,10 +1840,14 @@ export default class Select extends Component { renderLiveRegion() { if (!this.state.isFocused) return null; return ( - -

 {this.state.ariaLiveSelection}

-

 {this.constructAriaLiveMessage()}

-
+ + +

 {this.state.ariaLiveSelection}

+
+ +

 {this.constructAriaLiveMessage()}

+
+
); } From 88790255a02733efe87150248e1cdb8ab84af583 Mon Sep 17 00:00:00 2001 From: gwyneplaine Date: Fri, 10 May 2019 15:36:55 +1000 Subject: [PATCH 4/8] fix tests --- src/__tests__/Select.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/__tests__/Select.test.js b/src/__tests__/Select.test.js index 5613053ed1..3470b21f22 100644 --- a/src/__tests__/Select.test.js +++ b/src/__tests__/Select.test.js @@ -1103,7 +1103,6 @@ cases('Clicking Enter on a focused select', ({ props = BASIC_PROPS, expectedValu const selectWrapper = wrapper.find(Select); selectWrapper.instance().setState({ focusedOption: OPTIONS[0] }); selectWrapper.instance().onKeyDown(event); - console.log(event.defaultPrevented); expect(event.defaultPrevented).toBe(expectedValue); }, { 'while menuIsOpen && focusedOption && !isComposing > should invoke event.preventDefault': { From f8c7f013de6a4a7c2f49a6353dc35d98ff0a5454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20R=C3=A5degran?= Date: Thu, 13 Aug 2020 13:27:19 +0200 Subject: [PATCH 5/8] Added test for accessibility prop --- .../react-select/src/__tests__/Select.test.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/react-select/src/__tests__/Select.test.js b/packages/react-select/src/__tests__/Select.test.js index b0ab8d52c6..2403bf6cc0 100644 --- a/packages/react-select/src/__tests__/Select.test.js +++ b/packages/react-select/src/__tests__/Select.test.js @@ -1862,6 +1862,44 @@ test('accessibility > interacting with disabled options shows correct A11yText', ); }); +test('accessibility > A11yTexts can be provided through accessibility prop', () => { + + const accessibility = { + valueEventAriaMessage: ( + event, + context + ) => { + const { value, isDisabled } = context; + if (event === 'select-option' && !isDisabled) { + return `CUSTOM: option ${value} is selected.`; + } + } + }; + + let { container } = render( + { - return `custom aria option focus message: ${getOptionLabel(focusedOption)}`; - } - }} - id="select-example" - className="basic-single" - classNamePrefix="select" - name="color" - options={colourOptions} - /> - - ); - } -} From 24d1e60f5f3fa998bdea6cf93e962013030c3428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20R=C3=A5degran?= Date: Wed, 9 Sep 2020 10:30:08 +0200 Subject: [PATCH 8/8] Extracted accessibility related types --- packages/react-select/src/Select.js | 33 ++----------------- .../react-select/src/accessibility/index.js | 32 ++++++++++++++++++ 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/react-select/src/Select.js b/packages/react-select/src/Select.js index ed5fd111e6..0a0f8bebc9 100644 --- a/packages/react-select/src/Select.js +++ b/packages/react-select/src/Select.js @@ -18,6 +18,8 @@ import { resultsAriaMessage, valueEventAriaMessage, instructionsAriaMessage, + type AccessibilityProp, + type AccessibilityConfig, type InstructionsContext, type ValueEventContext, type ValueEventType, @@ -75,37 +77,6 @@ type FormatOptionLabelMeta = { inputValue: string, selectValue: ValueType, }; -export type AccessibilityProp = { - valueFocusAriaMessage?: (args: { - focusedValue: OptionType, - getOptionLabel: (data: OptionType) => string, - selectValue: OptionsType - }) => string, - optionFocusAriaMessage?: (args: { - focusedOption: OptionType, - getOptionLabel: (data: OptionType) => string, - options: OptionsType - }) => string, - resultsAriaMessage?: (args: { inputValue: string, screenReaderMessage: string }) => string, - valueEventAriaMessage?: (event: ValueEventType, context: ValueEventContext) => string, - instructionsAriaMessage?: (event: InstructionEventType, context?: InstructionsContext) => string -}; - -export type AccessibilityConfig = { - valueFocusAriaMessage: (args: { - focusedValue: OptionType, - getOptionLabel: (data: OptionType) => string, - selectValue: OptionsType - }) => string, - optionFocusAriaMessage: (args: { - focusedOption: OptionType, - getOptionLabel: (data: OptionType) => string, - options: OptionsType - }) => string, - resultsAriaMessage: (args: { inputValue: string, screenReaderMessage: string }) => string, - valueEventAriaMessage: (event: ValueEventType, context: ValueEventContext) => string, - instructionsAriaMessage: (event: InstructionEventType, context?: InstructionsContext) => string -} export type Props = { /* Custom ARIA message functions */ diff --git a/packages/react-select/src/accessibility/index.js b/packages/react-select/src/accessibility/index.js index acc652f8d7..52b9bb0306 100644 --- a/packages/react-select/src/accessibility/index.js +++ b/packages/react-select/src/accessibility/index.js @@ -2,6 +2,38 @@ import { type OptionType, type OptionsType } from '../types'; +export type AccessibilityProp = { + valueFocusAriaMessage?: (args: { + focusedValue: OptionType, + getOptionLabel: (data: OptionType) => string, + selectValue: OptionsType + }) => string, + optionFocusAriaMessage?: (args: { + focusedOption: OptionType, + getOptionLabel: (data: OptionType) => string, + options: OptionsType + }) => string, + resultsAriaMessage?: (args: { inputValue: string, screenReaderMessage: string }) => string, + valueEventAriaMessage?: (event: ValueEventType, context: ValueEventContext) => string, + instructionsAriaMessage?: (event: InstructionEventType, context?: InstructionsContext) => string +}; + +export type AccessibilityConfig = { + valueFocusAriaMessage: (args: { + focusedValue: OptionType, + getOptionLabel: (data: OptionType) => string, + selectValue: OptionsType + }) => string, + optionFocusAriaMessage: (args: { + focusedOption: OptionType, + getOptionLabel: (data: OptionType) => string, + options: OptionsType + }) => string, + resultsAriaMessage: (args: { inputValue: string, screenReaderMessage: string }) => string, + valueEventAriaMessage: (event: ValueEventType, context: ValueEventContext) => string, + instructionsAriaMessage: (event: InstructionEventType, context?: InstructionsContext) => string +} + export type InstructionsContext = { isSearchable?: boolean, isMulti?: boolean,