Skip to content

Commit de15432

Browse files
committed
fix(select a11y): start writing recipes & fix things found along the way
1 parent 3126037 commit de15432

25 files changed

+5413
-74
lines changed

collections/forms/i18n/en.pot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ msgstr ""
55
"Content-Type: text/plain; charset=utf-8\n"
66
"Content-Transfer-Encoding: 8bit\n"
77
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
8-
"POT-Creation-Date: 2024-10-28T03:31:24.818Z\n"
9-
"PO-Revision-Date: 2024-10-28T03:31:24.818Z\n"
8+
"POT-Creation-Date: 2024-11-04T09:32:49.754Z\n"
9+
"PO-Revision-Date: 2024-11-04T09:32:49.755Z\n"
1010

1111
msgid "Upload file"
1212
msgstr "Upload file"

collections/ui/API.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,8 @@ import { Input } from '@dhis2/ui'
761761
|Name|Type|Default|Required|Description|
762762
|---|---|---|---|---|
763763
|ariaLabel|string|||Add an aria-label attribute to the input element *|
764+
|ariaControls|string|||Add an aria-controls attribute to the input element *|
765+
|ariaHaspopup|string|||Add an aria-haspopup attribute to the input element *|
764766
|autoComplete|string|||The [native `autocomplete` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autocomplete)|
765767
|className|string||||
766768
|dataTest|string|`'dhis2-uicore-input'`|||
@@ -1858,18 +1860,18 @@ import { SingleSelectA11y } from '@dhis2/ui'
18581860
|---|---|---|---|---|
18591861
|idPrefix|string||*|necessary for IDs that are required for accessibility *|
18601862
|options|arrayOf(custom)||*|An array of options *|
1861-
|value|string|`''`||As of now, this component does not support being uncontrolled *|
18621863
|onChange|function||*|A callback that will be called with the new value or an empty string *|
18631864
|autoFocus|boolean|`false`||Will focus the select initially *|
18641865
|className|string|`''`||Additional class names that will be applied to the root element *|
18651866
|clearText|custom|`''`||This will allow us to put an aria-label on the clear button *|
18661867
|clearable|boolean|`false`||Whether a clear button should be displayed or not *|
1867-
|customOption|elementType|||Allows to override what's rendered inside the `button[role="option"]`.<br/>Can be overriden on an individual option basis *|
1868+
|customOption|elementType|`undefined`||Allows to override what's rendered inside the `button[role="option"]`.<br/>Can be overriden on an individual option basis *|
18681869
|dataTest|string|`'dhis2-singleselecta11y'`||A value for a `data-test` attribute on the root element *|
18691870
|dense|boolean|`false`||Renders a select with lower height *|
18701871
|disabled|boolean|`false`||Disables all interactions with the select (except focussing) *|
18711872
|empty|node|`false`||Text or component to display when there are no options *|
18721873
|error|custom|`false`||Applies 'error' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props *|
1874+
|filterHelpText|string|`''`||Help text that will be displayed below the input *|
18731875
|filterLabel|string|`''`||Value will be used as aria-label attribute on the filter input *|
18741876
|filterPlaceholder|string|`''`||Placeholder for the filter input *|
18751877
|filterValue|string|`''`||Value of the filter input *|
@@ -1884,9 +1886,11 @@ import { SingleSelectA11y } from '@dhis2/ui'
18841886
|prefix|string|`''`||String that will be displayed before the label of the selected option *|
18851887
|tabIndex|string │ number|`'0'`||Standard HTML tab-index attribute that will be put on the combobox's root element *|
18861888
|valid|custom|`false`||Applies 'valid' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props *|
1889+
|value|string|`''`||As of now, this component does not support being uncontrolled *|
18871890
|valueLabel|custom|`''`||When the option is not in the options list (e.g. not loaded or list is<br/>filtered), but a selected value needs to be displayed, then this prop can<br/>be used to supply the text to be shown.|
18881891
|warning|custom|`false`||Applies 'warning' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props *|
18891892
|onBlur|function|`() => undefined`||Will be called when the combobox is loses focus *|
1893+
|onEndReached|function|||Will be called when the last option is scrolled into the visible area *|
18901894
|onFilterChange|function|`() => undefined`||Will be called when the filter value changes *|
18911895
|onFocus|function|`() => undefined`||Will be called when the combobox is being focused *|
18921896

