From fb24ab359db24fcf0945033cff30f571310e5c8c Mon Sep 17 00:00:00 2001 From: Progyan Bhattacharya Date: Wed, 9 Dec 2020 20:53:39 +0530 Subject: [PATCH 1/2] [ARIA] Add more Accessible Tags in Input and MenuList Added combo box role for Input item, listbox role to MenuList and option role to MenuItems. Added relationship between elements by ID. Signed-off-by: Progyan Bhattacharya --- packages/react-select/src/Select.js | 11 +- .../react-select/src/__tests__/Select.test.js | 5 + .../__snapshots__/Async.test.js.snap | 2 + .../__snapshots__/AsyncCreatable.test.js.snap | 2 + .../__snapshots__/Creatable.test.js.snap | 2 + .../__snapshots__/Select.test.js.snap | 200 ++++++++++++++++++ .../__snapshots__/StateManaged.test.js.snap | 2 + packages/react-select/src/components/Menu.js | 15 +- .../react-select/src/components/Option.js | 1 + 9 files changed, 237 insertions(+), 3 deletions(-) diff --git a/packages/react-select/src/Select.js b/packages/react-select/src/Select.js index c6cf12ae79..8832d42bf1 100644 --- a/packages/react-select/src/Select.js +++ b/packages/react-select/src/Select.js @@ -806,7 +806,7 @@ export default class Select extends Component { const custom = this.props.styles[key]; return custom ? custom(base, props) : base; }; - getElementId = (element: 'group' | 'input' | 'listbox' | 'option') => { + getElementId = (element: 'group' | 'input' | 'listbox' | 'combobox' | 'option') => { return `${this.instancePrefix}-${element}`; }; getActiveDescendentId = () => { @@ -1421,9 +1421,13 @@ export default class Select extends Component { const { inputIsHidden } = this.state; const id = inputId || this.getElementId('input'); + const menuRole = isSearchable ? 'combobox' : 'listbox'; + const menuId = this.getElementId(menuRole); // aria attributes makes the JSX "noisy", separated for clarity const ariaAttributes = { + role: menuRole, + 'aria-owns': menuId, 'aria-autocomplete': 'list', 'aria-label': this.props['aria-label'], 'aria-labelledby': this.props['aria-labelledby'], @@ -1668,6 +1672,7 @@ export default class Select extends Component { noOptionsMessage, onMenuScrollToTop, onMenuScrollToBottom, + isSearchable, } = this.props; if (!menuIsOpen) return null; @@ -1728,6 +1733,9 @@ export default class Select extends Component { menuShouldScrollIntoView, }; + const menuRole = isSearchable ? 'combobox' : 'listbox'; + const menuId = this.getElementId(menuRole); + const menuElement = ( {({ ref, placerProps: { placement, maxHeight } }) => ( @@ -1750,6 +1758,7 @@ export default class Select extends Component { { expect(container).toMatchSnapshot(); }); +test('snapshot - listbox', () => { + const { container } = render( diff --git a/packages/react-select/src/__tests__/__snapshots__/Async.test.js.snap b/packages/react-select/src/__tests__/__snapshots__/Async.test.js.snap index 23814a323b..fe5a067aed 100644 --- a/packages/react-select/src/__tests__/__snapshots__/Async.test.js.snap +++ b/packages/react-select/src/__tests__/__snapshots__/Async.test.js.snap @@ -160,10 +160,12 @@ exports[`defaults - snapshot 1`] = ` > `; + +exports[`snapshot - listbox 1`] = ` +.emotion-8 { + position: relative; + box-sizing: border-box; +} + +.emotion-7 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: hsl(0,0%,100%); + border-color: hsl(0,0%,80%); + border-radius: 4px; + border-style: solid; + border-width: 1px; + cursor: default; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + min-height: 38px; + outline: 0 !important; + position: relative; + -webkit-transition: all 100ms; + transition: all 100ms; + box-sizing: border-box; +} + +.emotion-7:hover { + border-color: hsl(0,0%,70%); +} + +.emotion-2 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding: 2px 8px; + -webkit-overflow-scrolling: touch; + position: relative; + overflow: hidden; + box-sizing: border-box; +} + +.emotion-0 { + color: hsl(0,0%,50%); + margin-left: 2px; + margin-right: 2px; + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + box-sizing: border-box; +} + +.emotion-6 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + align-self: stretch; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + box-sizing: border-box; +} + +.emotion-3 { + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + align-self: stretch; + background-color: hsl(0,0%,80%); + margin-bottom: 8px; + margin-top: 8px; + width: 1px; + box-sizing: border-box; +} + +.emotion-5 { + color: hsl(0,0%,80%); + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 8px; + -webkit-transition: color 150ms; + transition: color 150ms; + box-sizing: border-box; +} + +.emotion-5:hover { + color: hsl(0,0%,60%); +} + +.emotion-4 { + display: inline-block; + fill: currentColor; + line-height: 1; + stroke: currentColor; + stroke-width: 0; +} + +.emotion-1 { + background: 0; + border: 0; + font-size: inherit; + outline: 0; + padding: 0; + width: 1px; + color: transparent; + left: -100px; + opacity: 0; + position: relative; + -webkit-transform: scale(0); + -ms-transform: scale(0); + transform: scale(0); +} + +
+
+
+
+
+ Select... +
+ +
+
+ + +
+
+
+
+`; diff --git a/packages/react-select/src/__tests__/__snapshots__/StateManaged.test.js.snap b/packages/react-select/src/__tests__/__snapshots__/StateManaged.test.js.snap index 64ab356056..ff90bb7740 100644 --- a/packages/react-select/src/__tests__/__snapshots__/StateManaged.test.js.snap +++ b/packages/react-select/src/__tests__/__snapshots__/StateManaged.test.js.snap @@ -160,10 +160,12 @@ exports[`defaults > snapshot 1`] = ` > { - const { children, className, cx, getStyles, isMulti, innerRef, innerProps } = props; + const { children, className, cx, getStyles, isMulti, innerRef, innerProps, id } = props; + + // aria attributes makes the JSX "noisy", separated for clarity + const ariaAttributes = { + role: 'listbox', + 'aria-expanded': true, + }; + return (
{ )} ref={innerRef} {...innerProps} + {...ariaAttributes} > {children}
diff --git a/packages/react-select/src/components/Option.js b/packages/react-select/src/components/Option.js index de4eeff676..0e27bd8e31 100644 --- a/packages/react-select/src/components/Option.js +++ b/packages/react-select/src/components/Option.js @@ -84,6 +84,7 @@ const Option = (props: OptionProps) => { } = props; return (
Date: Fri, 25 Dec 2020 14:54:16 +0530 Subject: [PATCH 2/2] fixup! [ARIA] Add more Accessible Tags in Input and MenuList --- packages/react-select/src/Select.js | 8 +++----- .../src/__tests__/__snapshots__/Async.test.js.snap | 2 +- .../__tests__/__snapshots__/AsyncCreatable.test.js.snap | 2 +- .../src/__tests__/__snapshots__/Creatable.test.js.snap | 2 +- .../src/__tests__/__snapshots__/Select.test.js.snap | 4 ++-- .../src/__tests__/__snapshots__/StateManaged.test.js.snap | 2 +- packages/react-select/src/components/Menu.js | 2 +- packages/react-select/src/components/Option.js | 1 + 8 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/react-select/src/Select.js b/packages/react-select/src/Select.js index 8832d42bf1..7f4c0efa4d 100644 --- a/packages/react-select/src/Select.js +++ b/packages/react-select/src/Select.js @@ -806,7 +806,7 @@ export default class Select extends Component { const custom = this.props.styles[key]; return custom ? custom(base, props) : base; }; - getElementId = (element: 'group' | 'input' | 'listbox' | 'combobox' | 'option') => { + getElementId = (element: 'group' | 'input' | 'menu' | 'option') => { return `${this.instancePrefix}-${element}`; }; getActiveDescendentId = () => { @@ -1422,7 +1422,7 @@ export default class Select extends Component { const id = inputId || this.getElementId('input'); const menuRole = isSearchable ? 'combobox' : 'listbox'; - const menuId = this.getElementId(menuRole); + const menuId = this.getElementId('menu'); // aria attributes makes the JSX "noisy", separated for clarity const ariaAttributes = { @@ -1672,7 +1672,6 @@ export default class Select extends Component { noOptionsMessage, onMenuScrollToTop, onMenuScrollToBottom, - isSearchable, } = this.props; if (!menuIsOpen) return null; @@ -1733,8 +1732,7 @@ export default class Select extends Component { menuShouldScrollIntoView, }; - const menuRole = isSearchable ? 'combobox' : 'listbox'; - const menuId = this.getElementId(menuRole); + const menuId = this.getElementId('menu'); const menuElement = ( diff --git a/packages/react-select/src/__tests__/__snapshots__/Async.test.js.snap b/packages/react-select/src/__tests__/__snapshots__/Async.test.js.snap index fe5a067aed..3584ea3697 100644 --- a/packages/react-select/src/__tests__/__snapshots__/Async.test.js.snap +++ b/packages/react-select/src/__tests__/__snapshots__/Async.test.js.snap @@ -160,7 +160,7 @@ exports[`defaults - snapshot 1`] = ` > snapshot 1`] = ` > { className )} ref={innerRef} - {...innerProps} {...ariaAttributes} + {...innerProps} > {children}
diff --git a/packages/react-select/src/components/Option.js b/packages/react-select/src/components/Option.js index 0e27bd8e31..8cf245a9cc 100644 --- a/packages/react-select/src/components/Option.js +++ b/packages/react-select/src/components/Option.js @@ -85,6 +85,7 @@ const Option = (props: OptionProps) => { return (