Skip to content

Commit

Permalink
a11y for MultiStateCheckbox
Browse files Browse the repository at this point in the history
  • Loading branch information
cagataycivici committed Apr 25, 2022
1 parent 36c6f90 commit 7357148
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 57 deletions.
28 changes: 8 additions & 20 deletions api-generator/components/multistatecheckbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ const MultiStateCheckboxProps = [
default: 'null',
description: 'Unique identifier of the element.'
},
{
name: 'inputId',
type: 'string',
default: 'null',
description: 'Unique identifier of the native checkbox element.'
},
{
name: 'value',
type: 'any',
Expand All @@ -29,17 +23,23 @@ const MultiStateCheckboxProps = [
default: 'null',
description: 'Property name to use as the value of an option, defaults to the option itself when not defined.'
},
{
name: 'optionLabel',
type: 'string',
default: 'null',
description: 'Property name to refer to the option label, used by screen readers only. Defaults to optionValue.'
},
{
name: 'iconTemplate',
type: 'any',
default: 'null',
description: 'Template of icon for the selected option.'
},
{
name: 'name',
name: 'dataKey',
type: 'string',
default: 'null',
description: 'Name of the checkbox element .'
description: 'A property to uniquely match the value in options for better performance.'
},
{
name: 'style',
Expand Down Expand Up @@ -82,18 +82,6 @@ const MultiStateCheckboxProps = [
type: 'object',
default: 'null',
description: 'Configuration of the tooltip, refer to the tooltip documentation for more information.'
},
{
name: 'ariaLabelledBy',
type: 'string',
default: 'null',
description: 'Establishes relationships between the component and label(s) where its value should be one or more element IDs.'
},
{
name: 'dataKey',
type: 'string',
default: 'null',
description: 'A property to uniquely match the value in options for better performance.'
}
];

Expand Down
69 changes: 49 additions & 20 deletions components/doc/multistatecheckbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,6 @@ import { MultiStateCheckbox } from 'primereact/multistatecheckbox';
<td>null</td>
<td>Unique identifier of the element.</td>
</tr>
<tr>
<td>inputId</td>
<td>string</td>
<td>null</td>
<td>Unique identifier of the native checkbox element.</td>
</tr>
<tr>
<td>value</td>
<td>any</td>
Expand All @@ -234,17 +228,23 @@ import { MultiStateCheckbox } from 'primereact/multistatecheckbox';
<td>null</td>
<td>Property name to use as the value of an option, defaults to the option itself when not defined.</td>
</tr>
<tr>
<td>optionLabel</td>
<td>string</td>
<td>null</td>
<td>Property name to refer to the option label, used by screen readers only. Defaults to optionValue.</td>
</tr>
<tr>
<td>iconTemplate</td>
<td>any</td>
<td>null</td>
<td>Template of icon for the selected option.</td>
</tr>
<tr>
<td>name</td>
<td>dataKey</td>
<td>string</td>
<td>null</td>
<td>Name of the checkbox element .</td>
<td>A property to uniquely match the value in options for better performance.</td>
</tr>
<tr>
<td>style</td>
Expand All @@ -270,6 +270,12 @@ import { MultiStateCheckbox } from 'primereact/multistatecheckbox';
<td>false</td>
<td>When present, it specifies that the element value cannot be altered.</td>
</tr>
<tr>
<td>tabIndex</td>
<td>number</td>
<td>null</td>
<td>Index of the element in tabbing order.</td>
</tr>
<tr>
<td>empty</td>
<td>boolean</td>
Expand All @@ -288,18 +294,6 @@ import { MultiStateCheckbox } from 'primereact/multistatecheckbox';
<td>null</td>
<td>Configuration of the tooltip, refer to the tooltip documentation for more information.</td>
</tr>
<tr>
<td>ariaLabelledBy</td>
<td>string</td>
<td>null</td>
<td>Establishes relationships between the component and label(s) where its value should be one or more element IDs.</td>
</tr>
<tr>
<td>dataKey</td>
<td>string</td>
<td>null</td>
<td>A property to uniquely match the value in options for better performance.</td>
</tr>
</tbody>
</table>
</div>
Expand Down Expand Up @@ -357,6 +351,41 @@ import { MultiStateCheckbox } from 'primereact/multistatecheckbox';
</table>
</div>

<h5>Accessibility</h5>
<h6>Screen Reader</h6>
<p>MultiStateCheckbox component uses an element with <i>checkbox</i> role. Value to describe the component can either be provided with <i>aria-labelledby</i> or <i>aria-label</i> props. Component adds an element with
<i>aria-live</i> attribute that is only visible to screen readers to read the value displayed. Values to read are defined with the <i>optionLabel</i> property that defaults to <i>optionValue</i> if not defined. Unchecked state label on the other hand is
retrieved from <i>nullLabel</i> key of the <i>aria</i> property from the <Link href="/theming">locale</Link> API. This is an example of a custom accessibility implementation as there is no one to one mapping between the component design and the WCAG specification.</p>
<CodeHighlight>
{`
<span id="chkbox1">Access Type</span>
<MultiStateCheckbox aria-labelledby="chkbox1" />
<TriStateCheckbox aria-label="Access Type" />
`}
</CodeHighlight>
<h6>Keyboard Support</h6>
<div className="doc-tablewrapper">
<table className="doc-table">
<thead>
<tr>
<th>Key</th>
<th>Function</th>
</tr>
</thead>
<tbody>
<tr>
<td><i>tab</i></td>
<td>Moves focus to the checkbox.</td>
</tr>
<tr>
<td><i>space</i></td>
<td>Toggles between the values.</td>
</tr>
</tbody>
</table>
</div>

<h5>Dependencies</h5>
<p>None.</p>
</TabPanel>
Expand Down
37 changes: 22 additions & 15 deletions components/lib/multistatecheckbox/MultiStateCheckbox.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import * as React from 'react';
import { useMountEffect } from '../hooks/Hooks';
import { Tooltip } from '../tooltip/Tooltip';
import { ariaLabel } from '../api/Api';
import { classNames, ObjectUtils } from '../utils/Utils';

export const MultiStateCheckbox = React.memo(React.forwardRef((props, ref) => {
const [focusedState, setFocusedState] = React.useState(false);
const elementRef = React.useRef(null);
const inputRef = React.useRef(props.inputRef);
const equalityKey = props.optionValue ? null : props.dataKey;

const onClick = (event) => {
if (!props.disabled && !props.readOnly) {
toggle(event);
inputRef.current.focus();
}
}

const getOptionValue = (option) => {
return props.optionValue ? ObjectUtils.resolveFieldData(option, props.optionValue) : option;
}

const getOptionAriaLabel = (option) => {
const ariaField = props.optionLabel || props.optionValue;
return ariaField ? ObjectUtils.resolveFieldData(option, ariaField) : option;
}

const findNextOption = () => {
if (props.options) {
return selectedOptionIndex === props.options.length - 1 ? (props.empty ? null : props.options[0]) : props.options[selectedOptionIndex + 1];
Expand Down Expand Up @@ -54,6 +58,13 @@ export const MultiStateCheckbox = React.memo(React.forwardRef((props, ref) => {
setFocusedState(false);
}

const onKeyDown = (e) => {
if (e.keyCode === 32) {
toggle(e);
e.preventDefault();
}
}

const getSelectedOptionMap = () => {
let option, index;

Expand All @@ -65,10 +76,6 @@ export const MultiStateCheckbox = React.memo(React.forwardRef((props, ref) => {
return { option, index };
}

React.useEffect(() => {
ObjectUtils.combinedRefs(inputRef, props.inputRef);
}, [inputRef, props.inputRef]);

useMountEffect(() => {
if (!props.empty && props.value === null) {
toggle();
Expand Down Expand Up @@ -107,17 +114,17 @@ export const MultiStateCheckbox = React.memo(React.forwardRef((props, ref) => {
'p-focus': focusedState
}, selectedOption && selectedOption.className);
const icon = createIcon();
console.log(!!selectedOption);
const ariaValueLabel = !!selectedOption ? getOptionAriaLabel(selectedOption) : ariaLabel('nullLabel');

return (
<>
<div ref={elementRef} id={props.id} className={className} style={props.style} {...otherProps} onClick={onClick}>
<div className="p-hidden-accessible">
<input ref={inputRef} type="checkbox" aria-labelledby={props.ariaLabelledBy} id={props.inputId} name={props.name}
onFocus={onFocus} onBlur={onBlur} disabled={props.disabled} readOnly={props.readOnly} defaultChecked={!!selectedOption} />
</div>
<div className={boxClassName} role="checkbox" aria-checked={!!selectedOption} style={selectedOption && selectedOption.style}>
<div className={boxClassName} style={selectedOption && selectedOption.style} tabIndex={props.tabIndex} onFocus={onFocus} onBlur={onBlur} onKeyDown={onKeyDown}
role="checkbox" aria-labelledby={props['aria-labelledby']} aria-label={props['aria-label']}>
{icon}
</div>
{focusedState && <span className="p-sr-only" aria-live="polite">{ariaValueLabel}</span>}
</div>
{hasTooltip && <Tooltip target={elementRef} content={props.tooltip} {...props.tooltipOptions} />}
</>
Expand All @@ -128,21 +135,21 @@ MultiStateCheckbox.displayName = 'MultiStateCheckbox';
MultiStateCheckbox.defaultProps = {
__TYPE: 'MultiStateCheckbox',
id: null,
inputRef: null,
inputId: null,
value: null,
options: null,
optionValue: null,
optionLabel: null,
iconTemplate: null,
dataKey: null,
name: null,
style: null,
className: null,
disabled: false,
readOnly: false,
empty: true,
tabIndex: "0",
'aria-label': null,
'aria-labelledby': null,
tooltip: null,
tooltipOptions: null,
ariaLabelledBy: null,
onChange: null
}
2 changes: 1 addition & 1 deletion pages/multistatecheckbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const MultiStateCheckboxDemo = () => {
<div className="content-section implementation">
<div className="card">
<div className="field-checkbox m-0">
<MultiStateCheckbox value={value} options={options} optionValue="value" onChange={(e) => setValue(e.value)} />
<MultiStateCheckbox value={value} options={options} optionValue="value" onChange={(e) => setValue(e.value)} aria-label="Access Type" />
<label>{value}</label>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion pages/tristatecheckbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const TriStateCheckboxDemo = () => {
<div className="content-section implementation">
<div className="card">
<div className="field-checkbox m-0">
<TriStateCheckbox value={value} onChange={(e) => setValue(e.value)} aria-label="Confirmation" />
<TriStateCheckbox value={value} onChange={(e) => setValue(e.value)} aria-label="Terms Accepted" />
<label>{String(value)}</label>
</div>
</div>
Expand Down

0 comments on commit 7357148

Please sign in to comment.