@@ -2004,7 +2008,10 @@ import { Menu } from '@dhis2/ui'
20042008
|selected|string||||
20052009
|onBlur|function||||
20062010
|onClose|function||||
2011+
|onEndReached|function||||
20072012
|onFilterChange|function||||
2013+
|onFilterInputKeyDown|function||||
2014+
|onSearch|function||||
20082015

20092016
### SelectorBar
20102017

components/input/API.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { Input } from '@dhis2/ui'
1515
|Name|Type|Default|Required|Description|
1616
|---|---|---|---|---|
1717
|ariaLabel|string|||Add an aria-label attribute to the input element *|
18+
|ariaControls|string|||Add an aria-controls attribute to the input element *|
19+
|ariaHaspopup|string|||Add an aria-haspopup attribute to the input element *|
1820
|autoComplete|string|||The [native `autocomplete` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autocomplete)|
1921
|className|string||||
2022
|dataTest|string|`'dhis2-uicore-input'`|||

components/input/src/input/input.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ export class Input extends Component {
138138
const {
139139
role,
140140
ariaLabel,
141+
ariaControls,
142+
ariaHaspopup,
141143
className,
142144
type = 'text',
143145
dense,
@@ -162,6 +164,8 @@ export class Input extends Component {
162164
<div className={cx('input', className)} data-test={dataTest}>
163165
<input
164166
aria-label={ariaLabel}
167+
aria-controls={ariaControls}
168+
aria-haspopup={ariaHaspopup}
165169
role={role}
166170
id={name}
167171
name={name}
@@ -208,6 +212,10 @@ export class Input extends Component {
208212
}
209213

210214
Input.propTypes = {
215+
/** Add an aria-controls attribute to the input element **/
216+
ariaControls: PropTypes.string,
217+
/** Add an aria-haspopup attribute to the input element **/
218+
ariaHaspopup: PropTypes.string,
211219
/** Add an aria-label attribute to the input element **/
212220
ariaLabel: PropTypes.string,
213221
/** The [native `autocomplete` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autocomplete) */

components/select/API.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,18 +177,18 @@ import { SingleSelectA11y } from '@dhis2/ui'
177177
|---|---|---|---|---|
178178
|idPrefix|string||*|necessary for IDs that are required for accessibility *|
179179
|options|arrayOf(custom)||*|An array of options *|
180-
|value|string|`''`||As of now, this component does not support being uncontrolled *|
181180
|onChange|function||*|A callback that will be called with the new value or an empty string *|
182181
|autoFocus|boolean|`false`||Will focus the select initially *|
183182
|className|string|`''`||Additional class names that will be applied to the root element *|
184183
|clearText|custom(function)|`''`||This will allow us to put an aria-label on the clear button *|
185184
|clearable|boolean|`false`||Whether a clear button should be displayed or not *|
186-
|customOption|elementType|||Allows to override what's rendered inside the `button[role="option"]`.<br/>Can be overriden on an individual option basis *|
185+
|customOption|elementType|`undefined`||Allows to override what's rendered inside the `button[role="option"]`.<br/>Can be overriden on an individual option basis *|
187186
|dataTest|string|`'dhis2-singleselecta11y'`||A value for a `data-test` attribute on the root element *|
188187
|dense|boolean|`false`||Renders a select with lower height *|
189188
|disabled|boolean|`false`||Disables all interactions with the select (except focussing) *|
190189
|empty|node|`false`||Text or component to display when there are no options *|
191190
|error|custom|`false`||Applies 'error' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props *|
191+
|filterHelpText|string|`''`||Help text that will be displayed below the input *|
192192
|filterLabel|string|`''`||Value will be used as aria-label attribute on the filter input *|
193193
|filterPlaceholder|string|`''`||Placeholder for the filter input *|
194194
|filterValue|string|`''`||Value of the filter input *|
@@ -203,9 +203,11 @@ import { SingleSelectA11y } from '@dhis2/ui'
203203
|prefix|string|`''`||String that will be displayed before the label of the selected option *|
204204
|tabIndex|string │ number|`'0'`||Standard HTML tab-index attribute that will be put on the combobox's root element *|
205205
|valid|custom|`false`||Applies 'valid' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props *|
206+
|value|string|`''`||As of now, this component does not support being uncontrolled *|
206207
|valueLabel|custom(function)|`''`||When the option is not in the options list (e.g. not loaded or list is<br/>filtered), but a selected value needs to be displayed, then this prop can<br/>be used to supply the text to be shown.|
207208
|warning|custom|`false`||Applies 'warning' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props *|
208209
|onBlur|function|`() => undefined`||Will be called when the combobox is loses focus *|
210+
|onEndReached|function|||Will be called when the last option is scrolled into the visible area *|
209211
|onFilterChange|function|`() => undefined`||Will be called when the filter value changes *|
210212
|onFocus|function|`() => undefined`||Will be called when the combobox is being focused *|
211213

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react'
2+
import { SingleSelectA11y } from '../single-select-a11y.js'
3+
import { options } from './options.js'
4+
5+
export const Loading = () => {
6+
return (
7+
<SingleSelectA11y
8+
loading
9+
idPrefix="a11y"
10+
value=""
11+
valueLabel=""
12+
onChange={() => null}
13+
options={options}
14+
/>
15+
)
16+
}

components/select/src/single-select-a11y/menu/menu-filter.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@ import React from 'react'
55
import i18n from '../../locales/index.js'
66

77
export function MenuFilter({
8+
dataTest,
89
idPrefix,
10+
label,
11+
placeholder,
12+
tabIndex,
913
value,
1014
onChange,
11-
dataTest,
12-
placeholder,
13-
label,
1415
onKeyDown,
1516
}) {
1617
return (
1718
<div data-test={dataTest}>
1819
<Input
1920
dense
21+
tabIndex={tabIndex}
2022
ariaControls={`${idPrefix}-listbox`}
2123
ariaHaspopup="listbox"
2224
ariaLabel={label || i18n.t('Search options')}
@@ -31,14 +33,13 @@ export function MenuFilter({
3133

3234
<style jsx>{`
3335
div {
34-
position: sticky;
36+
height: 100%;
3537
inset-block-start: 0;
3638
background: ${colors.white};
37-
padding-block-start: ${spacers.dp8};
38-
padding-inline-end: ${spacers.dp8};
39-
padding-block-end: ${spacers.dp4};
40-
padding-inline-start: ${spacers.dp8};
41-
z-index: 1;
39+
// padding-block-start: ${spacers.dp8};
40+
// padding-inline-end: ${spacers.dp8};
41+
// padding-block-end: ${spacers.dp4};
42+
// padding-inline-start: ${spacers.dp8};
4243
}
4344
`}</style>
4445
</div>
@@ -52,5 +53,6 @@ MenuFilter.propTypes = {
5253
dataTest: PropTypes.string,
5354
label: PropTypes.string,
5455
placeholder: PropTypes.string,
56+
tabIndex: PropTypes.string,
5557
onKeyDown: PropTypes.func,
5658
}

components/select/src/single-select-a11y/menu/menu-loading.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ export function MenuLoading({ message }) {
1414

1515
<style jsx>{`
1616
.container {
17-
/* box-sizing: border-box; does not respect padding-block and padding-inline */
18-
width: calc(100% - 48px);
19-
height: calc(100% - 16px);
17+
box-sizing: border-box;
18+
width: 100%;
19+
height: 100%;
2020
display: flex;
2121
gap: ${spacers.dp16};
2222
align-items: center;

components/select/src/single-select-a11y/menu/menu.js

Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ export function Menu({
3535
optionUpdateStrategy,
3636
selectRef,
3737
selected,
38+
tabIndex,
3839
onBlur,
3940
onClose,
4041
onEndReached,
4142
onFilterChange,
4243
onFilterInputKeyDown,
43-
onSearch,
4444
}) {
4545
const [menuWidth, setMenuWidth] = useState('auto')
4646
const dataTestPrefix = `${dataTest}-menu`
@@ -80,48 +80,52 @@ export function Menu({
8080
style={{ width: menuWidth, maxHeight }}
8181
>
8282
{filterable && (
83-
<MenuFilter
84-
idPrefix={idPrefix}
85-
dataTest={`${dataTestPrefix}-filter`}
86-
value={filterValue}
87-
onChange={onFilterChange}
88-
label={filterLabel}
89-
placeholder={filterPlaceholder}
90-
onSearch={onSearch}
91-
onKeyDown={onFilterInputKeyDown}
92-
/>
83+
<div className="filter-container">
84+
<MenuFilter
85+
idPrefix={idPrefix}
86+
dataTest={`${dataTestPrefix}-filter`}
87+
value={filterValue}
88+
label={filterLabel}
89+
placeholder={filterPlaceholder}
90+
tabIndex={tabIndex}
91+
onChange={onFilterChange}
92+
onKeyDown={onFilterInputKeyDown}
93+
/>
94+
</div>
9395
)}
9496

9597
{isEmpty && <Empty>{empty}</Empty>}
9698

9799
{hasNoFilterMatch && <NoMatch>{noMatchText}</NoMatch>}
98100

99101
<div className="listbox-container">
100-
<MenuOptionsList
101-
ref={listBoxRef}
102-
comboBoxId={comboBoxId}
103-
customOption={customOption}
104-
dataTest={`${dataTestPrefix}-list`}
105-
disabled={disabled}
106-
expanded={!hidden}
107-
focussedOptionIndex={focussedOptionIndex}
108-
idPrefix={idPrefix}
109-
labelledBy={labelledBy}
110-
loading={loading}
111-
optionUpdateStrategy={optionUpdateStrategy}
112-
options={options}
113-
selected={selected}
114-
onBlur={onBlur}
115-
onChange={onChange}
116-
onEndReached={onEndReached}
117-
/>
118-
</div>
119-
120-
{loading && (
121-
<div className="menu-loading-container">
122-
<MenuLoading message={loadingText} />
102+
<div className="listbox-wrapper">
103+
<MenuOptionsList
104+
ref={listBoxRef}
105+
comboBoxId={comboBoxId}
106+
customOption={customOption}
107+
dataTest={`${dataTestPrefix}-list`}
108+
disabled={disabled}
109+
expanded={!hidden}
110+
focussedOptionIndex={focussedOptionIndex}
111+
idPrefix={idPrefix}
112+
labelledBy={labelledBy}
113+
loading={loading}
114+
optionUpdateStrategy={optionUpdateStrategy}
115+
options={options}
116+
selected={selected}
117+
onBlur={onBlur}
118+
onChange={onChange}
119+
onEndReached={onEndReached}
120+
/>
123121
</div>
124-
)}
122+
123+
{loading && (
124+
<div className="menu-loading-container">
125+
<MenuLoading message={loadingText} />
126+
</div>
127+
)}
128+
</div>
125129

126130
<style jsx>{`
127131
.menu {
@@ -138,17 +142,26 @@ export function Menu({
138142
box-sizing: content-box;
139143
}
140144
145+
.filter-container {
146+
}
147+
141148
.listbox-container {
142149
position: relative;
150+
flex-grow: 1;
151+
display: flex;
152+
flex-direction: column;
153+
overflow: hidden;
154+
}
155+
156+
.listbox-wrapper {
143157
overflow: auto;
144-
height: 100%;
145158
flex-grow: 1;
146159
}
147160
148161
.menu-loading-container {
149162
position: absolute;
150163
left: 0;
151-
bottom: 0;
164+
top: 0;
152165
width: 100%;
153166
height: 100%;
154167
}
@@ -185,10 +198,10 @@ Menu.propTypes = {
185198
optionUpdateStrategy: PropTypes.oneOf(['off', 'polite', 'assertive']),
186199
selectRef: PropTypes.instanceOf(HTMLElement),
187200
selected: PropTypes.string,
201+
tabIndex: PropTypes.string,
188202
onBlur: PropTypes.func,
189203
onClose: PropTypes.func,
190204
onEndReached: PropTypes.func,
191205
onFilterChange: PropTypes.func,
192206
onFilterInputKeyDown: PropTypes.func,
193-
onSearch: PropTypes.func,
194207
}

components/select/src/single-select-a11y/menu/option.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,15 @@ export function Option({
7777
useEffect(() => {
7878
if (onBecameVisible) {
7979
const scrollableContainer = listBoxRef.current.parentNode
80+
8081
const intersectionOptions = {
8182
root: scrollableContainer,
82-
rootMargin: '0px',
83-
threshold: 1,
83+
threshold: VISIBILE_INTERSECTION_RATIO,
8484
}
8585

8686
const intersectionHandler = (entries) => {
87-
entries.forEach(({ intersectionRatio }) => {
87+
entries.forEach((result) => {
88+
const { intersectionRatio } = result
8889
if (intersectionRatio >= VISIBILE_INTERSECTION_RATIO) {
8990
onBecameVisible()
9091
}

0 commit comments

Comments
 (0)