diff --git a/core/src/components/range/range.tsx b/core/src/components/range/range.tsx index 5cbfe42667..3507d5bfe6 100644 --- a/core/src/components/range/range.tsx +++ b/core/src/components/range/range.tsx @@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop import { getIonMode } from '../../global/ionic-global'; import { Color, Gesture, GestureDetail, KnobName, RangeChangeEventDetail, RangeValue, StyleEventDetail } from '../../interface'; -import { clamp, debounceEvent, renderHiddenInput } from '../../utils/helpers'; +import { clamp, debounceEvent, getAriaLabel, inheritAttributes, renderHiddenInput } from '../../utils/helpers'; import { createColorClasses, hostContext } from '../../utils/theme'; /** @@ -28,12 +28,14 @@ import { createColorClasses, hostContext } from '../../utils/theme'; }) export class Range implements ComponentInterface { + private rangeId?: string; private didLoad = false; private noUpdate = false; private rect!: ClientRect; private hasFocus = false; private rangeSlider?: HTMLElement; private gesture?: Gesture; + private inheritedAttributes: { [k: string]: any } = {}; @Element() el!: HTMLIonRangeElement; @@ -60,6 +62,8 @@ export class Range implements ComponentInterface { this.ionChange = debounceEvent(this.ionChange, this.debounce); } + // TODO: In Ionic Framework v6 this should initialize to this.rangeId like the other form components do. + /** * The name of the control, which is submitted with the form data. */ @@ -194,6 +198,16 @@ export class Range implements ComponentInterface { } } + componentWillLoad() { + /** + * If user has custom ID set then we should + * not assign the default incrementing ID. + */ + this.rangeId = (this.el.hasAttribute('id')) ? this.el.getAttribute('id')! : `ion-r-${rangeIds++}`; + + this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']); + } + componentDidLoad() { this.setupGesture(); this.didLoad = true; @@ -395,8 +409,17 @@ export class Range implements ComponentInterface { } render() { - const { min, max, step, el, handleKeyboard, pressedKnob, disabled, pin, ratioLower, ratioUpper } = this; + const { min, max, step, el, handleKeyboard, pressedKnob, disabled, pin, ratioLower, ratioUpper, inheritedAttributes, rangeId } = this; + /** + * Look for external label, ion-label, or aria-labelledby. + * If none, see if user placed an aria-label on the host + * and use that instead. + */ + let { labelText } = getAriaLabel(el, rangeId!); + if (labelText === undefined || labelText === null) { + labelText = inheritedAttributes['aria-label']; + } const mode = getIonMode(this); const barStart = `${ratioLower * 100}%`; const barEnd = `${100 - ratioUpper * 100}%`; @@ -439,6 +462,7 @@ export class Range implements ComponentInterface { @@ -509,11 +535,12 @@ interface RangeKnob { disabled: boolean; pressed: boolean; pin: boolean; + labelText?: string | null; handleKeyboard: (name: KnobName, isIncrease: boolean) => void; } -const renderKnob = (isRTL: boolean, { knob, value, ratio, min, max, disabled, pressed, pin, handleKeyboard }: RangeKnob) => { +const renderKnob = (isRTL: boolean, { knob, value, ratio, min, max, disabled, pressed, pin, handleKeyboard, labelText }: RangeKnob) => { const start = isRTL ? 'right' : 'left'; const knobStyle = () => { @@ -550,6 +577,7 @@ const renderKnob = (isRTL: boolean, { knob, value, ratio, min, max, disabled, pr style={knobStyle()} role="slider" tabindex={disabled ? -1 : 0} + aria-label={labelText} aria-valuemin={min} aria-valuemax={max} aria-disabled={disabled ? 'true' : null} @@ -577,3 +605,5 @@ const ratioToValue = ( const valueToRatio = (value: number, min: number, max: number): number => { return clamp(0, (value - min) / (max - min), 1); }; + +let rangeIds = 0; diff --git a/core/src/components/range/test/a11y/index.html b/core/src/components/range/test/a11y/index.html new file mode 100644 index 0000000000..7c03e9ddf0 --- /dev/null +++ b/core/src/components/range/test/a11y/index.html @@ -0,0 +1,32 @@ + + + + + Range - a11y + + + + + + + + + + + Range - Basic + + + + + + Volume + + + + + + + + + + diff --git a/core/src/components/range/test/a11y/screen-readers.md b/core/src/components/range/test/a11y/screen-readers.md new file mode 100644 index 0000000000..9c2ffc3658 --- /dev/null +++ b/core/src/components/range/test/a11y/screen-readers.md @@ -0,0 +1,14 @@ +"native" refers to a native `input[type=range]` element. + +### Selecting Range Knob + +| | native | Ionic | +| ------------------------ | ------------------------ | ---------------------- | +| VoiceOver macOS - Chrome | 0, Volume, slider | 0, Volume, slider | +| VoiceOver macOS - Safari | 0, Volume, slider | 0, Volume, slider | +| VoiceOver iOS | Volume, 0.00, adjustable | Volume, 0%, adjustable | +| Android TalkBack | 0%, Volumn, slider | 0%, Volumn, slider | +| Windows NVDA | Volume slider 0 | Volume slider 0 | + +Note: On TalkBack you can use the volume keys to adjust the range slider. + diff --git a/core/src/components/range/test/basic/index.html b/core/src/components/range/test/basic/index.html index cb060b1ef2..834ff294c3 100644 --- a/core/src/components/range/test/basic/index.html +++ b/core/src/components/range/test/basic/index.html @@ -66,20 +66,20 @@ - + - + - + - + - + @@ -100,15 +100,15 @@ - +
- +
- +
@@ -123,10 +123,10 @@ - + - + @@ -137,25 +137,25 @@ - + - + - + - + - + - + - + @@ -168,13 +168,13 @@ - + - + - + diff --git a/core/src/components/range/test/standalone/index.html b/core/src/components/range/test/standalone/index.html index 3effd63b2a..004f5c1a38 100644 --- a/core/src/components/range/test/standalone/index.html +++ b/core/src/components/range/test/standalone/index.html @@ -12,19 +12,19 @@ - - - - + + + + - + - - + +