diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 33dd532d..da822d4f 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -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 diff --git a/src/components/input/input.ts b/src/components/input/input.ts index eaa72705..eff58a12 100644 --- a/src/components/input/input.ts +++ b/src/components/input/input.ts @@ -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} diff --git a/src/components/range/range.ts b/src/components/range/range.ts index 86302906..1127f937 100644 --- a/src/components/range/range.ts +++ b/src/components/range/range.ts @@ -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)} diff --git a/src/components/select/select.ts b/src/components/select/select.ts index 9f56f415..29a13401 100644 --- a/src/components/select/select.ts +++ b/src/components/select/select.ts @@ -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'} diff --git a/src/components/textarea/textarea.ts b/src/components/textarea/textarea.ts index 6ad200ee..b0aee806 100644 --- a/src/components/textarea/textarea.ts +++ b/src/components/textarea/textarea.ts @@ -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)} diff --git a/src/internal/form-control.ts b/src/internal/form-control.ts index 5ed34374..8a90f7e3 100644 --- a/src/internal/form-control.ts +++ b/src/internal/form-control.ts @@ -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 >