kopia lustrzana https://github.com/shoelace-style/shoelace
improve aria-labelledby for form controls
rodzic
4b4361c514
commit
c2314e9871
|
@ -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
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue