feat(range): component can be used outside of ion-item (#26479)

This commit is contained in:
Liam DeBeasi
2022-12-15 15:52:56 -05:00
committed by GitHub
parent 6c82435025
commit 49baad8ee6
277 changed files with 2239 additions and 545 deletions

View File

@ -1602,13 +1602,13 @@ mouse drag, touch gesture, or keyboard interaction.
@ProxyCmp({
defineCustomElementFn: undefined,
inputs: ['activeBarStart', 'color', 'debounce', 'disabled', 'dualKnobs', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
inputs: ['activeBarStart', 'color', 'debounce', 'disabled', 'dualKnobs', 'labelPlacement', 'legacy', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
})
@Component({
selector: 'ion-range',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
inputs: ['activeBarStart', 'color', 'debounce', 'disabled', 'dualKnobs', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
inputs: ['activeBarStart', 'color', 'debounce', 'disabled', 'dualKnobs', 'labelPlacement', 'legacy', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
})
export class IonRange {
protected el: HTMLElement;

View File

@ -1025,6 +1025,8 @@ ion-range,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secon
ion-range,prop,debounce,number | undefined,undefined,false,false
ion-range,prop,disabled,boolean,false,false,false
ion-range,prop,dualKnobs,boolean,false,false,false
ion-range,prop,labelPlacement,"end" | "fixed" | "start",'start',false,false
ion-range,prop,legacy,boolean | undefined,undefined,false,false
ion-range,prop,max,number,100,false,false
ion-range,prop,min,number,0,false,false
ion-range,prop,mode,"ios" | "md",undefined,false,false

View File

@ -2196,6 +2196,14 @@ export namespace Components {
* Show two knobs.
*/
"dualKnobs": boolean;
/**
* Where to place the label relative to the range. `'start'`: The label will appear to the left of the range in LTR and to the right in RTL. `'end'`: The label will appear to the right of the range in LTR and to the left in RTL. `'fixed'`: The label has the same behavior as `'start'` except it also has a fixed width. Long text will be truncated with ellipses ("...").
*/
"labelPlacement": 'start' | 'end' | 'fixed';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the `label` property. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/
"legacy"?: boolean;
/**
* Maximum integer value of the range.
*/
@ -6102,6 +6110,14 @@ declare namespace LocalJSX {
* Show two knobs.
*/
"dualKnobs"?: boolean;
/**
* Where to place the label relative to the range. `'start'`: The label will appear to the left of the range in LTR and to the right in RTL. `'end'`: The label will appear to the right of the range in LTR and to the left in RTL. `'fixed'`: The label has the same behavior as `'start'` except it also has a fixed width. Long text will be truncated with ellipses ("...").
*/
"labelPlacement"?: 'start' | 'end' | 'fixed';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the `label` property. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/
"legacy"?: boolean;
/**
* Maximum integer value of the range.
*/

View File

@ -107,6 +107,15 @@
overflow: initial;
}
/**
* The shadow of the range knob should
* not be clipped by the item.
*/
// TODO FW-2997 This should check for a slotted ion-range
:host(.item-has-modern-range) {
overflow: initial;
}
// Item: Color
// --------------------------------------------------

View File

@ -311,8 +311,11 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
}
private hasModernInput(): boolean {
const input = this.el.querySelector('ion-input:not(.legacy-input)');
return input !== null;
return this.el.querySelector('ion-input:not(.legacy-input)') !== null;
}
private hasModernRange(): boolean {
return this.el.querySelector('ion-range:not(.legacy-range)') !== null;
}
private getFirstInput(): HTMLIonInputElement | HTMLIonTextareaElement {
@ -401,6 +404,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
const fillValue = fill || 'none';
const inList = hostContext('ion-list', this.el);
const hasModernInput = this.hasModernInput();
const hasModernRange = this.hasModernRange();
return (
<Host
@ -421,6 +425,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
'ion-focusable': this.focusable,
'item-rtl': document.dir === 'rtl',
'item-has-modern-input': hasModernInput,
'item-has-modern-range': hasModernRange,
}),
}}
role={inList ? 'listitem' : null}

View File

@ -85,7 +85,7 @@
<ion-progress-bar id="progressBar"></ion-progress-bar>
<ion-item>
<ion-range pin="true" value="0" id="progressValue">
<ion-range legacy="true" pin="true" value="0" id="progressValue">
<ion-label slot="start">0</ion-label>
<ion-label slot="end">100</ion-label>
</ion-range>
@ -113,7 +113,7 @@
<ion-progress-bar class="progressBarBuffer" value="0.20" buffer="0.4" reversed="true"></ion-progress-bar>
<ion-item>
<ion-range pin="true" value="0" id="progressValueBuffer">
<ion-range legacy="true" pin="true" value="0" id="progressValueBuffer">
<ion-label slot="start">0</ion-label>
<ion-label slot="end">100</ion-label>
</ion-range>

View File

@ -14,7 +14,11 @@
--bar-background-active: #{ion-color(primary, base)};
--bar-border-radius: #{$range-ios-bar-border-radius};
--height: #{$range-ios-slider-height};
--margin: 16px;
}
// TODO FW-2997 remove this
:host(.legacy-range) {
@include padding($range-ios-padding-vertical, $range-ios-padding-horizontal);
}
@ -24,11 +28,11 @@
}
::slotted([slot="start"]) {
@include margin(0, 16px, 0, 0);
@include margin(0, var(--margin), 0, 0);
}
::slotted([slot="end"]) {
@include margin(0, 0, 0, 16px);
@include margin(0, 0, 0, var(--margin));
}
:host(.range-has-pin) {

View File

@ -16,12 +16,22 @@
--height: #{$range-md-slider-height};
--pin-background: #{ion-color(primary, base)};
--pin-color: #{ion-color(primary, contrast)};
--margin: 14px;
@include padding($range-md-padding-vertical, $range-md-padding-horizontal);
// TODO FW-2997 Apply this to the start/end slots, and the native wrapper
font-size: $range-md-pin-font-size;
}
// TODO FW-2997 Remove this
::slotted([slot="label"]) {
font-size: initial;
}
// TODO FW-2997 remove this
:host(.legacy-range) {
@include padding($range-md-padding-vertical, $range-md-padding-horizontal);
}
:host(.ion-color) .range-bar {
background: current-color(base, 0.26);
}
@ -37,11 +47,11 @@
}
::slotted([slot="start"]) {
@include margin(0, 14px, 0, 0);
@include margin(0, var(--margin), 0, 0);
}
::slotted([slot="end"]) {
@include margin(0, 0, 0, 14px);
@include margin(0, 0, 0, var(--margin));
}
:host(.range-has-pin) {

View File

@ -18,6 +18,7 @@
* @prop --pin-color: Color of the range pin (only available in MD mode)
*/
--knob-handle-size: calc(var(--knob-size) * 2);
--margin: 8px;
display: flex;
position: relative;
@ -172,3 +173,140 @@
:host(.in-item) ::slotted(ion-label) {
align-self: center;
}
// Range Wrapper
// --------------------------------------------------
.range-wrapper {
display: flex;
position: relative;
flex-grow: 1;
align-items: center;
height: 100%;
}
// Range Label
// ----------------------------------------------------------------
/**
* When the range is disabled, only the text
* receives an opacity. The range changes color instead.
*/
:host(.range-disabled) .label-text-wrapper {
opacity: 0.3;
}
::slotted([slot="label"]) {
/**
* Label text should not extend
* beyond the bounds of the range.
* However, we do not set the max
* width to 100% because then
* only the label would show and users
* would not be able to see the range.
*/
max-width: 200px;
/**
* This ensures that double tapping this text
* clicks the <label> and focuses the range
* when a screen reader is enabled.
*/
pointer-events: none;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
/**
* If no label text is placed into the slot
* then the element should be hidden otherwise
* there will be additional margins added.
*/
.label-text-wrapper-hidden {
display: none;
}
// Range Native Wrapper
// ----------------------------------------------------------------
.native-wrapper {
display: flex;
flex-grow: 1;
align-items: center;
}
// Range Label Placement - Start
// ----------------------------------------------------------------
/**
* Label is on the left of the range in LTR and
* on the right in RTL.
*/
:host(.range-label-placement-start) .range-wrapper {
flex-direction: row;
}
:host(.range-label-placement-start) .label-text-wrapper {
/**
* The margin between the label and
* the range should be on the end
* when the label sits at the start.
*/
@include margin(0, var(--margin), 0, 0);
}
// Range Label Placement - End
// ----------------------------------------------------------------
/**
* Label is on the right of the range in LTR and
* on the left in RTL.
*/
:host(.range-label-placement-end) .range-wrapper {
flex-direction: row-reverse;
}
/**
* The margin between the label and
* the range should be on the start
* when the label sits at the end.
*/
:host(.range-label-placement-end) .label-text-wrapper {
@include margin(0, 0, 0, var(--margin));
}
// Range Label Placement - Fixed
// ----------------------------------------------------------------
:host(.range-label-placement-fixed) .label-text-wrapper {
/**
* The margin between the label and
* the range should be on the end
* when the label sits at the start.
*/
@include margin(0, var(--margin), 0, 0);
}
/**
* Label is on the left of the range in LTR and
* on the right in RTL. Label also has a fixed width.
*/
:host(.range-label-placement-fixed) .label-text-wrapper {
flex: 0 0 100px;
width: 100px;
min-width: 100px;
max-width: 200px;
}

View File

@ -14,6 +14,8 @@ import type {
StyleEventDetail,
} from '../../interface';
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from '../../utils/content';
import type { LegacyFormController } from '../../utils/forms';
import { createLegacyFormController } from '../../utils/forms';
import type { Attributes } from '../../utils/helpers';
import { inheritAriaAttributes, clamp, debounceEvent, getAriaLabel, renderHiddenInput } from '../../utils/helpers';
import { printIonWarning } from '../../utils/logging';
@ -25,6 +27,7 @@ import type { PinFormatter } from './range-interface';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
* @slot label - The label text to associate with the range. Use the "labelPlacement" property to control where the label is placed relative to the range.
* @slot start - Content is placed to the left of the range slider in LTR, and to the right in RTL.
* @slot end - Content is placed to the right of the range slider in LTR, and to the left in RTL.
*
@ -55,6 +58,10 @@ export class Range implements ComponentInterface {
private contentEl: HTMLElement | null = null;
private initialContentScrollY = true;
private originalIonInput?: EventEmitter<RangeChangeEventDetail>;
private legacyFormController!: LegacyFormController;
// This flag ensures we log the deprecation warning at most once.
private hasLoggedDeprecationWarning = false;
@Element() el!: HTMLIonRangeElement;
@ -211,6 +218,25 @@ export class Range implements ComponentInterface {
}
};
/**
* Where to place the label relative to the range.
* `'start'`: The label will appear to the left of the range in LTR and to the right in RTL.
* `'end'`: The label will appear to the right of the range in LTR and to the left in RTL.
* `'fixed'`: The label has the same behavior as `'start'` except it also has a fixed width. Long text will be truncated with ellipses ("...").
*/
@Prop() labelPlacement: 'start' | 'end' | 'fixed' = 'start';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup.
* Ionic will only opt components in to the modern form markup when they are
* using either the `aria-label` attribute or the `label` property. As a result,
* the `legacy` property should only be used as an escape hatch when you want to
* avoid this automatic opt-in behavior.
* Note that this property will be removed in an upcoming major release
* of Ionic, and all form components will be opted-in to using the modern form markup.
*/
@Prop() legacy?: boolean;
/**
* The `ionChange` event is fired for `<ion-range>` elements when the user
* modifies the element's value:
@ -289,6 +315,10 @@ export class Range implements ComponentInterface {
}
connectedCallback() {
const { el } = this;
this.legacyFormController = createLegacyFormController(el);
this.updateRatio();
this.debounceChanged();
this.disabledChanged();
@ -352,12 +382,15 @@ export class Range implements ComponentInterface {
}
}
// TODO FW-2997 remove this
private emitStyle() {
if (this.legacyFormController.hasLegacyControl()) {
this.ionStyle.emit({
interactive: true,
'interactive-disabled': this.disabled,
});
}
}
/**
* Emits an `ionChange` event.
@ -517,7 +550,102 @@ export class Range implements ComponentInterface {
}
};
render() {
// TODO FW-2997 remove this
private renderLegacyRange() {
if (!this.hasLoggedDeprecationWarning) {
printIonWarning(
`Using ion-range with an ion-label has been deprecated. To migrate, remove the ion-label and pass your label directly into ion-toggle instead.
Example: <ion-range>Volume:</ion-toggle>
For ranges that do not have a visible label, developers should use "aria-label" so screen readers can announce the purpose of the range.`,
this.el
);
if (this.legacy) {
printIonWarning(
`ion-range is being used with the "legacy" property enabled which will forcibly enable the legacy form markup. This property will be removed in an upcoming major release of Ionic where this form control will use the modern form markup.
Developers can dismiss this warning by removing their usage of the "legacy" property and using the new range syntax.`,
this.el
);
}
this.hasLoggedDeprecationWarning = true;
}
const { el, pressedKnob, disabled, pin, rangeId } = this;
const mode = getIonMode(this);
renderHiddenInput(true, el, this.name, JSON.stringify(this.getValue()), disabled);
return (
<Host
onFocusin={this.onFocus}
onFocusout={this.onBlur}
id={rangeId}
class={createColorClasses(this.color, {
[mode]: true,
'in-item': hostContext('ion-item', el),
'range-disabled': disabled,
'range-pressed': pressedKnob !== undefined,
'range-has-pin': pin,
'legacy-range': true,
})}
>
<slot name="start"></slot>
{this.renderRangeSlider()}
<slot name="end"></slot>
</Host>
);
}
private renderRange() {
const { disabled, el, rangeId, pin, pressedKnob, labelPlacement } = this;
const mode = getIonMode(this);
renderHiddenInput(true, el, this.name, JSON.stringify(this.getValue()), disabled);
return (
<Host
onFocusin={this.onFocus}
onFocusout={this.onBlur}
id={rangeId}
class={createColorClasses(this.color, {
[mode]: true,
'in-item': hostContext('ion-item', el),
'range-disabled': disabled,
'range-pressed': pressedKnob !== undefined,
'range-has-pin': pin,
[`range-label-placement-${labelPlacement}`]: true,
})}
>
<label class="range-wrapper" id="range-label">
<div
class={{
'label-text-wrapper': true,
'label-text-wrapper-hidden': !this.hasLabel,
}}
>
<slot name="label"></slot>
</div>
<div class="native-wrapper">
<slot name="start"></slot>
{this.renderRangeSlider()}
<slot name="end"></slot>
</div>
</label>
</Host>
);
}
private get hasLabel() {
return this.el.querySelector('[slot="label"]') !== null;
}
private renderRangeSlider() {
const {
min,
max,
@ -543,7 +671,7 @@ export class Range implements ComponentInterface {
if (labelText === undefined || labelText === null) {
labelText = inheritedAttributes['aria-label'];
}
const mode = getIonMode(this);
let barStart = `${ratioLower * 100}%`;
let barEnd = `${100 - ratioUpper * 100}%`;
@ -613,22 +741,12 @@ export class Range implements ComponentInterface {
}
}
renderHiddenInput(true, el, this.name, JSON.stringify(this.getValue()), disabled);
let labelledBy: string | undefined;
if (!this.legacyFormController.hasLegacyControl() && this.hasLabel) {
labelledBy = 'range-label';
}
return (
<Host
onFocusin={this.onFocus}
onFocusout={this.onBlur}
id={rangeId}
class={createColorClasses(this.color, {
[mode]: true,
'in-item': hostContext('ion-item', el),
'range-disabled': disabled,
'range-pressed': pressedKnob !== undefined,
'range-has-pin': pin,
})}
>
<slot name="start"></slot>
<div class="range-slider" ref={(rangeEl) => (this.rangeSlider = rangeEl)}>
{ticks.map((tick) => (
<div
@ -659,6 +777,7 @@ export class Range implements ComponentInterface {
min,
max,
labelText,
labelledBy,
})}
{this.dualKnobs &&
@ -674,12 +793,16 @@ export class Range implements ComponentInterface {
min,
max,
labelText,
labelledBy,
})}
</div>
<slot name="end"></slot>
</Host>
);
}
render() {
const { legacyFormController } = this;
return legacyFormController.hasLegacyControl() ? this.renderLegacyRange() : this.renderRange();
}
}
interface RangeKnob {
@ -693,13 +816,26 @@ interface RangeKnob {
pin: boolean;
pinFormatter: PinFormatter;
labelText?: string | null;
labelledBy?: string;
handleKeyboard: (name: KnobName, isIncrease: boolean) => void;
}
const renderKnob = (
rtl: boolean,
{ knob, value, ratio, min, max, disabled, pressed, pin, handleKeyboard, labelText, pinFormatter }: RangeKnob
{
knob,
value,
ratio,
min,
max,
disabled,
pressed,
pin,
handleKeyboard,
labelText,
labelledBy,
pinFormatter,
}: RangeKnob
) => {
const start = rtl ? 'right' : 'left';
@ -738,7 +874,8 @@ const renderKnob = (
style={knobStyle()}
role="slider"
tabindex={disabled ? -1 : 0}
aria-label={labelText}
aria-label={labelledBy === undefined ? labelText : null}
aria-labelledby={labelledBy !== undefined ? labelledBy : null}
aria-valuemin={min}
aria-valuemax={max}
aria-disabled={disabled ? 'true' : null}

View File

@ -3,33 +3,26 @@
<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"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.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>
<main>
<h1>Range - a11y</h1>
<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>
<ion-range><span slot="label">my label</span></ion-range
><br />
<ion-range aria-label="my aria label"></ion-range><br />
<ion-range>
<span slot="label">temperature</span>
<ion-icon name="snow" slot="start"></ion-icon>
<ion-icon name="flame" slot="end"></ion-icon> </ion-range
><br />
</main>
</body>
</html>

View File

@ -1,59 +1,22 @@
import AxeBuilder from '@axe-core/playwright';
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('range: a11y', () => {
test('should not have visual regressions', async ({ page, skip }) => {
test.beforeEach(async ({ skip }) => {
skip.rtl();
skip.mode('ios', 'iOS mode does not display hover/active/focus styles.');
await page.setContent(
`<ion-app>
<ion-content>
<ion-range min="0" max="100" value="80"></ion-range>
</ion-content>
</ion-app>
`
);
const range = page.locator('ion-range');
const rangeHandle = range.locator('.range-knob-handle');
await rangeHandle.evaluate((el) => el.classList.add('ion-focused'));
await page.waitForChanges();
expect(await range.screenshot()).toMatchSnapshot(`range-focus-${page.getSnapshotSettings()}.png`);
const box = (await rangeHandle.boundingBox())!;
const centerX = box.x + box.width / 2;
const centerY = box.y + box.height / 2;
await page.mouse.move(centerX, centerY);
await page.mouse.down();
await page.waitForChanges();
expect(await range.screenshot()).toMatchSnapshot(`range-active-${page.getSnapshotSettings()}.png`);
skip.mode('md');
});
test.describe('with pin', () => {
test('should not have visual regressions', async ({ page, skip }) => {
skip.rtl();
test('should not have accessibility violations', async ({ page }) => {
await page.goto(`/src/components/range/test/a11y`);
await page.setContent(
`<ion-app>
<ion-content>
<ion-range min="0" max="100" value="50" pin="true"></ion-range>
</ion-content>
</ion-app>
`
);
const range = page.locator('ion-range');
const rangeHandle = range.locator('.range-knob-handle');
await rangeHandle.evaluate((el) => el.classList.add('ion-focused'));
await page.waitForChanges();
expect(await range.screenshot()).toMatchSnapshot(`range-focus-with-pin-${page.getSnapshotSettings()}.png`);
});
/**
* Axe does not take <slot> elements into account
* when checking color-contrast. As a result, it will
* incorrectly report color-contrast issues: https://github.com/dequelabs/axe-core/issues/3329
*/
const results = await new AxeBuilder({ page }).disableRules('color-contrast').analyze();
expect(results.violations).toEqual([]);
});
});

