feat(range): component can be used outside of ion-item (#26479)
@ -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;
|
||||
|
@ -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
|
||||
|
16
core/src/components.d.ts
vendored
@ -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.
|
||||
*/
|
||||
|
@ -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
|
||||
// --------------------------------------------------
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 145 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 137 KiB |
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 145 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 136 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 109 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 111 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 103 KiB |
@ -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>
|
||||
|
@ -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`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 8.8 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 214 KiB |
After Width: | Height: | Size: 9.8 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 9.8 KiB |
After Width: | Height: | Size: 6.0 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 9.4 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 5.3 KiB |
20
core/src/components/range/test/color/range.e2e.ts
Normal 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`);
|
||||
});
|
||||
});
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 7.5 KiB |
85
core/src/components/range/test/custom/index.html
Normal 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>
|
14
core/src/components/range/test/custom/range.e2e.ts
Normal 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`);
|
||||
});
|
||||
});
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 2.3 KiB |
134
core/src/components/range/test/item/index.html
Normal 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>
|
37
core/src/components/range/test/item/range.e2e.ts
Normal 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`);
|
||||
});
|
||||
});
|
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 8.8 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 9.2 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 8.8 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 6.0 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.9 KiB |
100
core/src/components/range/test/label/index.html
Normal 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>
|