mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 19:57:22 +08:00
fix(range): knob can now have an accessible name (#23338)
resolves #23295
This commit is contained in:
@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop
|
|||||||
|
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
import { getIonMode } from '../../global/ionic-global';
|
||||||
import { Color, Gesture, GestureDetail, KnobName, RangeChangeEventDetail, RangeValue, StyleEventDetail } from '../../interface';
|
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';
|
import { createColorClasses, hostContext } from '../../utils/theme';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,12 +28,14 @@ import { createColorClasses, hostContext } from '../../utils/theme';
|
|||||||
})
|
})
|
||||||
export class Range implements ComponentInterface {
|
export class Range implements ComponentInterface {
|
||||||
|
|
||||||
|
private rangeId?: string;
|
||||||
private didLoad = false;
|
private didLoad = false;
|
||||||
private noUpdate = false;
|
private noUpdate = false;
|
||||||
private rect!: ClientRect;
|
private rect!: ClientRect;
|
||||||
private hasFocus = false;
|
private hasFocus = false;
|
||||||
private rangeSlider?: HTMLElement;
|
private rangeSlider?: HTMLElement;
|
||||||
private gesture?: Gesture;
|
private gesture?: Gesture;
|
||||||
|
private inheritedAttributes: { [k: string]: any } = {};
|
||||||
|
|
||||||
@Element() el!: HTMLIonRangeElement;
|
@Element() el!: HTMLIonRangeElement;
|
||||||
|
|
||||||
@ -60,6 +62,8 @@ export class Range implements ComponentInterface {
|
|||||||
this.ionChange = debounceEvent(this.ionChange, this.debounce);
|
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.
|
* 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() {
|
componentDidLoad() {
|
||||||
this.setupGesture();
|
this.setupGesture();
|
||||||
this.didLoad = true;
|
this.didLoad = true;
|
||||||
@ -395,8 +409,17 @@ export class Range implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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 mode = getIonMode(this);
|
||||||
const barStart = `${ratioLower * 100}%`;
|
const barStart = `${ratioLower * 100}%`;
|
||||||
const barEnd = `${100 - ratioUpper * 100}%`;
|
const barEnd = `${100 - ratioUpper * 100}%`;
|
||||||
@ -439,6 +462,7 @@ export class Range implements ComponentInterface {
|
|||||||
<Host
|
<Host
|
||||||
onFocusin={this.onFocus}
|
onFocusin={this.onFocus}
|
||||||
onFocusout={this.onBlur}
|
onFocusout={this.onBlur}
|
||||||
|
id={rangeId}
|
||||||
class={createColorClasses(this.color, {
|
class={createColorClasses(this.color, {
|
||||||
[mode]: true,
|
[mode]: true,
|
||||||
'in-item': hostContext('ion-item', el),
|
'in-item': hostContext('ion-item', el),
|
||||||
@ -479,7 +503,8 @@ export class Range implements ComponentInterface {
|
|||||||
disabled,
|
disabled,
|
||||||
handleKeyboard,
|
handleKeyboard,
|
||||||
min,
|
min,
|
||||||
max
|
max,
|
||||||
|
labelText
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{ this.dualKnobs && renderKnob(isRTL, {
|
{ this.dualKnobs && renderKnob(isRTL, {
|
||||||
@ -491,7 +516,8 @@ export class Range implements ComponentInterface {
|
|||||||
disabled,
|
disabled,
|
||||||
handleKeyboard,
|
handleKeyboard,
|
||||||
min,
|
min,
|
||||||
max
|
max,
|
||||||
|
labelText
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<slot name="end"></slot>
|
<slot name="end"></slot>
|
||||||
@ -509,11 +535,12 @@ interface RangeKnob {
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
pressed: boolean;
|
pressed: boolean;
|
||||||
pin: boolean;
|
pin: boolean;
|
||||||
|
labelText?: string | null;
|
||||||
|
|
||||||
handleKeyboard: (name: KnobName, isIncrease: boolean) => void;
|
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 start = isRTL ? 'right' : 'left';
|
||||||
|
|
||||||
const knobStyle = () => {
|
const knobStyle = () => {
|
||||||
@ -550,6 +577,7 @@ const renderKnob = (isRTL: boolean, { knob, value, ratio, min, max, disabled, pr
|
|||||||
style={knobStyle()}
|
style={knobStyle()}
|
||||||
role="slider"
|
role="slider"
|
||||||
tabindex={disabled ? -1 : 0}
|
tabindex={disabled ? -1 : 0}
|
||||||
|
aria-label={labelText}
|
||||||
aria-valuemin={min}
|
aria-valuemin={min}
|
||||||
aria-valuemax={max}
|
aria-valuemax={max}
|
||||||
aria-disabled={disabled ? 'true' : null}
|
aria-disabled={disabled ? 'true' : null}
|
||||||
@ -577,3 +605,5 @@ const ratioToValue = (
|
|||||||
const valueToRatio = (value: number, min: number, max: number): number => {
|
const valueToRatio = (value: number, min: number, max: number): number => {
|
||||||
return clamp(0, (value - min) / (max - min), 1);
|
return clamp(0, (value - min) / (max - min), 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let rangeIds = 0;
|
||||||
|
32
core/src/components/range/test/a11y/index.html
Normal file
32
core/src/components/range/test/a11y/index.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Range - a11y</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
|
||||||
|
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
|
||||||
|
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||||
|
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<ion-app>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Range - Basic</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content id="content">
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Volume</ion-label>
|
||||||
|
<ion-range></ion-range>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-range aria-label="Volume"></ion-range>
|
||||||
|
</ion-item>
|
||||||
|
</ion-content>
|
||||||
|
</ion-app>
|
||||||
|
</body>
|
||||||
|
</html>
|
14
core/src/components/range/test/a11y/screen-readers.md
Normal file
14
core/src/components/range/test/a11y/screen-readers.md
Normal file
@ -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.
|
||||||
|
|
@ -66,20 +66,20 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-list-header>
|
</ion-list-header>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range value="20"></ion-range>
|
<ion-range value="20" aria-label="Default Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range value="60" color="light" step="10" pin="true"></ion-range>
|
<ion-range value="60" color="light" step="10" pin="true" aria-label="Light Pin Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range value="80" color="dark" step="10" snaps="true" pin="true"></ion-range>
|
<ion-range value="80" color="dark" step="10" snaps="true" pin="true" aria-label="Dark Pin Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range pin="true" color="secondary" value="86">
|
<ion-range pin="true" color="secondary" value="86" aria-label="Secondary Dual Range">
|
||||||
<ion-icon small name="sunny" slot="start"></ion-icon>
|
<ion-icon small name="sunny" slot="start"></ion-icon>
|
||||||
<ion-icon name="sunny" slot="end"></ion-icon>
|
<ion-icon name="sunny" slot="end"></ion-icon>
|
||||||
</ion-range>
|
</ion-range>
|
||||||
<ion-range pin="true" color="danger" value="54">
|
<ion-range pin="true" color="danger" value="54" aria-label="Danger Dual Range">
|
||||||
<ion-icon small name="thermometer" slot="start"></ion-icon>
|
<ion-icon small name="thermometer" slot="start"></ion-icon>
|
||||||
<ion-icon name="thermometer" slot="end"></ion-icon>
|
<ion-icon name="thermometer" slot="end"></ion-icon>
|
||||||
</ion-range>
|
</ion-range>
|
||||||
@ -100,15 +100,15 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-list-header>
|
</ion-list-header>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range pin="true" step="0" color="secondary" id="progressValue"></ion-range>
|
<ion-range pin="true" step="0" color="secondary" id="progressValue" aria-label="Progress Range"></ion-range>
|
||||||
<div id="progressValueResult" slot="end"></div>
|
<div id="progressValueResult" slot="end"></div>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range pin="true" color="danger" id="brightnessValue"></ion-range>
|
<ion-range pin="true" color="danger" id="brightnessValue" aria-label="Brightness Range"></ion-range>
|
||||||
<div id="brightnessValueResult" slot="end"></div>
|
<div id="brightnessValueResult" slot="end"></div>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range pin="true" color="dark" id="contrastValue"></ion-range>
|
<ion-range pin="true" color="dark" id="contrastValue" aria-label="Contrast Range"></ion-range>
|
||||||
<div id="contrastValueResult" slot="end"></div>
|
<div id="contrastValueResult" slot="end"></div>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-button onclick="increaseRangeValues()">
|
<ion-button onclick="increaseRangeValues()">
|
||||||
@ -123,10 +123,10 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-list-header>
|
</ion-list-header>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range value="50" mode="md"></ion-range>
|
<ion-range value="50" mode="md" aria-label="Material Design Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range value="50" mode="ios"></ion-range>
|
<ion-range value="50" mode="ios" aria-label="iOS Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
@ -137,25 +137,25 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-list-header>
|
</ion-list-header>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range class="range-part" min="-200" max="200" step="10" snaps="true" pin="true" dual-knobs="true"></ion-range>
|
<ion-range class="range-part" min="-200" max="200" step="10" snaps="true" pin="true" dual-knobs="true" aria-label="Custom Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range pin="true"></ion-range>
|
<ion-range pin="true" aria-label="Custom Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range min="-200" max="200" step="10" snaps="true" pin="true"></ion-range>
|
<ion-range min="-200" max="200" step="10" snaps="true" pin="true" aria-label="Custom Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range min="1000" max="2000" step="100" snaps="true" id="range"></ion-range>
|
<ion-range min="1000" max="2000" step="100" snaps="true" id="range" aria-label="Custom Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range min="1000" max="2000" step="100" snaps="true" ticks="false"></ion-range>
|
<ion-range min="1000" max="2000" step="100" snaps="true" ticks="false" aria-label="Custom Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range dual-knobs="true" id="multiKnob"></ion-range>
|
<ion-range dual-knobs="true" id="multiKnob" aria-label="Custom Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range id="debounce" debounce="5000"></ion-range>
|
<ion-range id="debounce" debounce="5000" aria-label="Custom Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
@ -168,13 +168,13 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-list-header>
|
</ion-list-header>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range min="0" value="0" max="50" id="minRange"></ion-range>
|
<ion-range min="0" value="0" max="50" id="minRange" aria-label="Coupled Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range min="50" value="100" max="100" id="maxRange"></ion-range>
|
<ion-range min="50" value="100" max="100" id="maxRange" aria-label="Coupled Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range min="0" value="50" max="100" id="targetRange"></ion-range>
|
<ion-range min="0" value="50" max="100" id="targetRange" aria-label="Coupled Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
@ -12,19 +12,19 @@
|
|||||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script></head>
|
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script></head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<ion-range value="20"></ion-range>
|
<ion-range value="20" aria-label="Range"></ion-range>
|
||||||
<ion-range value="60" color="light"></ion-range>
|
<ion-range value="60" color="light" aria-label="Light Range"></ion-range>
|
||||||
<ion-range value="80" color="dark"></ion-range>
|
<ion-range value="80" color="dark" aria-label="Dark Range"></ion-range>
|
||||||
<ion-range pin="true" color="secondary" value="86">
|
<ion-range pin="true" color="secondary" value="86" aria-label="Dual Range">
|
||||||
<ion-icon size="small" name="sunny" slot="start"></ion-icon>
|
<ion-icon size="small" name="sunny" slot="start"></ion-icon>
|
||||||
<ion-icon name="sunny" slot="end"></ion-icon>
|
<ion-icon name="sunny" slot="end"></ion-icon>
|
||||||
</ion-range>
|
</ion-range>
|
||||||
<ion-range pin="true" color="danger" value="54">
|
<ion-range pin="true" color="danger" value="54" aria-label="Danger Range">
|
||||||
<ion-icon size="small" name="thermometer" slot="start"></ion-icon>
|
<ion-icon size="small" name="thermometer" slot="start"></ion-icon>
|
||||||
<ion-icon name="thermometer" slot="end"></ion-icon>
|
<ion-icon name="thermometer" slot="end"></ion-icon>
|
||||||
</ion-range>
|
</ion-range>
|
||||||
<ion-range value="50" pin class="custom"></ion-range>
|
<ion-range value="50" pin class="custom" aria-label="Pin Range"></ion-range>
|
||||||
<ion-range value="150" pin color="tertiary" class="custom"></ion-range>
|
<ion-range value="150" pin color="tertiary" class="custom" aria-label="Custom Range"></ion-range>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.custom {
|
.custom {
|
||||||
|
Reference in New Issue
Block a user