View File

@ -11,16 +11,31 @@
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/core.css" rel="stylesheet" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
ion-range {
/* End knob is being cut-off */
width: 90%;
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
}
@media screen and (max-width: 800px) {
.grid {
grid-template-columns: 1fr;
padding: 0;
}
}
</style>
</head>
@ -32,12 +47,12 @@
<ion-title>Range - activeBarStart</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding" id="content">
<ion-list>
<ion-item>
<ion-label position="stacked">activeBarStart is 0 and value is 0.</ion-label>
<ion-content class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>activeBarStart 0, value 0</h2>
<ion-range
class="ion-no-padding"
aria-label="my range"
min="-100"
max="100"
snaps="10"
@ -45,13 +60,13 @@
pin="true"
active-bar-start="0"
value="0"
>
</ion-range>
</ion-item>
<ion-item>
<ion-label position="stacked"> activeBarStart is 0 and value is 70. </ion-label>
></ion-range>
</div>
<div class="grid-item">
<h2>activeBarStart 0, value 70</h2>
<ion-range
class="ion-no-padding"
aria-label="my range"
min="-100"
max="100"
snaps="10"
@ -59,13 +74,13 @@
pin="true"
active-bar-start="0"
value="70"
>
</ion-range>
</ion-item>
<ion-item>
<ion-label position="stacked"> activeBarStart is 0 and value is -70. </ion-label>
></ion-range>
</div>
<div class="grid-item">
<h2>activeBarStart 0, value -70</h2>
<ion-range
class="ion-no-padding"
aria-label="my range"
min="-100"
max="100"
snaps="10"
@ -73,13 +88,13 @@
pin="true"
active-bar-start="0"
value="-70"
>
</ion-range>
</ion-item>
<ion-item>
<ion-label position="stacked"> activeBarStart is -30 and value is 0. </ion-label>
></ion-range>
</div>
<div class="grid-item">
<h2>activeBarStart -30, value 0</h2>
<ion-range
class="ion-no-padding"
aria-label="my range"
min="-100"
max="100"
snaps="10"
@ -87,13 +102,13 @@
pin="true"
active-bar-start="-30"
value="0"
>
</ion-range>
</ion-item>
<ion-item>
<ion-label position="stacked"> activeBarStart is 30 and value is 0. </ion-label>
></ion-range>
</div>
<div class="grid-item">
<h2>activeBarStart 30, value 0</h2>
<ion-range
class="ion-no-padding"
aria-label="my range"
min="-100"
max="100"
snaps="10"
@ -101,13 +116,13 @@
pin="true"
active-bar-start="30"
value="0"
>
</ion-range>
</ion-item>
<ion-item>
<ion-label position="stacked">activeBarStart is between steps</ion-label>
></ion-range>
</div>
<div class="grid-item">
<h2>activeBarStart is between steps</h2>
<ion-range
class="ion-no-padding"
aria-label="my range"
min="-100"
max="100"
snaps="10"
@ -116,21 +131,38 @@
active-bar-start="25"
value="0"
></ion-range>
</ion-item>
<ion-item>
<ion-label position="stacked">invalid activeBarStart value (less than min)</ion-label>
<ion-range class="ion-no-padding" min="0" max="100" snaps="10" step="10" pin="true" active-bar-start="-30">
</ion-range>
</ion-item>
<ion-item>
<ion-label position="stacked">invalid activeBarStart value (greater than max)</ion-label>
<ion-range class="ion-no-padding" min="0" max="100" snaps="10" step="10" pin="true" active-bar-start="110">
</ion-range>
</ion-item>
<ion-item>
<ion-label position="stacked"> activeBarStart is ignored with dual knobs enabled. </ion-label>
</div>
<div class="grid-item">
<h2>invalid activeBarStart value (less than min)</h2>
<ion-range
class="ion-no-padding"
aria-label="my range"
min="0"
max="100"
snaps="10"
step="10"
pin="true"
active-bar-start="-30"
></ion-range>
</div>
<div class="grid-item">
<h2>invalid activeBarStart value (greater than max)</h2>
<ion-range
aria-label="my range"
min="0"
max="100"
snaps="10"
step="10"
pin="true"
active-bar-start="110"
></ion-range>
</div>
<div class="grid-item">
<h2>activeBarStart is ignored with dual knobs enabled</h2>
<ion-range
aria-label="my range"
min="-100"
max="100"
snaps="10"
@ -140,8 +172,8 @@
dual-knobs="true"
value="30"
></ion-range>
</ion-item>
</ion-list>
</div>
</div>
</ion-content>
</ion-app>
</body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

