Skip to content

Commit

Permalink
fix(select a11y): start writing recipes & fix things found along the way
Browse files Browse the repository at this point in the history
  • Loading branch information
Mohammer5 committed Nov 6, 2024
1 parent 3126037 commit de15432
Show file tree
Hide file tree
Showing 25 changed files with 5,413 additions and 74 deletions.
4 changes: 2 additions & 2 deletions collections/forms/i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2024-10-28T03:31:24.818Z\n"
"PO-Revision-Date: 2024-10-28T03:31:24.818Z\n"
"POT-Creation-Date: 2024-11-04T09:32:49.754Z\n"
"PO-Revision-Date: 2024-11-04T09:32:49.755Z\n"

msgid "Upload file"
msgstr "Upload file"
Expand Down
11 changes: 9 additions & 2 deletions collections/ui/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,8 @@ import { Input } from '@dhis2/ui'
|Name|Type|Default|Required|Description|
|---|---|---|---|---|
|ariaLabel|string|||Add an aria-label attribute to the input element *|
|ariaControls|string|||Add an aria-controls attribute to the input element *|
|ariaHaspopup|string|||Add an aria-haspopup attribute to the input element *|
|autoComplete|string|||The [native `autocomplete` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autocomplete)|
|className|string||||
|dataTest|string|`'dhis2-uicore-input'`|||
Expand Down Expand Up @@ -1858,18 +1860,18 @@ import { SingleSelectA11y } from '@dhis2/ui'
|---|---|---|---|---|
|idPrefix|string||*|necessary for IDs that are required for accessibility *|
|options|arrayOf(custom)||*|An array of options *|
|value|string|`''`||As of now, this component does not support being uncontrolled *|
|onChange|function||*|A callback that will be called with the new value or an empty string *|
|autoFocus|boolean|`false`||Will focus the select initially *|
|className|string|`''`||Additional class names that will be applied to the root element *|
|clearText|custom|`''`||This will allow us to put an aria-label on the clear button *|
|clearable|boolean|`false`||Whether a clear button should be displayed or not *|
|customOption|elementType|||Allows to override what's rendered inside the `button[role="option"]`.<br/>Can be overriden on an individual option basis *|
|customOption|elementType|`undefined`||Allows to override what's rendered inside the `button[role="option"]`.<br/>Can be overriden on an individual option basis *|
|dataTest|string|`'dhis2-singleselecta11y'`||A value for a `data-test` attribute on the root element *|
|dense|boolean|`false`||Renders a select with lower height *|
|disabled|boolean|`false`||Disables all interactions with the select (except focussing) *|
|empty|node|`false`||Text or component to display when there are no options *|
|error|custom|`false`||Applies 'error' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props *|
|filterHelpText|string|`''`||Help text that will be displayed below the input *|
|filterLabel|string|`''`||Value will be used as aria-label attribute on the filter input *|
|filterPlaceholder|string|`''`||Placeholder for the filter input *|
|filterValue|string|`''`||Value of the filter input *|
Expand All @@ -1884,9 +1886,11 @@ import { SingleSelectA11y } from '@dhis2/ui'
|prefix|string|`''`||String that will be displayed before the label of the selected option *|
|tabIndex|string │ number|`'0'`||Standard HTML tab-index attribute that will be put on the combobox's root element *|
|valid|custom|`false`||Applies 'valid' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props *|
|value|string|`''`||As of now, this component does not support being uncontrolled *|
|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.|
|warning|custom|`false`||Applies 'warning' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props *|
|onBlur|function|`() => undefined`||Will be called when the combobox is loses focus *|
|onEndReached|function|||Will be called when the last option is scrolled into the visible area *|
|onFilterChange|function|`() => undefined`||Will be called when the filter value changes *|
|onFocus|function|`() => undefined`||Will be called when the combobox is being focused *|

Expand Down Expand Up @@ -2004,7 +2008,10 @@ import { Menu } from '@dhis2/ui'
|selected|string||||
|onBlur|function||||
|onClose|function||||
|onEndReached|function||||
|onFilterChange|function||||
|onFilterInputKeyDown|function||||
|onSearch|function||||

### SelectorBar

