diff --git a/core/src/components/select/select.scss b/core/src/components/select/select.scss index f0f4e5a877..dd31ef52fa 100644 --- a/core/src/components/select/select.scss +++ b/core/src/components/select/select.scss @@ -50,8 +50,18 @@ opacity: var(--placeholder-opacity); } -button { +label { @include input-cover(); + + display: flex; + + align-items: center; + + opacity: 0; +} + +button { + @include visually-hidden(); } .select-icon { diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 5bb8e2945b..1eb6dd4652 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Meth import { getIonMode } from '../../global/ionic-global'; import { ActionSheetButton, ActionSheetOptions, AlertInput, AlertOptions, CssClassMap, OverlaySelect, PopoverOptions, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, StyleEventDetail } from '../../interface'; -import { findItemLabel, renderHiddenInput } from '../../utils/helpers'; +import { findItemLabel, getAriaLabel, renderHiddenInput } from '../../utils/helpers'; import { actionSheetController, alertController, popoverController } from '../../utils/overlays'; import { hostContext } from '../../utils/theme'; import { watchForOptions } from '../../utils/watch-options'; @@ -29,7 +29,7 @@ export class Select implements ComponentInterface { private inputId = `ion-sel-${selectIds++}`; private overlay?: OverlaySelect; private didInit = false; - private buttonEl?: HTMLButtonElement; + private focusEl?: HTMLButtonElement; private mutationO?: MutationObserver; @Element() el!: HTMLIonSelectElement; @@ -403,8 +403,8 @@ export class Select implements ComponentInterface { } private setFocus() { - if (this.buttonEl) { - this.buttonEl.focus(); + if (this.focusEl) { + this.focusEl.focus(); } } @@ -420,6 +420,9 @@ export class Select implements ComponentInterface { } private onClick = (ev: UIEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + this.setFocus(); this.open(ev); } @@ -432,23 +435,21 @@ export class Select implements ComponentInterface { } render() { - const { placeholder, name, disabled, isExpanded, value, el } = this; + const { disabled, el, inputId, isExpanded, name, placeholder, value } = this; const mode = getIonMode(this); - const labelId = this.inputId + '-lbl'; - const label = findItemLabel(el); - if (label) { - label.id = labelId; - } + const { labelText, labelId } = getAriaLabel(el, inputId); + + renderHiddenInput(true, el, name, parseValue(value), disabled); + + const displayValue = this.getText(); let addPlaceholderClass = false; - let selectText = this.getText(); + let selectText = displayValue; if (selectText === '' && placeholder != null) { selectText = placeholder; addPlaceholderClass = true; } - renderHiddenInput(true, el, name, parseValue(value), disabled); - const selectTextClasses: CssClassMap = { 'select-text': true, 'select-placeholder': addPlaceholderClass @@ -456,14 +457,20 @@ export class Select implements ComponentInterface { const textPart = addPlaceholderClass ? 'placeholder' : 'text'; + // If there is a label then we need to concatenate it with the + // current value and a comma so it separates nicely when the screen reader + // announces it, otherwise just announce the value + const displayLabel = labelText !== undefined + ? `${displayValue}, ${labelText}` + : displayValue; + return (
+ + ref={(focusEl => this.focusEl = focusEl)} + >
); } diff --git a/core/src/components/select/test/a11y/index.html b/core/src/components/select/test/a11y/index.html new file mode 100644 index 0000000000..394742cef6 --- /dev/null +++ b/core/src/components/select/test/a11y/index.html @@ -0,0 +1,152 @@ + + + + + + Select - a11y + + + + + + + + + + + + + + Select - a11y + + + + + + + + Native Select + + + +
+ + + +
+
+ + + + + Default Ionic Select + + + + + Choose a Pet + + + Dog + Cat + Hamster + Parrot + Spider + Goldfish + + + + + + + + Custom Label Ionic Select + + + + + + + + Dog + Cat + Hamster + Parrot + Spider + Goldfish + + + + + + + + Popover Ionic Select + + + + + Choose a Pet + + + Dog + Cat + Hamster + Parrot + Spider + Goldfish + + + + + + + + Action Sheet Ionic Select + + + + + Choose a Pet + + + Dog + Cat + Hamster + Parrot + Spider + Goldfish + + + + + +
+ + + + +
+ + + diff --git a/core/src/utils/helpers.ts b/core/src/utils/helpers.ts index 2b5d6ec272..10e6d23a97 100644 --- a/core/src/utils/helpers.ts +++ b/core/src/utils/helpers.ts @@ -124,11 +124,15 @@ export const getAriaLabel = (componentEl: HTMLElement, inputId: string): { label // we should use that instead of looking for an ion-label const labelledBy = componentEl.getAttribute('aria-labelledby'); - const labelId = labelledBy !== null + // Grab the id off of the component in case they are using + // a custom label using the label element + const componentId = componentEl.id; + + let labelId = labelledBy !== null ? labelledBy : inputId + '-lbl'; - const label = labelledBy !== null + let label = labelledBy !== null && labelledBy.trim() !== '' ? document.querySelector(`#${labelledBy}`) : findItemLabel(componentEl); @@ -139,6 +143,16 @@ export const getAriaLabel = (componentEl: HTMLElement, inputId: string): { label labelText = label.textContent; label.setAttribute('aria-hidden', 'true'); + + // if there is no label, check to see if the user has provided + // one by setting an id on the component and using the label element + } else if (componentId.trim() !== '') { + label = document.querySelector(`label[for=${componentId}]`); + + if (label) { + label.id = labelId = `${componentId}-lbl`; + labelText = label.textContent; + } } return { label, labelId, labelText };