@ -12,46 +12,30 @@
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<style>
.range-part::part(bar) {
background: red;
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
.range-part::part(tick) {
background: purple;
color: #6f7378;
margin-top: 10px;
}
.range-part::part(bar-active) {
background: green;
@media screen and (max-width: 800px) {
.grid {
grid-template-columns: 1fr;
padding: 0;
}
.range-part::part(tick-active) {
background: orange;
}
.range-part::part(knob) {
background: hotpink;
}
.ios.range-part::part(pin) {
background: orange;
top: -13px;
height: 20px;
line-height: 4px;
border-radius: 4px;
transform: translate(0, 0, 0);
}
.md.range-part::part(pin),
.md.range-part::part(pin)::before {
background: orange;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
@ -60,221 +44,42 @@
</ion-toolbar>
</ion-header>
<ion-content id="content">
<ion-list>
<ion-list-header>
<ion-label> Range color </ion-label>
</ion-list-header>
<ion-item>
<ion-range id="basic" value="20" aria-label="Default Range"></ion-range>
</ion-item>
<ion-item>
<ion-range value="60" color="light" step="10" pin="true" aria-label="Light Pin Range"></ion-range>
</ion-item>
<ion-item>
<ion-range
value="80"
color="dark"
step="10"
snaps="true"
pin="true"
aria-label="Dark Pin Range"
></ion-range>
</ion-item>
<ion-item>
<ion-range pin="true" color="secondary" value="86" aria-label="Secondary Dual Range">
<ion-icon small name="sunny" slot="start"></ion-icon>
<ion-icon name="sunny" slot="end"></ion-icon>
<ion-content id="content" class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>Default</h2>
<ion-range class="default" value="50">
<span slot="label">Temperature</span>
</ion-range>
<ion-range pin="true" color="danger" value="54" aria-label="Danger Dual Range">
<ion-icon small name="thermometer" slot="start"></ion-icon>
<ion-icon name="thermometer" slot="end"></ion-icon>
</div>
<div class="grid-item">
<h2>Dual Knobs</h2>
<ion-range dual-knobs="true" class="dual-knobs">
<span slot="label">Temperature</span>
</ion-range>
</ion-item>
<ion-item>
<ion-label position="stacked">Stacked Label</ion-label>
<ion-range value="40" id="stacked-range">
<ion-label slot="start">Start</ion-label>
<ion-label slot="end">End</ion-label>
</div>
<div class="grid-item">
<h2>Ticks with Step</h2>
<ion-range class="ticks" step="10" snaps="true" value="50">
<span slot="label">Temperature</span>
</ion-range>
</ion-item>
</ion-list>
<ion-list>
<ion-list-header>
<ion-label> Dynamic Value </ion-label>
</ion-list-header>
<ion-item>
<ion-range pin="true" step="0" color="secondary" id="progressValue" aria-label="Progress Range"></ion-range>
<div id="progressValueResult" slot="end"></div>
</ion-item>
<ion-item>
<ion-range pin="true" color="danger" id="brightnessValue" aria-label="Brightness Range"></ion-range>
<div id="brightnessValueResult" slot="end"></div>
</ion-item>
<ion-item>
<ion-range pin="true" color="dark" id="contrastValue" aria-label="Contrast Range"></ion-range>
<div id="contrastValueResult" slot="end"></div>
</ion-item>
<ion-button onclick="increaseRangeValues()"> Increase Values </ion-button>
</ion-list>
<ion-list>
<ion-list-header>
<ion-label> Mode </ion-label>
</ion-list-header>
<ion-item>
<ion-range value="50" mode="md" aria-label="Material Design Range"></ion-range>
</ion-item>
<ion-item>
<ion-range value="50" mode="ios" aria-label="iOS Range"></ion-range>
</ion-item>
</ion-list>
<ion-list>
<ion-list-header>
<ion-label> Options </ion-label>
</ion-list-header>
<ion-item>
<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-range pin="true" aria-label="Custom Range"></ion-range>
</ion-item>
<ion-item>
<ion-range min="-200" max="200" step="10" snaps="true" pin="true" aria-label="Custom Range"></ion-range>
</ion-item>
<ion-item>
<ion-range min="1000" max="2000" step="100" snaps="true" id="range" aria-label="Custom Range"></ion-range>
</ion-item>
<ion-item>
<ion-range
min="1000"
max="2000"
step="100"
snaps="true"
ticks="false"
aria-label="Custom Range"
></ion-range>
</ion-item>
<ion-item>
<ion-range dual-knobs="true" id="multiKnob" aria-label="Custom Range"></ion-range>
</ion-item>
<ion-item>
<ion-range id="debounce" debounce="5000" aria-label="Custom Range"></ion-range>
</ion-item>
</ion-list>
<ion-button onclick="elTest()">Test</ion-button>
<ion-list>
<ion-list-header>
<ion-label> Coupled sliders </ion-label>
</ion-list-header>
<ion-item>
<ion-range min="0" value="0" max="50" id="minRange" aria-label="Coupled Range"></ion-range>
</ion-item>
<ion-item>
<ion-range min="50" value="100" max="100" id="maxRange" aria-label="Coupled Range"></ion-range>
</ion-item>
<ion-item>
<ion-range min="0" value="50" max="100" id="targetRange" aria-label="Coupled Range"></ion-range>
</ion-item>
</ion-list>
<ion-list>
<ion-list-header>
<ion-label> Custom pin label </ion-label>
</ion-list-header>
<ion-item>
<ion-range pin min="1" step="0.1" max="2" id="customLabel"></ion-range>
</ion-item>
</ion-list>
</div>
<div class="grid-item">
<h2>Pin</h2>
<ion-range class="pin" pin="true">
<span slot="label">Temperature</span>
</ion-range>
</div>
</div>
</ion-content>
</ion-app>
<script>
const progressValue = document.getElementById('progressValue');
const brightnessValue = document.getElementById('brightnessValue');
const contrastValue = document.getElementById('contrastValue');
const ranges = [progressValue, brightnessValue, contrastValue];
for (var i = 0; i < ranges.length; i++) {
var el = ranges[i];
el.value = (i + 1) * 10;
updateRangeResult(el);
el.addEventListener('ionChange', function (ev) {
updateRangeResult(ev.target);
});
}
progressValue.addEventListener('ionChange', function (ev) {
console.log(ev.detail.value);
brightnessValue.value = ev.detail.value;
contrastValue.value = ev.target.value;
});
function updateRangeResult(el) {
var resultEl = document.getElementById(`${el.id}Result`);
resultEl.innerHTML = Math.round(el.value);
}
function increaseRangeValues() {
for (var i = 0; i < ranges.length; i++) {
var el = ranges[i];
var newValue = el.value + 10;
if (newValue > 100) {
newValue = 10;
}
el.value = newValue;
}
}
var knob = document.getElementById('multiKnob');
var debounceRange = document.getElementById('debounce');
knob.value = {
lower: 33,
upper: 60,
const dualKnobs = document.querySelector('.dual-knobs');
dualKnobs.value = {
lower: '10',
upper: '90',
};
function elTest() {
var range = document.getElementById('range');
range.disabled = !range.disabled;
}
const minRange = document.getElementById('minRange');
const maxRange = document.getElementById('maxRange');
const targetRange = document.getElementById('targetRange');
minRange.addEventListener('ionChange', function (ev) {
targetRange.min = ev.detail.value;
});
maxRange.addEventListener('ionChange', function (ev) {
targetRange.max = ev.detail.value;
});
const rangePart = document.querySelector('.range-part');
rangePart.value = {
lower: '-100',
upper: '100',
};
const customLabel = document.getElementById('customLabel');
customLabel.pinFormatter = (value) => value.toFixed(1);
</script>
</body>
</html>

View File

@ -1,92 +1,33 @@
import { expect } from '@playwright/test';
import { dragElementBy, test } from '@utils/test/playwright';
import { test } from '@utils/test/playwright';
test.describe('range: basic', () => {
test('should not have visual regressions', async ({ page }) => {
await page.goto(`/src/components/range/test/basic`);
await page.setIonViewport();
expect(await page.screenshot()).toMatchSnapshot(`range-diff-${page.getSnapshotSettings()}.png`);
});
/**
* The mouse events are flaky on CI
* TODO FW-2873
*/
test.fixme('should emit start/end events', async ({ page }, testInfo) => {
await page.setContent(`<ion-range value="20"></ion-range>`);
const rangeStart = await page.spyOnEvent('ionKnobMoveStart');
const rangeEnd = await page.spyOnEvent('ionKnobMoveEnd');
const rangeEl = page.locator('ion-range');
await dragElementBy(rangeEl, page, testInfo.project.metadata.rtl ? -300 : 300, 0);
await page.waitForChanges();
/**
* dragElementBy defaults to starting the drag from the middle of the el,
* so the start value should jump to 50 despite the range defaulting to 20.
*/
expect(rangeStart).toHaveReceivedEventDetail({ value: 50 });
expect(rangeEnd).toHaveReceivedEventDetail({ value: 100 });
/**
* Verify both events fire if range is clicked without dragging.
*/
await dragElementBy(rangeEl, page, 0, 0);
await page.waitForChanges();
expect(rangeStart).toHaveReceivedEventDetail({ value: 50 });
expect(rangeEnd).toHaveReceivedEventDetail({ value: 50 });
});
test('should emit start/end events, keyboard', async ({ page }) => {
await page.setContent(`<ion-range value="20"></ion-range>`);
const rangeStart = await page.spyOnEvent('ionKnobMoveStart');
const rangeEnd = await page.spyOnEvent('ionKnobMoveEnd');
await page.keyboard.press('Tab'); // focus first range
await page.keyboard.press('ArrowRight');
await rangeStart.next();
await rangeEnd.next();
expect(rangeStart).toHaveReceivedEventDetail({ value: 20 });
expect(rangeEnd).toHaveReceivedEventDetail({ value: 21 });
});
// TODO FW-2873
test.skip('should not scroll when the knob is swiped', async ({ page, skip }) => {
skip.browser('webkit', 'mouse.wheel is not available in WebKit');
test.beforeEach(async ({ skip, page }) => {
skip.rtl();
await page.goto(`/src/components/range/test/basic`);
await page.goto('/src/components/range/test/basic');
});
test('should render default range', async ({ page }) => {
const range = page.locator('ion-range.default');
expect(await range.screenshot()).toMatchSnapshot(`range-default-${page.getSnapshotSettings()}.png`);
});
test('should render dual knob range', async ({ page }) => {
const range = page.locator('ion-range.dual-knobs');
expect(await range.screenshot()).toMatchSnapshot(`range-dual-knobs-${page.getSnapshotSettings()}.png`);
});
test('should render range with ticks', async ({ page }) => {
const range = page.locator('ion-range.ticks');
expect(await range.screenshot()).toMatchSnapshot(`range-ticks-${page.getSnapshotSettings()}.png`);
});
test('should render pin', async ({ page }) => {
const range = page.locator('ion-range.pin');
const knob = range.locator('.range-knob-handle');
const knobEl = page.locator('ion-range#stacked-range .range-knob-handle');
const scrollEl = page.locator('ion-content .inner-scroll');
// Force the pin to show
await knob.evaluate((el: HTMLElement) => el.classList.add('ion-focused'));
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
const box = (await knobEl.boundingBox())!;
const centerX = box.x + box.width / 2;
const centerY = box.y + box.height / 2;
await page.mouse.move(centerX, centerY);
await page.mouse.down();
await page.mouse.move(centerX + 30, centerY);
/**
* Do not use scrollToBottom() or other scrolling methods
* on ion-content as those will update the scroll position.
* Setting scrollTop still works even with overflow-y: hidden.
* However, simulating a user gesture should not scroll the content.
*/
await page.mouse.wheel(0, 100);
await page.waitForChanges();
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
expect(await range.screenshot({ animations: 'disabled' })).toMatchSnapshot(
`range-pin-${page.getSnapshotSettings()}.png`
);
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,20 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('range: color', () => {
test.beforeEach(({ skip }) => {
skip.rtl();
});
test('should apply color', async ({ page }) => {
await page.setContent(`
<ion-range color="danger" value="50">
<ion-icon name="volume-off" slot="start"></ion-icon>
<ion-icon name="volume-high" slot="end"></ion-icon>
<span slot="label">Volume</span>
</ion-range>
`);
const range = page.locator('ion-range');
expect(await range.screenshot()).toMatchSnapshot(`range-color-${page.getSnapshotSettings()}.png`);
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Range - Custom</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 nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<style>
.range-part::part(bar) {
background: red;
}
.range-part::part(tick) {
background: purple;
}
.range-part::part(bar-active) {
background: green;
}
.range-part::part(tick-active) {
background: orange;
}
.range-part::part(knob) {
background: hotpink;
}
.ios.range-part::part(pin) {
background: orange;
top: -13px;
height: 20px;
line-height: 4px;
border-radius: 4px;
transform: translate(0, 0, 0);
}
.md.range-part::part(pin),
.md.range-part::part(pin)::before {
background: orange;
}
</style>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Range - Custom</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<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-content>
</ion-app>
<script>
const rangePart = document.querySelector('.range-part');
rangePart.value = {
lower: '-100',
upper: '100',
};
</script>
</body>
</html>

View File

@ -0,0 +1,14 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('range: customization', () => {
test.beforeEach(({ skip }) => {
skip.rtl();
});
test('should be customizable', async ({ page }) => {
await page.goto(`/src/components/range/test/custom`);
const range = page.locator('ion-range');
expect(await range.screenshot()).toMatchSnapshot(`range-custom-${page.getSnapshotSettings()}.png`);
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Range - Item</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 nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
}
@media screen and (max-width: 800px) {
.grid {
grid-template-columns: 1fr;
padding: 0;
}
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Range - Item</ion-title>
</ion-toolbar>
</ion-header>
<ion-content color="light" id="content" class="ion-padding">
<h1>List</h1>
<div class="grid">
<div class="grid-item">
<h2>Label, No Items</h2>
<ion-list>
<ion-item>
<ion-range>
<span slot="label">Temperature</span>
</ion-range>
</ion-item>
</ion-list>
</div>
<div class="grid-item">
<h2>Label, Items</h2>
<ion-list>
<ion-item>
<ion-range>
<span slot="label">Temperature</span>
<ion-icon name="volume-off" slot="start"></ion-icon>
<ion-icon name="volume-high" slot="end"></ion-icon>
</ion-range>
</ion-item>
</ion-list>
</div>
<div class="grid-item">
<h2>No Label, Items</h2>
<ion-list>
<ion-item>
<ion-range aria-label="Temperature">
<ion-icon name="volume-off" slot="start"></ion-icon>
<ion-icon name="volume-high" slot="end"></ion-icon>
</ion-range> </ion-item
></ion-list>
</div>
<div class="grid-item">
<h2>No Label, No Items</h2>
<ion-list>
<ion-item> <ion-range aria-label="Temperature"></ion-range> </ion-item
></ion-list>
</div>
</div>
<h1>Inset List</h1>
<div class="grid">
<div class="grid-item">
<h2>Label, No Items</h2>
<ion-list inset="true">
<ion-item>
<ion-range>
<span slot="label">Temperature</span>
</ion-range>
</ion-item>
</ion-list>
</div>
<div class="grid-item">
<h2>Label, Items</h2>
<ion-list inset="true">
<ion-item>
<ion-range>
<span slot="label">Temperature</span>
<ion-icon name="volume-off" slot="start"></ion-icon>
<ion-icon name="volume-high" slot="end"></ion-icon>
</ion-range>
</ion-item>
</ion-list>
</div>
<div class="grid-item">
<h2>No Label, Items</h2>
<ion-list inset="true">
<ion-item>
<ion-range aria-label="Temperature">
<ion-icon name="volume-off" slot="start"></ion-icon>
<ion-icon name="volume-high" slot="end"></ion-icon>
</ion-range> </ion-item
></ion-list>
</div>
<div class="grid-item">
<h2>No Label, No Items</h2>
<ion-list inset="true">
<ion-item> <ion-range aria-label="Temperature"></ion-range> </ion-item
></ion-list>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -0,0 +1,37 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('range: item', () => {
test('should render correctly in list', async ({ page }) => {
await page.setContent(`
<ion-list>
<ion-item>
<ion-range><div slot="label">Temperature</div></ion-range>
</ion-item>
</ion-list>
`);
const list = page.locator('ion-list');
expect(await list.screenshot()).toMatchSnapshot(`range-list-${page.getSnapshotSettings()}.png`);
});
test('should render correctly in inset list', async ({ page }) => {
await page.setContent(`
<ion-list inset="true">
<ion-item>
<ion-range><div slot="label">Temperature</div></ion-range>
</ion-item>
</ion-list>
`);
const list = page.locator('ion-list');
expect(await list.screenshot()).toMatchSnapshot(`range-inset-list-${page.getSnapshotSettings()}.png`);
});
test('label should have correct contrast when used in an item', async ({ page, skip }) => {
skip.rtl();
await page.setContent(`
<ion-item color="danger">
<ion-range><div slot="label">Temperature</div></ion-range>
</ion-item>
`);
const item = page.locator('ion-item');
expect(await item.screenshot()).toMatchSnapshot(`range-item-color-${page.getSnapshotSettings()}.png`);
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Range - Label</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 nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
}
@media screen and (max-width: 800px) {
.grid {
grid-template-columns: 1fr;
padding: 0;
}
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Range - Label</ion-title>
</ion-toolbar>
</ion-header>
<ion-content id="content" class="ion-padding">
<h1>No Slotted Items</h1>
<div class="grid">
<div class="grid-item">
<h2>Placement Start</h2>
<ion-range>
<span slot="label">Temperature</span>
</ion-range>
</div>
<div class="grid-item">
<h2>Placement End</h2>
<ion-range label-placement="end">
<span slot="label">Temperature</span>
</ion-range>
</div>
<div class="grid-item">
<h2>Placement Fixed</h2>
<ion-range label-placement="fixed">
<span slot="label">Temperature</span>
</ion-range>
</div>
</div>
<h1>Slotted Items</h1>
<div class="grid">
<div class="grid-item">
<h2>Placement Start</h2>
<ion-range label-placement="start">
<span slot="label">Temperature</span>
<ion-icon name="snow" slot="start"></ion-icon>
<ion-icon name="flame" slot="end"></ion-icon>
</ion-range>
</div>
<div class="grid-item">
<h2>Placement End</h2>
<ion-range label-placement="end">
<span slot="label">Temperature</span>
<ion-icon name="snow" slot="start"></ion-icon>
<ion-icon name="flame" slot="end"></ion-icon>
</ion-range>
</div>
<div class="grid-item">
<h2>Placement Fixed</h2>
<ion-range label-placement="fixed">
<span slot="label">Temperature</span>
<ion-icon name="snow" slot="start"></ion-icon>
<ion-icon name="flame" slot="end"></ion-icon>
</ion-range>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More