Expand Down
2 changes: 2 additions & 0 deletions components/input/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { Input } from '@dhis2/ui'
|Name|Type|Default|Required|Description|
|---|---|---|---|---|
|ariaLabel|string|||Add an aria-label attribute to the input element *|
|ariaControls|string|||Add an aria-controls attribute to the input element *|
|ariaHaspopup|string|||Add an aria-haspopup attribute to the input element *|
|autoComplete|string|||The [native `autocomplete` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autocomplete)|
|className|string||||
|dataTest|string|`'dhis2-uicore-input'`|||
Expand Down
8 changes: 8 additions & 0 deletions components/input/src/input/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ export class Input extends Component {
const {
role,
ariaLabel,
ariaControls,
ariaHaspopup,
className,
type = 'text',
dense,
Expand All @@ -162,6 +164,8 @@ export class Input extends Component {
<div className={cx('input', className)} data-test={dataTest}>
<input
aria-label={ariaLabel}
aria-controls={ariaControls}
aria-haspopup={ariaHaspopup}
role={role}
id={name}
name={name}
Expand Down Expand Up @@ -208,6 +212,10 @@ export class Input extends Component {
}

Input.propTypes = {
/** Add an aria-controls attribute to the input element **/
ariaControls: PropTypes.string,
/** Add an aria-haspopup attribute to the input element **/
ariaHaspopup: PropTypes.string,
/** Add an aria-label attribute to the input element **/
ariaLabel: PropTypes.string,
/** The [native `autocomplete` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autocomplete) */
Expand Down
6 changes: 4 additions & 2 deletions components/select/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,18 @@ import { SingleSelectA11y } from '@dhis2/ui'
|---|---|---|---|---|
|idPrefix|string||*|necessary for IDs that are required for accessibility *|
|options|arrayOf(custom)||*|An array of options *|
|value|string|`''`||As of now, this component does not support being uncontrolled *|
|onChange|function||*|A callback that will be called with the new value or an empty string *|
|autoFocus|boolean|`false`||Will focus the select initially *|
|className|string|`''`||Additional class names that will be applied to the root element *|
|clearText|custom(function)|`''`||This will allow us to put an aria-label on the clear button *|
|clearable|boolean|`false`||Whether a clear button should be displayed or not *|
|customOption|elementType|||Allows to override what's rendered inside the `button[role="option"]`.<br/>Can be overriden on an individual option basis *|
|customOption|elementType|`undefined`||Allows to override what's rendered inside the `button[role="option"]`.<br/>Can be overriden on an individual option basis *|
|dataTest|string|`'dhis2-singleselecta11y'`||A value for a `data-test` attribute on the root element *|
|dense|boolean|`false`||Renders a select with lower height *|
|disabled|boolean|`false`||Disables all interactions with the select (except focussing) *|
|empty|node|`false`||Text or component to display when there are no options *|
|error|custom|`false`||Applies 'error' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props *|
|filterHelpText|string|`''`||Help text that will be displayed below the input *|
|filterLabel|string|`''`||Value will be used as aria-label attribute on the filter input *|
|filterPlaceholder|string|`''`||Placeholder for the filter input *|
|filterValue|string|`''`||Value of the filter input *|
Expand All @@ -203,9 +203,11 @@ import { SingleSelectA11y } from '@dhis2/ui'
|prefix|string|`''`||String that will be displayed before the label of the selected option *|
|tabIndex|string │ number|`'0'`||Standard HTML tab-index attribute that will be put on the combobox's root element *|
|valid|custom|`false`||Applies 'valid' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props *|
|value|string|`''`||As of now, this component does not support being uncontrolled *|
|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.|
|warning|custom|`false`||Applies 'warning' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props *|
|onBlur|function|`() => undefined`||Will be called when the combobox is loses focus *|
|onEndReached|function|||Will be called when the last option is scrolled into the visible area *|
|onFilterChange|function|`() => undefined`||Will be called when the filter value changes *|
|onFocus|function|`() => undefined`||Will be called when the combobox is being focused *|

Expand Down
16 changes: 16 additions & 0 deletions components/select/src/single-select-a11y/__stories__/Loading.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react'
import { SingleSelectA11y } from '../single-select-a11y.js'
import { options } from './options.js'

export const Loading = () => {
return (
<SingleSelectA11y
loading
idPrefix="a11y"
value=""
valueLabel=""
onChange={() => null}
options={options}
/>
)
}
20 changes: 11 additions & 9 deletions components/select/src/single-select-a11y/menu/menu-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ import React from 'react'
import i18n from '../../locales/index.js'

export function MenuFilter({
dataTest,
idPrefix,
label,
placeholder,
tabIndex,
value,
onChange,
dataTest,
placeholder,
label,
onKeyDown,
}) {
return (
<div data-test={dataTest}>
<Input
dense
tabIndex={tabIndex}
ariaControls={`${idPrefix}-listbox`}
ariaHaspopup="listbox"
ariaLabel={label || i18n.t('Search options')}
Expand All @@ -31,14 +33,13 @@ export function MenuFilter({

<style jsx>{`
div {
position: sticky;
height: 100%;
inset-block-start: 0;
background: ${colors.white};
padding-block-start: ${spacers.dp8};
padding-inline-end: ${spacers.dp8};
padding-block-end: ${spacers.dp4};
padding-inline-start: ${spacers.dp8};
z-index: 1;
// padding-block-start: ${spacers.dp8};
// padding-inline-end: ${spacers.dp8};
// padding-block-end: ${spacers.dp4};
// padding-inline-start: ${spacers.dp8};
}
`}</style>
</div>
Expand All @@ -52,5 +53,6 @@ MenuFilter.propTypes = {
dataTest: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
tabIndex: PropTypes.string,
onKeyDown: PropTypes.func,
}
6 changes: 3 additions & 3 deletions components/select/src/single-select-a11y/menu/menu-loading.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export function MenuLoading({ message }) {

<style jsx>{`
.container {
/* box-sizing: border-box; does not respect padding-block and padding-inline */
width: calc(100% - 48px);
height: calc(100% - 16px);
box-sizing: border-box;
width: 100%;
height: 100%;
display: flex;
gap: ${spacers.dp16};
align-items: center;
Expand Down
89 changes: 51 additions & 38 deletions components/select/src/single-select-a11y/menu/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ export function Menu({
optionUpdateStrategy,
selectRef,
selected,
tabIndex,
onBlur,
onClose,
onEndReached,
onFilterChange,
onFilterInputKeyDown,
onSearch,
}) {
const [menuWidth, setMenuWidth] = useState('auto')
const dataTestPrefix = `${dataTest}-menu`
Expand Down Expand Up @@ -80,48 +80,52 @@ export function Menu({
style={{ width: menuWidth, maxHeight }}
>
{filterable && (
<MenuFilter
idPrefix={idPrefix}
dataTest={`${dataTestPrefix}-filter`}
value={filterValue}
onChange={onFilterChange}
label={filterLabel}
placeholder={filterPlaceholder}
onSearch={onSearch}
onKeyDown={onFilterInputKeyDown}
/>
<div className="filter-container">
<MenuFilter
idPrefix={idPrefix}
dataTest={`${dataTestPrefix}-filter`}
value={filterValue}
label={filterLabel}
placeholder={filterPlaceholder}
tabIndex={tabIndex}
onChange={onFilterChange}
onKeyDown={onFilterInputKeyDown}
/>
</div>
)}

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

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

<div className="listbox-container">
<MenuOptionsList
ref={listBoxRef}
comboBoxId={comboBoxId}
customOption={customOption}
dataTest={`${dataTestPrefix}-list`}
disabled={disabled}
expanded={!hidden}
focussedOptionIndex={focussedOptionIndex}
idPrefix={idPrefix}
labelledBy={labelledBy}
loading={loading}
optionUpdateStrategy={optionUpdateStrategy}
options={options}
selected={selected}
onBlur={onBlur}
onChange={onChange}
onEndReached={onEndReached}
/>
</div>

{loading && (
<div className="menu-loading-container">
<MenuLoading message={loadingText} />
<div className="listbox-wrapper">
<MenuOptionsList
ref={listBoxRef}
comboBoxId={comboBoxId}
customOption={customOption}
dataTest={`${dataTestPrefix}-list`}
disabled={disabled}
expanded={!hidden}
focussedOptionIndex={focussedOptionIndex}
idPrefix={idPrefix}
labelledBy={labelledBy}
loading={loading}
optionUpdateStrategy={optionUpdateStrategy}
options={options}
selected={selected}
onBlur={onBlur}
onChange={onChange}
onEndReached={onEndReached}
/>
</div>
)}

{loading && (
<div className="menu-loading-container">
<MenuLoading message={loadingText} />
</div>
)}
</div>

<style jsx>{`
.menu {
Expand All @@ -138,17 +142,26 @@ export function Menu({
box-sizing: content-box;
}
.filter-container {
}
.listbox-container {
position: relative;
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.listbox-wrapper {
overflow: auto;
height: 100%;
flex-grow: 1;
}
.menu-loading-container {
position: absolute;
left: 0;
bottom: 0;
top: 0;
width: 100%;
height: 100%;
}
Expand Down Expand Up @@ -185,10 +198,10 @@ Menu.propTypes = {
optionUpdateStrategy: PropTypes.oneOf(['off', 'polite', 'assertive']),
selectRef: PropTypes.instanceOf(HTMLElement),
selected: PropTypes.string,
tabIndex: PropTypes.string,
onBlur: PropTypes.func,
onClose: PropTypes.func,
onEndReached: PropTypes.func,
onFilterChange: PropTypes.func,
onFilterInputKeyDown: PropTypes.func,
onSearch: PropTypes.func,
}
7 changes: 4 additions & 3 deletions components/select/src/single-select-a11y/menu/option.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,15 @@ export function Option({
useEffect(() => {
if (onBecameVisible) {
const scrollableContainer = listBoxRef.current.parentNode

const intersectionOptions = {
root: scrollableContainer,
rootMargin: '0px',
threshold: 1,
threshold: VISIBILE_INTERSECTION_RATIO,
}

const intersectionHandler = (entries) => {
entries.forEach(({ intersectionRatio }) => {
entries.forEach((result) => {
const { intersectionRatio } = result
if (intersectionRatio >= VISIBILE_INTERSECTION_RATIO) {
onBecameVisible()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,16 @@ export function SelectedValue({
</div>
)}

<button
<span
className="toggle-icon"
aria-label="Open select"
onClick={(e) => {
e.stopPropagation()
onClick={() => {
comboBoxRef.current.focus()
onClick()
}}
>
<IconChevronDown16 />
</button>
</span>

<style jsx>{`
.selected-option-label {
Expand Down
Loading

0 comments on commit de15432

Please sign in to comment.