improve aria-labelledby for form controls

pull/411/head
Cory LaViska 2021-04-05 17:13:17 -04:00
rodzic 4b4361c514
commit c2314e9871
6 zmienionych plików z 105 dodań i 39 usunięć

Wyświetl plik

@ -15,7 +15,8 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
- Fixed a bug in `sl-details` where `show()` and `hide()` would toggle the control when disabled
- Fixed incorrect event names for `sl-after-show` and `sl-after-hide` in `sl-details`
- Improved a11y for disabled buttons that are rendered as links
- Improved a11y for `sl-button-group`
- Improved a11y for `sl-button-group` by adding the correct `role` attribute
- Improved a11y for `sl-input`, `sl-range`, `sl-select`, and `sl-textarea` so labels and helper text are read properly by screen readers
- Removed `sl-show`, `sl-hide`, `sl-after-show`, `sl-after-hide` events from `sl-color-picker` (the color picker's visibility cannot be controlled programmatically so these shouldn't have been exposed; the dropdown events now bubble up so you can listen for those instead)
- Reworked `sl-button-group` so it doesn't require light DOM styles

Wyświetl plik

@ -4,7 +4,7 @@ import { ifDefined } from 'lit-html/directives/if-defined';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter, watch } from '../../internal/decorators';
import styles from 'sass:./input.scss';
import { renderFormControl } from '../../internal/form-control';
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
import { hasSlot } from '../../internal/slot';
let id = 0;
@ -39,8 +39,8 @@ export default class SlInput extends LitElement {
@query('.input__control') input: HTMLInputElement;
private helpTextId = `input-help-text-${id}`;
private inputId = `input-${++id}`;
private helpTextId = `input-help-text-${id}`;
private labelId = `input-label-${id}`;
@state() private hasFocus = false;
@ -319,8 +319,16 @@ export default class SlInput extends LitElement {
spellcheck=${ifDefined(this.spellcheck)}
pattern=${ifDefined(this.pattern)}
inputmode=${ifDefined(this.inputmode)}
aria-labelledby=${this.labelId}
aria-describedby=${this.helpTextId}
aria-labelledby=${ifDefined(
getLabelledBy({
label: this.label,
labelId: this.labelId,
hasLabelSlot: this.hasLabelSlot,
helpText: this.helpText,
helpTextId: this.helpTextId,
hasHelpTextSlot: this.hasHelpTextSlot
})
)}
aria-invalid=${this.invalid ? 'true' : 'false'}
@change=${this.handleChange}
@input=${this.handleInput}

Wyświetl plik

@ -4,7 +4,7 @@ import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
import { event, EventEmitter, watch } from '../../internal/decorators';
import styles from 'sass:./range.scss';
import { renderFormControl } from '../../internal/form-control';
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
import { hasSlot } from '../../internal/slot';
let id = 0;
@ -27,8 +27,8 @@ export default class SlRange extends LitElement {
@query('.range__control') input: HTMLInputElement;
@query('.range__tooltip') output: HTMLOutputElement;
private helpTextId = `input-help-text-${id}`;
private inputId = `input-${++id}`;
private helpTextId = `input-help-text-${id}`;
private labelId = `input-label-${id}`;
private resizeObserver: ResizeObserver;
@ -194,11 +194,21 @@ export default class SlRange extends LitElement {
type="range"
class="range__control"
name=${this.name}
.value=${this.value}
.value=${this.value + ''}
?disabled=${this.disabled}
min=${ifDefined(this.min)}
max=${ifDefined(this.max)}
step=${ifDefined(this.step)}
aria-labelledby=${ifDefined(
getLabelledBy({
label: this.label,
labelId: this.labelId,
hasLabelSlot: this.hasLabelSlot,
helpText: this.helpText,
helpTextId: this.helpTextId,
hasHelpTextSlot: this.hasHelpTextSlot
})
)}
@input=${this.handleInput.bind(this)}
@focus=${this.handleFocus.bind(this)}
@blur=${this.handleBlur.bind(this)}

Wyświetl plik

@ -1,10 +1,11 @@
import { LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import { customElement, property, query, state } from 'lit/decorators';
import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
import { event, EventEmitter, watch } from '../../internal/decorators';
import styles from 'sass:./select.scss';
import { SlDropdown, SlIconButton, SlMenu, SlMenuItem } from '../../shoelace';
import { renderFormControl } from '../../internal/form-control';
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
import { getTextContent } from '../../internal/slot';
import { hasSlot } from '../../internal/slot';
@ -42,8 +43,8 @@ export default class SlSelect extends LitElement {
@query('.select__hidden-select') input: HTMLInputElement;
@query('.select__menu') menu: SlMenu;
private helpTextId = `select-help-text-${id}`;
private inputId = `select-${++id}`;
private helpTextId = `select-help-text-${id}`;
private labelId = `select-label-${id}`;
private resizeObserver: ResizeObserver;
@ -422,8 +423,16 @@ export default class SlSelect extends LitElement {
id=${this.inputId}
class="select__box"
role="combobox"
aria-labelledby=${this.labelId}
aria-describedby=${this.helpTextId}
aria-labelledby=${ifDefined(
getLabelledBy({
label: this.label,
labelId: this.labelId,
hasLabelSlot: this.hasLabelSlot,
helpText: this.helpText,
helpTextId: this.helpTextId,
hasHelpTextSlot: this.hasHelpTextSlot
})
)}
aria-haspopup="true"
aria-expanded=${this.isOpen ? 'true' : 'false'}
tabindex=${this.disabled ? '-1' : '0'}

Wyświetl plik

@ -4,7 +4,7 @@ import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
import { event, EventEmitter, watch } from '../../internal/decorators';
import styles from 'sass:./textarea.scss';
import { renderFormControl } from '../../internal/form-control';
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
import { hasSlot } from '../../internal/slot';
let id = 0;
@ -28,8 +28,8 @@ export default class SlTextarea extends LitElement {
@query('.textarea__control') input: HTMLTextAreaElement;
private helpTextId = `textarea-help-text-${id}`;
private inputId = `textarea-${++id}`;
private helpTextId = `textarea-help-text-${id}`;
private labelId = `textarea-label-${id}`;
private resizeObserver: ResizeObserver;
@ -298,7 +298,16 @@ export default class SlTextarea extends LitElement {
?autofocus=${this.autofocus}
spellcheck=${ifDefined(this.spellcheck)}
inputmode=${ifDefined(this.inputmode)}
aria-labelledby=${this.labelId}
aria-labelledby=${ifDefined(
getLabelledBy({
label: this.label,
labelId: this.labelId,
hasLabelSlot: this.hasLabelSlot,
helpText: this.helpText,
helpTextId: this.helpTextId,
hasHelpTextSlot: this.hasHelpTextSlot
})
)}
@change=${this.handleChange.bind(this)}
@input=${this.handleInput.bind(this)}
@focus=${this.handleFocus.bind(this)}

Wyświetl plik

@ -1,36 +1,38 @@
import { html, TemplateResult } from 'lit';
import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
export interface FormControlProps {
/** The input id, used to map the input to the label */
inputId: string;
export const renderFormControl = (
props: {
/** The input id, used to map the input to the label */
inputId: string;
/** The size of the form control */
size: 'small' | 'medium' | 'large';
/** The size of the form control */
size: 'small' | 'medium' | 'large';
/** The label id, used to map the label to the input */
labelId?: string;
/** The label id, used to map the label to the input */
labelId?: string;
/** The label text (if the label slot isn't used) */
label?: string;
/** The label text (if the label slot isn't used) */
label?: string;
/** Whether or not a label slot has been provided. */
hasLabelSlot?: boolean;
/** Whether or not a label slot has been provided. */
hasLabelSlot?: boolean;
/** The help text id, used to map the input to the help text */
helpTextId?: string;
/** The help text id, used to map the input to the help text */
helpTextId?: string;
/** The help text (if the help-text slot isn't used) */
helpText?: string;
/** The help text (if the help-text slot isn't used) */
helpText?: string;
/** Whether or not a help text slot has been provided. */
hasHelpTextSlot?: boolean;
/** Whether or not a help text slot has been provided. */
hasHelpTextSlot?: boolean;
/** A function that gets called when the label is clicked. */
onLabelClick?: (event: MouseEvent) => void;
}
export const renderFormControl = (props: FormControlProps, input: TemplateResult) => {
/** A function that gets called when the label is clicked. */
onLabelClick?: (event: MouseEvent) => void;
},
input: TemplateResult
) => {
const hasLabel = props.label ? true : !!props.hasLabelSlot;
const hasHelpText = props.helpText ? true : !!props.hasHelpTextSlot;
@ -48,7 +50,7 @@ export const renderFormControl = (props: FormControlProps, input: TemplateResult
>
<label
part="label"
id=${props.labelId}
id=${ifDefined(props.labelId)}
class="form-control__label"
for=${props.inputId}
aria-hidden=${hasLabel ? 'false' : 'true'}
@ -61,7 +63,7 @@ export const renderFormControl = (props: FormControlProps, input: TemplateResult
<div
part="help-text"
id=${props.helpTextId}
id=${ifDefined(props.helpTextId)}
class="form-control__help-text"
aria-hidden=${hasHelpText ? 'false' : 'true'}
>
@ -70,3 +72,30 @@ export const renderFormControl = (props: FormControlProps, input: TemplateResult
</div>
`;
};
export function getLabelledBy(props: {
/** The label id, used to map the label to the input */
labelId: string;
/** The label text (if the label slot isn't used) */
label: string;
/** Whether or not a label slot has been provided. */
hasLabelSlot: boolean;
/** The help text id, used to map the input to the help text */
helpTextId: string;
/** The help text (if the help-text slot isn't used) */
helpText: string;
/** Whether or not a help text slot has been provided. */
hasHelpTextSlot: boolean;
}) {
const labelledBy = [
props.label || props.hasLabelSlot ? props.labelId : '',
props.helpText || props.hasHelpTextSlot ? props.helpTextId : ''
].filter(val => val);
return labelledBy.join(' ') || undefined;
}