mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(range): add classes and expose parts to allow individual styling of dual knobs (#30941)
Issue number: resolves #29862 --------- ## What is the current behavior? Range exposes a single part for both knobs & pins. This makes it impossible to style the knobs/pins differently when dual knobs is enabled. ## What is the new behavior? - Fixes a bug where the knobs would swap A & B when they cross over each other - Fixes the focus behavior so that dual knobs act the same as a single knob range when focusing a knob - Adds the following classes to the host element when `dualKnobs` is enabled: - `range-dual-knobs` - `range-pressed-a` when the knob with name A is pressed - `range-pressed-b` when the knob with name B is pressed - `range-pressed-lower` when the lower knob is pressed - `range-pressed-upper` when the upper knob is pressed - Adds parts for the following: - `knob-handle-a` — The container for the knob with the static `A` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value. - `knob-handle-b` — The container for the knob with the static `B` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value. - `knob-handle-lower` — The container for the knob whose current `value` is `lower` when `dualKnobs` is `true`. The lower and upper parts swap which knob handle they refer to when the knobs cross. - `knob-handle-upper` — The container for the knob whose current `value` is `upper` when `dualKnobs` is `true`. The lower and upper parts swap which knob handle they refer to when the knobs cross. - `pin-a` — The value indicator above the knob with the static `A` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value. - `pin-b` — The value indicator above the knob with the static `B` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value. - `pin-lower` — The value indicator above the knob whose current `value` is `lower` when `dualKnobs` is `true`. The lower and upper parts swap which pin they refer to when the knobs cross. - `pin-upper` — The value indicator above the knob whose current `value` is `upper` when `dualKnobs` is `true`. The lower and upper parts swap which pin they refer to when the knobs cross. - `knob-a` — The visual knob for the static `A` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value. - `knob-b` — The visual knob for the static `B` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value. - `knob-lower` — The visual knob whose current `value` is `lower` when `dualKnobs` is `true`. The lower and upper parts swap which knob they refer to when the knobs cross. - `knob-upper` — The visual knob whose current `value` is `upper` when `dualKnobs` is `true`. The lower and upper parts swap which knob they refer to when the knobs cross. - `activated` — Added to the knob-handle, knob, and pin when the knob is active. Only one set has this part at a time when `dualKnobs` is `true`. - `focused` — Added to the knob-handle, knob, and pin that currently has focus. Only one set has this part at a time when `dualKnobs` is `true`. - `hover` — Added to the knob-handle, knob, and pin when the knob has hover. Only one set has this part at a time when `dualKnobs` is `true`. - `pressed` — Added to the knob-handle, knob, and pin that is currently being pressed to drag. Only one set has this part at a time when `dualKnobs` is `true`. - Adds e2e tests for the following: - customizing label part - customizing bar parts - customizing pin parts - customizing tick parts - customizing knob parts - customizing dual knob a & b parts - customizing dual knob lower & upper parts - verifies that a & b parts stay on the original elements but lower & upper parts swap when the values swap - Adds spec tests for the following: - css classes - value state classes - boolean property classes - pressed state classes - shadow parts - static shadow parts - verifies the shadow parts exist based on the existence of certain range properties - state shadow parts - verifies the shadow parts exist based on the state of the range knob (pressed, focused, activated, hover) ## Does this introduce a breaking change? - [ ] Yes - [x] No --------- Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
This commit is contained in:
17
core/api.txt
17
core/api.txt
@@ -1488,11 +1488,28 @@ ion-range,css-prop,--pin-background,ios
|
||||
ion-range,css-prop,--pin-background,md
|
||||
ion-range,css-prop,--pin-color,ios
|
||||
ion-range,css-prop,--pin-color,md
|
||||
ion-range,part,activated
|
||||
ion-range,part,bar
|
||||
ion-range,part,bar-active
|
||||
ion-range,part,focused
|
||||
ion-range,part,hover
|
||||
ion-range,part,knob
|
||||
ion-range,part,knob-a
|
||||
ion-range,part,knob-b
|
||||
ion-range,part,knob-handle
|
||||
ion-range,part,knob-handle-a
|
||||
ion-range,part,knob-handle-b
|
||||
ion-range,part,knob-handle-lower
|
||||
ion-range,part,knob-handle-upper
|
||||
ion-range,part,knob-lower
|
||||
ion-range,part,knob-upper
|
||||
ion-range,part,label
|
||||
ion-range,part,pin
|
||||
ion-range,part,pin-a
|
||||
ion-range,part,pin-b
|
||||
ion-range,part,pin-lower
|
||||
ion-range,part,pin-upper
|
||||
ion-range,part,pressed
|
||||
ion-range,part,tick
|
||||
ion-range,part,tick-active
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ expect.extend({
|
||||
throw new Error('expected toHaveShadowPart to be called on an element with a shadow root');
|
||||
}
|
||||
|
||||
const shadowPart = received.shadowRoot.querySelector(`[part="${part}"]`);
|
||||
// Use attribute selector with ~= to match space-separated part values
|
||||
// e.g., [part~="knob"] matches elements with part="knob" or part="knob knob-a"
|
||||
const shadowPart = received.shadowRoot.querySelector(`[part~="${part}"]`);
|
||||
const pass = shadowPart !== null;
|
||||
|
||||
const message = `expected ${received.tagName.toLowerCase()} to have shadow part "${part}"`;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export type KnobName = 'A' | 'B' | undefined;
|
||||
|
||||
export type KnobPosition = 'lower' | 'upper' | undefined;
|
||||
|
||||
export type RangeValue = number | { lower: number; upper: number };
|
||||
|
||||
export type PinFormatter = (value: number) => number | string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
|
||||
import { Build, Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
|
||||
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from '@utils/content';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput, isSafeNumber } from '@utils/helpers';
|
||||
@@ -13,6 +13,7 @@ import { roundToMaxDecimalPlaces } from '../../utils/floating-point';
|
||||
|
||||
import type {
|
||||
KnobName,
|
||||
KnobPosition,
|
||||
RangeChangeEventDetail,
|
||||
RangeKnobMoveEndEventDetail,
|
||||
RangeKnobMoveStartEventDetail,
|
||||
@@ -29,13 +30,30 @@ import type {
|
||||
* @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.
|
||||
*
|
||||
* @part label - The label text describing the range.
|
||||
* @part tick - An inactive tick mark.
|
||||
* @part tick-active - An active tick mark.
|
||||
* @part pin - The counter that appears above a knob.
|
||||
* @part knob - The handle that is used to drag the range.
|
||||
* @part bar - The inactive part of the bar.
|
||||
* @part bar-active - The active part of the bar.
|
||||
* @part label - The label text describing the range.
|
||||
* @part knob-handle - The container that wraps the knob and handles drag interactions.
|
||||
* @part knob-handle-a - The container for the knob with the static `A` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
|
||||
* @part knob-handle-b - The container for the knob with the static `B` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
|
||||
* @part knob-handle-lower - The container for the knob whose current `value` is `lower` when `dualKnobs` is `true`. The lower and upper parts swap which knob handle they refer to when the knobs cross.
|
||||
* @part knob-handle-upper - The container for the knob whose current `value` is `upper` when `dualKnobs` is `true`. The lower and upper parts swap which knob handle they refer to when the knobs cross.
|
||||
* @part pin - The value indicator displayed above a knob.
|
||||
* @part pin-a - The value indicator above the knob with the static `A` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
|
||||
* @part pin-b - The value indicator above the knob with the static `B` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
|
||||
* @part pin-lower - The value indicator above the knob whose current `value` is `lower` when `dualKnobs` is `true`. The lower and upper parts swap which pin they refer to when the knobs cross.
|
||||
* @part pin-upper - The value indicator above the knob whose current `value` is `upper` when `dualKnobs` is `true`. The lower and upper parts swap which pin they refer to when the knobs cross.
|
||||
* @part knob - The visual knob element on the range track.
|
||||
* @part knob-a - The visual knob for the static `A` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
|
||||
* @part knob-b - The visual knob for the static `B` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
|
||||
* @part knob-lower - The visual knob whose current `value` is `lower` when `dualKnobs` is `true`. The lower and upper parts swap which knob they refer to when the knobs cross.
|
||||
* @part knob-upper - The visual knob whose current `value` is `upper` when `dualKnobs` is `true`. The lower and upper parts swap which knob they refer to when the knobs cross.
|
||||
* @part activated - Added to the knob-handle, knob, and pin when the knob is active. Only one set has this part at a time when `dualKnobs` is `true`.
|
||||
* @part focused - Added to the knob-handle, knob, and pin that currently has focus. Only one set has this part at a time when `dualKnobs` is `true`.
|
||||
* @part hover - Added to the knob-handle, knob, and pin when the knob has hover. Only one set has this part at a time when `dualKnobs` is `true`.
|
||||
* @part pressed - Added to the knob-handle, knob, and pin that is currently being pressed to drag. Only one set has this part at a time when `dualKnobs` is `true`.
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-range',
|
||||
@@ -57,11 +75,27 @@ export class Range implements ComponentInterface {
|
||||
private contentEl: HTMLElement | null = null;
|
||||
private initialContentScrollY = true;
|
||||
private originalIonInput?: EventEmitter<RangeChangeEventDetail>;
|
||||
/**
|
||||
* Used to avoid setting the focused state on click or tap. The focused
|
||||
* state is only set when the focus comes from the keyboard (e.g. Tab).
|
||||
* This is set to true on pointer down (mouse/touch).
|
||||
*/
|
||||
private focusFromPointer = false;
|
||||
/**
|
||||
* Observes class changes on the knob handles to keep the activatedKnob
|
||||
* state in sync with the ion-activated class. This is necessary to
|
||||
* determine which knob the user is dragging when using dual knobs and
|
||||
* apply the activated part correctly.
|
||||
*/
|
||||
private activatedObserver?: MutationObserver;
|
||||
|
||||
@Element() el!: HTMLIonRangeElement;
|
||||
|
||||
@State() private ratioA = 0;
|
||||
@State() private ratioB = 0;
|
||||
@State() private activatedKnob: KnobName;
|
||||
@State() private focusedKnob: KnobName;
|
||||
@State() private hoveredKnob: KnobName;
|
||||
@State() private pressedKnob: KnobName;
|
||||
|
||||
/**
|
||||
@@ -324,6 +358,34 @@ export class Range implements ComponentInterface {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Observes the knob handles for the ion-activated class and syncs
|
||||
* activatedKnob so the activated part is correctly set on the handle,
|
||||
* knob, and pin.
|
||||
*/
|
||||
private setupActivatedObserver = () => {
|
||||
const knobHandleA = this.el.shadowRoot!.querySelector('.range-knob-handle-a');
|
||||
const knobHandleB = this.el.shadowRoot!.querySelector('.range-knob-handle-b');
|
||||
|
||||
const syncActivated = () => {
|
||||
this.activatedKnob = (knobHandleA as HTMLElement)?.classList.contains('ion-activated')
|
||||
? 'A'
|
||||
: (knobHandleB as HTMLElement)?.classList.contains('ion-activated')
|
||||
? 'B'
|
||||
: undefined;
|
||||
};
|
||||
|
||||
if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
|
||||
this.activatedObserver = new MutationObserver(syncActivated);
|
||||
this.activatedObserver.observe(this.el.shadowRoot!, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
syncActivated();
|
||||
};
|
||||
|
||||
componentWillLoad() {
|
||||
/**
|
||||
* If user has custom ID set then we should
|
||||
@@ -345,6 +407,7 @@ export class Range implements ComponentInterface {
|
||||
this.originalIonInput = this.ionInput;
|
||||
this.setupGesture();
|
||||
this.updateRatio();
|
||||
this.setupActivatedObserver();
|
||||
this.didLoad = true;
|
||||
}
|
||||
|
||||
@@ -362,6 +425,7 @@ export class Range implements ComponentInterface {
|
||||
*/
|
||||
if (this.didLoad) {
|
||||
this.setupGesture();
|
||||
this.setupActivatedObserver();
|
||||
}
|
||||
|
||||
const ionContent = findClosestIonContent(this.el);
|
||||
@@ -373,6 +437,10 @@ export class Range implements ComponentInterface {
|
||||
this.gesture.destroy();
|
||||
this.gesture = undefined;
|
||||
}
|
||||
if (this.activatedObserver) {
|
||||
this.activatedObserver.disconnect();
|
||||
this.activatedObserver = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeyboard = (knob: KnobName, isIncrease: boolean) => {
|
||||
@@ -467,7 +535,7 @@ export class Range implements ComponentInterface {
|
||||
* started dragging the knob.
|
||||
*
|
||||
* This is necessary to determine which knob the user is dragging,
|
||||
* especially when it's a dual knob.
|
||||
* especially when using dual knobs.
|
||||
* Plus, it determines when to apply certain styles.
|
||||
*
|
||||
* This only needs to be done once since the knob won't change
|
||||
@@ -496,7 +564,7 @@ export class Range implements ComponentInterface {
|
||||
* dragged the knob. They just tapped on the bar.
|
||||
*
|
||||
* This is necessary to determine which knob the user is changing,
|
||||
* especially when it's a dual knob.
|
||||
* especially when using dual knobs.
|
||||
* Plus, it determines when to apply certain styles.
|
||||
*/
|
||||
if (this.pressedKnob === undefined) {
|
||||
@@ -515,6 +583,7 @@ export class Range implements ComponentInterface {
|
||||
|
||||
// update the active knob's position
|
||||
this.update(currentX);
|
||||
|
||||
/**
|
||||
* Reset the pressed knob to undefined since the user
|
||||
* may start dragging a different knob in the next gesture event.
|
||||
@@ -559,8 +628,6 @@ export class Range implements ComponentInterface {
|
||||
ratio = 1 - ratio;
|
||||
}
|
||||
this.pressedKnob = !this.dualKnobs || Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio) ? 'A' : 'B';
|
||||
|
||||
this.setFocus(this.pressedKnob);
|
||||
}
|
||||
|
||||
private get valA() {
|
||||
@@ -592,9 +659,26 @@ export class Range implements ComponentInterface {
|
||||
private updateRatio() {
|
||||
const value = this.getValue() as any;
|
||||
const { min, max } = this;
|
||||
|
||||
/**
|
||||
* For dual knobs, value gives lower/upper but not which is A vs B.
|
||||
* Assign (lowerRatio, upperRatio) to (ratioA, ratioB) in the way that
|
||||
* minimizes change from the current ratios so the knobs don't swap.
|
||||
*/
|
||||
if (this.dualKnobs) {
|
||||
this.ratioA = valueToRatio(value.lower, min, max);
|
||||
this.ratioB = valueToRatio(value.upper, min, max);
|
||||
const lowerRatio = valueToRatio(value.lower, min, max);
|
||||
const upperRatio = valueToRatio(value.upper, min, max);
|
||||
|
||||
if (
|
||||
Math.abs(this.ratioA - lowerRatio) + Math.abs(this.ratioB - upperRatio) <=
|
||||
Math.abs(this.ratioA - upperRatio) + Math.abs(this.ratioB - lowerRatio)
|
||||
) {
|
||||
this.ratioA = lowerRatio;
|
||||
this.ratioB = upperRatio;
|
||||
} else {
|
||||
this.ratioA = upperRatio;
|
||||
this.ratioB = lowerRatio;
|
||||
}
|
||||
} else {
|
||||
this.ratioA = valueToRatio(value, min, max);
|
||||
}
|
||||
@@ -614,20 +698,10 @@ export class Range implements ComponentInterface {
|
||||
this.noUpdate = false;
|
||||
}
|
||||
|
||||
private setFocus(knob: KnobName) {
|
||||
if (this.el.shadowRoot) {
|
||||
const knobEl = this.el.shadowRoot.querySelector(knob === 'A' ? '.range-knob-a' : '.range-knob-b') as
|
||||
| HTMLElement
|
||||
| undefined;
|
||||
if (knobEl) {
|
||||
knobEl.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onBlur = () => {
|
||||
if (this.hasFocus) {
|
||||
this.hasFocus = false;
|
||||
this.focusedKnob = undefined;
|
||||
this.ionBlur.emit();
|
||||
}
|
||||
};
|
||||
@@ -640,24 +714,20 @@ export class Range implements ComponentInterface {
|
||||
};
|
||||
|
||||
private onKnobFocus = (knob: KnobName) => {
|
||||
// Clicking focuses the range which is needed for the keyboard,
|
||||
// but we only want to add the ion-focused class when focused via Tab.
|
||||
if (!this.focusFromPointer) {
|
||||
this.focusedKnob = knob;
|
||||
} else {
|
||||
this.focusFromPointer = false;
|
||||
this.focusedKnob = undefined;
|
||||
}
|
||||
|
||||
// If the knob was not already focused, emit the focus event
|
||||
if (!this.hasFocus) {
|
||||
this.hasFocus = true;
|
||||
this.ionFocus.emit();
|
||||
}
|
||||
|
||||
// Manually manage ion-focused class for dual knobs
|
||||
if (this.dualKnobs && this.el.shadowRoot) {
|
||||
const knobA = this.el.shadowRoot.querySelector('.range-knob-a');
|
||||
const knobB = this.el.shadowRoot.querySelector('.range-knob-b');
|
||||
|
||||
// Remove ion-focused from both knobs first
|
||||
knobA?.classList.remove('ion-focused');
|
||||
knobB?.classList.remove('ion-focused');
|
||||
|
||||
// Add ion-focused only to the focused knob
|
||||
const focusedKnobEl = knob === 'A' ? knobA : knobB;
|
||||
focusedKnobEl?.classList.add('ion-focused');
|
||||
}
|
||||
};
|
||||
|
||||
private onKnobBlur = () => {
|
||||
@@ -670,20 +740,21 @@ export class Range implements ComponentInterface {
|
||||
if (!isStillFocusedOnKnob) {
|
||||
if (this.hasFocus) {
|
||||
this.hasFocus = false;
|
||||
this.focusedKnob = undefined;
|
||||
this.ionBlur.emit();
|
||||
}
|
||||
|
||||
// Remove ion-focused from both knobs when focus leaves the range
|
||||
if (this.dualKnobs && this.el.shadowRoot) {
|
||||
const knobA = this.el.shadowRoot.querySelector('.range-knob-a');
|
||||
const knobB = this.el.shadowRoot.querySelector('.range-knob-b');
|
||||
knobA?.classList.remove('ion-focused');
|
||||
knobB?.classList.remove('ion-focused');
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
private onKnobMouseEnter = (knob: KnobName) => {
|
||||
this.hoveredKnob = knob;
|
||||
};
|
||||
|
||||
private onKnobMouseLeave = () => {
|
||||
this.hoveredKnob = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if content was passed to the "start" slot
|
||||
*/
|
||||
@@ -708,6 +779,9 @@ export class Range implements ComponentInterface {
|
||||
max,
|
||||
step,
|
||||
handleKeyboard,
|
||||
activatedKnob,
|
||||
focusedKnob,
|
||||
hoveredKnob,
|
||||
pressedKnob,
|
||||
disabled,
|
||||
pin,
|
||||
@@ -790,6 +864,9 @@ export class Range implements ComponentInterface {
|
||||
<div
|
||||
class="range-slider"
|
||||
ref={(rangeEl) => (this.rangeSlider = rangeEl)}
|
||||
onPointerDown={() => {
|
||||
this.focusFromPointer = true;
|
||||
}}
|
||||
/**
|
||||
* Since the gesture has a threshold, the value
|
||||
* won't change until the user has dragged past
|
||||
@@ -802,6 +879,8 @@ export class Range implements ComponentInterface {
|
||||
* we need to listen for the "pointerUp" event.
|
||||
*/
|
||||
onPointerUp={(ev: PointerEvent) => {
|
||||
this.focusFromPointer = false;
|
||||
|
||||
/**
|
||||
* If the user drags the knob on the web
|
||||
* version (does not occur on mobile),
|
||||
@@ -848,6 +927,11 @@ export class Range implements ComponentInterface {
|
||||
|
||||
{renderKnob(rtl, {
|
||||
knob: 'A',
|
||||
position: getKnobPosition('A', this.ratioA, this.ratioB, this.dualKnobs),
|
||||
dualKnobs: this.dualKnobs,
|
||||
activated: activatedKnob === 'A',
|
||||
focused: focusedKnob === 'A',
|
||||
hovered: hoveredKnob === 'A',
|
||||
pressed: pressedKnob === 'A',
|
||||
value: this.valA,
|
||||
ratio: this.ratioA,
|
||||
@@ -860,11 +944,18 @@ export class Range implements ComponentInterface {
|
||||
inheritedAttributes,
|
||||
onKnobFocus: this.onKnobFocus,
|
||||
onKnobBlur: this.onKnobBlur,
|
||||
onKnobMouseEnter: this.onKnobMouseEnter,
|
||||
onKnobMouseLeave: this.onKnobMouseLeave,
|
||||
})}
|
||||
|
||||
{this.dualKnobs &&
|
||||
renderKnob(rtl, {
|
||||
knob: 'B',
|
||||
position: getKnobPosition('B', this.ratioA, this.ratioB, this.dualKnobs),
|
||||
dualKnobs: this.dualKnobs,
|
||||
activated: activatedKnob === 'B',
|
||||
focused: focusedKnob === 'B',
|
||||
hovered: hoveredKnob === 'B',
|
||||
pressed: pressedKnob === 'B',
|
||||
value: this.valB,
|
||||
ratio: this.ratioB,
|
||||
@@ -877,6 +968,8 @@ export class Range implements ComponentInterface {
|
||||
inheritedAttributes,
|
||||
onKnobFocus: this.onKnobFocus,
|
||||
onKnobBlur: this.onKnobBlur,
|
||||
onKnobMouseEnter: this.onKnobMouseEnter,
|
||||
onKnobMouseLeave: this.onKnobMouseLeave,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
@@ -906,6 +999,14 @@ export class Range implements ComponentInterface {
|
||||
|
||||
const mode = getIonMode(this);
|
||||
|
||||
/**
|
||||
* Determine the name and position of the pressed knob to apply
|
||||
* Host classes for styling.
|
||||
*/
|
||||
const pressedKnobName = dualKnobs ? pressedKnob?.toLowerCase() : undefined;
|
||||
const pressedKnobPosition =
|
||||
dualKnobs && pressedKnob ? getKnobPosition(pressedKnob, this.ratioA, this.ratioB, dualKnobs) : undefined;
|
||||
|
||||
/**
|
||||
* Determine if any knob is at the min or max value to
|
||||
* apply Host classes for styling.
|
||||
@@ -924,7 +1025,10 @@ export class Range implements ComponentInterface {
|
||||
[mode]: true,
|
||||
'in-item': inItem,
|
||||
'range-disabled': disabled,
|
||||
'range-dual-knobs': dualKnobs,
|
||||
'range-pressed': pressedKnob !== undefined,
|
||||
[`range-pressed-${pressedKnobName}`]: pressedKnob !== undefined && pressedKnobName !== undefined,
|
||||
[`range-pressed-${pressedKnobPosition}`]: pressedKnob !== undefined && pressedKnobPosition !== undefined,
|
||||
'range-has-pin': pin,
|
||||
[`range-label-placement-${labelPlacement}`]: true,
|
||||
'range-item-start-adjustment': needsStartAdjustment,
|
||||
@@ -956,29 +1060,41 @@ export class Range implements ComponentInterface {
|
||||
|
||||
interface RangeKnob {
|
||||
knob: KnobName;
|
||||
position: KnobPosition;
|
||||
dualKnobs: boolean;
|
||||
value: number;
|
||||
ratio: number;
|
||||
min: number;
|
||||
max: number;
|
||||
disabled: boolean;
|
||||
pressed: boolean;
|
||||
focused: boolean;
|
||||
hovered: boolean;
|
||||
activated: boolean;
|
||||
pin: boolean;
|
||||
pinFormatter: PinFormatter;
|
||||
inheritedAttributes: Attributes;
|
||||
handleKeyboard: (name: KnobName, isIncrease: boolean) => void;
|
||||
onKnobFocus: (knob: KnobName) => void;
|
||||
onKnobBlur: () => void;
|
||||
onKnobMouseEnter: (knob: KnobName) => void;
|
||||
onKnobMouseLeave: () => void;
|
||||
}
|
||||
|
||||
const renderKnob = (
|
||||
rtl: boolean,
|
||||
{
|
||||
knob,
|
||||
position,
|
||||
dualKnobs,
|
||||
value,
|
||||
ratio,
|
||||
min,
|
||||
max,
|
||||
disabled,
|
||||
activated,
|
||||
focused,
|
||||
hovered,
|
||||
pressed,
|
||||
pin,
|
||||
handleKeyboard,
|
||||
@@ -986,6 +1102,8 @@ const renderKnob = (
|
||||
inheritedAttributes,
|
||||
onKnobFocus,
|
||||
onKnobBlur,
|
||||
onKnobMouseEnter,
|
||||
onKnobMouseLeave,
|
||||
}: RangeKnob
|
||||
) => {
|
||||
const start = rtl ? 'right' : 'left';
|
||||
@@ -1017,16 +1135,32 @@ const renderKnob = (
|
||||
}}
|
||||
onFocus={() => onKnobFocus(knob)}
|
||||
onBlur={onKnobBlur}
|
||||
onMouseEnter={() => onKnobMouseEnter(knob)}
|
||||
onMouseLeave={onKnobMouseLeave}
|
||||
class={{
|
||||
'range-knob-handle': true,
|
||||
'range-knob-a': knob === 'A',
|
||||
'range-knob-b': knob === 'B',
|
||||
'range-knob-handle-a': knob === 'A',
|
||||
'range-knob-handle-b': knob === 'B',
|
||||
'range-knob-pressed': pressed,
|
||||
'range-knob-min': value === min,
|
||||
'range-knob-max': value === max,
|
||||
'ion-activatable': true,
|
||||
'ion-focusable': true,
|
||||
'ion-focused': focused,
|
||||
}}
|
||||
part={[
|
||||
'knob-handle',
|
||||
dualKnobs && knob === 'A' && 'knob-handle-a',
|
||||
dualKnobs && knob === 'B' && 'knob-handle-b',
|
||||
dualKnobs && position === 'lower' && 'knob-handle-lower',
|
||||
dualKnobs && position === 'upper' && 'knob-handle-upper',
|
||||
pressed && 'pressed',
|
||||
focused && 'focused',
|
||||
hovered && 'hover',
|
||||
activated && 'activated',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
style={knobStyle()}
|
||||
role="slider"
|
||||
tabindex={disabled ? -1 : 0}
|
||||
@@ -1038,15 +1172,72 @@ const renderKnob = (
|
||||
aria-valuenow={value}
|
||||
>
|
||||
{pin && (
|
||||
<div class="range-pin" role="presentation" part="pin">
|
||||
<div
|
||||
class="range-pin"
|
||||
role="presentation"
|
||||
part={[
|
||||
'pin',
|
||||
dualKnobs && knob === 'A' && 'pin-a',
|
||||
dualKnobs && knob === 'B' && 'pin-b',
|
||||
dualKnobs && position === 'lower' && 'pin-lower',
|
||||
dualKnobs && position === 'upper' && 'pin-upper',
|
||||
pressed && 'pressed',
|
||||
focused && 'focused',
|
||||
hovered && 'hover',
|
||||
activated && 'activated',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{pinFormatter(value)}
|
||||
</div>
|
||||
)}
|
||||
<div class="range-knob" role="presentation" part="knob" />
|
||||
<div
|
||||
class="range-knob"
|
||||
role="presentation"
|
||||
part={[
|
||||
'knob',
|
||||
dualKnobs && knob === 'A' && 'knob-a',
|
||||
dualKnobs && knob === 'B' && 'knob-b',
|
||||
dualKnobs && position === 'lower' && 'knob-lower',
|
||||
dualKnobs && position === 'upper' && 'knob-upper',
|
||||
pressed && 'pressed',
|
||||
focused && 'focused',
|
||||
hovered && 'hover',
|
||||
activated && 'activated',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the given knob is at the lower or upper position based
|
||||
* on current ratios for the given knob.
|
||||
*
|
||||
* When both knobs have the same ratio, we only want one "lower" and one
|
||||
* "upper" position so that the `lower` and `upper` parts are not applied to
|
||||
* the same knob. In that case, we treat knob "A" as the lower position and
|
||||
* knob "B" as the upper position.
|
||||
*/
|
||||
const getKnobPosition = (knob: 'A' | 'B', ratioA: number, ratioB: number, dualKnobs: boolean): 'lower' | 'upper' => {
|
||||
if (!dualKnobs) {
|
||||
return 'lower';
|
||||
}
|
||||
|
||||
if (ratioA === ratioB) {
|
||||
return knob === 'A' ? 'lower' : 'upper';
|
||||
}
|
||||
|
||||
if (knob === 'A') {
|
||||
return ratioA < ratioB ? 'lower' : 'upper';
|
||||
}
|
||||
|
||||
return ratioB < ratioA ? 'lower' : 'upper';
|
||||
};
|
||||
|
||||
const ratioToValue = (ratio: number, min: number, max: number, step: number): number => {
|
||||
let value = (max - min) * ratio;
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ describe('range: dual knobs focus management', () => {
|
||||
await page.waitForChanges();
|
||||
|
||||
// Get the knob elements
|
||||
const knobA = range!.shadowRoot!.querySelector('.range-knob-a') as HTMLElement;
|
||||
const knobB = range!.shadowRoot!.querySelector('.range-knob-b') as HTMLElement;
|
||||
const knobA = range!.shadowRoot!.querySelector('.range-knob-handle-a') as HTMLElement;
|
||||
const knobB = range!.shadowRoot!.querySelector('.range-knob-handle-b') as HTMLElement;
|
||||
|
||||
expect(knobA).not.toBeNull();
|
||||
expect(knobB).not.toBeNull();
|
||||
@@ -41,8 +41,8 @@ describe('range: dual knobs focus management', () => {
|
||||
const range = page.body.querySelector('ion-range');
|
||||
await page.waitForChanges();
|
||||
|
||||
const knobA = range!.shadowRoot!.querySelector('.range-knob-a') as HTMLElement;
|
||||
const knobB = range!.shadowRoot!.querySelector('.range-knob-b') as HTMLElement;
|
||||
const knobA = range!.shadowRoot!.querySelector('.range-knob-handle-a') as HTMLElement;
|
||||
const knobB = range!.shadowRoot!.querySelector('.range-knob-handle-b') as HTMLElement;
|
||||
|
||||
// Focus knob A
|
||||
knobA.dispatchEvent(new Event('focus'));
|
||||
@@ -73,8 +73,8 @@ describe('range: dual knobs focus management', () => {
|
||||
const range = page.body.querySelector('ion-range');
|
||||
await page.waitForChanges();
|
||||
|
||||
const knobA = range!.shadowRoot!.querySelector('.range-knob-a') as HTMLElement;
|
||||
const knobB = range!.shadowRoot!.querySelector('.range-knob-b') as HTMLElement;
|
||||
const knobA = range!.shadowRoot!.querySelector('.range-knob-handle-a') as HTMLElement;
|
||||
const knobB = range!.shadowRoot!.querySelector('.range-knob-handle-b') as HTMLElement;
|
||||
|
||||
// Focus knob A
|
||||
knobA.dispatchEvent(new Event('focus'));
|
||||
@@ -112,8 +112,8 @@ describe('range: dual knobs focus management', () => {
|
||||
focusEventFiredCount++;
|
||||
});
|
||||
|
||||
const knobA = range.shadowRoot!.querySelector('.range-knob-a') as HTMLElement;
|
||||
const knobB = range.shadowRoot!.querySelector('.range-knob-b') as HTMLElement;
|
||||
const knobA = range.shadowRoot!.querySelector('.range-knob-handle-a') as HTMLElement;
|
||||
const knobB = range.shadowRoot!.querySelector('.range-knob-handle-b') as HTMLElement;
|
||||
|
||||
// Focus knob A
|
||||
knobA.dispatchEvent(new Event('focus'));
|
||||
@@ -140,7 +140,7 @@ describe('range: dual knobs focus management', () => {
|
||||
blurEventFired = true;
|
||||
});
|
||||
|
||||
const knobA = range.shadowRoot!.querySelector('.range-knob-a') as HTMLElement;
|
||||
const knobA = range.shadowRoot!.querySelector('.range-knob-handle-a') as HTMLElement;
|
||||
|
||||
// Focus and then blur knob A
|
||||
knobA.dispatchEvent(new Event('focus'));
|
||||
@@ -173,8 +173,8 @@ describe('range: dual knobs focus management', () => {
|
||||
const beforeButton = page.body.querySelector('#before') as HTMLElement;
|
||||
await page.waitForChanges();
|
||||
|
||||
const knobA = range.shadowRoot!.querySelector('.range-knob-a') as HTMLElement;
|
||||
const knobB = range.shadowRoot!.querySelector('.range-knob-b') as HTMLElement;
|
||||
const knobA = range.shadowRoot!.querySelector('.range-knob-handle-a') as HTMLElement;
|
||||
const knobB = range.shadowRoot!.querySelector('.range-knob-handle-b') as HTMLElement;
|
||||
|
||||
// Start with focus on element before the range
|
||||
beforeButton.focus();
|
||||
|
||||
@@ -14,6 +14,20 @@
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
<style>
|
||||
h2 {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
|
||||
color: #6f7378;
|
||||
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1st Custom Range: All Parts
|
||||
* -----------------------------------
|
||||
*/
|
||||
.range-part::part(bar) {
|
||||
background: red;
|
||||
}
|
||||
@@ -51,6 +65,271 @@
|
||||
.md.range-part::part(pin)::before {
|
||||
background: orange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared Custom Knob Styles
|
||||
* -----------------------------------
|
||||
*/
|
||||
.custom-knobs-range {
|
||||
--color-blue-light: #3b82f6;
|
||||
--color-blue-light-rgb: 59, 130, 246;
|
||||
|
||||
--color-blue: var(--ion-color-primary, #0054e9);
|
||||
--color-blue-rgb: var(--ion-color-primary-rgb, 0, 84, 233);
|
||||
|
||||
--color-blue-dark: #1e3a8a;
|
||||
--color-blue-dark-rgb: 30, 58, 138;
|
||||
|
||||
--color-purple: #8b5cf6;
|
||||
--color-green: #10b981;
|
||||
}
|
||||
|
||||
.custom-knobs-range::part(knob-handle) {
|
||||
--custom-knob-width: 50px;
|
||||
--custom-knob-height: 25px;
|
||||
|
||||
width: var(--custom-knob-width);
|
||||
height: var(--custom-knob-height);
|
||||
|
||||
/* Center vertically */
|
||||
top: calc((var(--height) - var(--custom-knob-height)) / 2);
|
||||
|
||||
/* Center horizontally */
|
||||
margin-inline-start: calc(0px - var(--custom-knob-width) / 2);
|
||||
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.custom-knobs-range::part(knob) {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
inset: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
/* MD scales smaller by default, make iOS scale to match */
|
||||
transform: scale(0.67);
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(3, 60, 89, 0.5);
|
||||
}
|
||||
|
||||
/* Hover/focus indicator */
|
||||
.custom-knobs-range::part(knob)::before {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
/* Override the default hover/focus indicator */
|
||||
.custom-knobs-range::part(knob activated)::before,
|
||||
.custom-knobs-range::part(knob hover)::before,
|
||||
.custom-knobs-range::part(knob pressed)::before,
|
||||
.custom-knobs-range::part(knob focused)::before {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* Displayed knob values */
|
||||
.custom-knobs-range .knob-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
border: 1px solid #000;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* iOS offset is larger than MD due to not having a bottom arrow */
|
||||
.ios.custom-knobs-range {
|
||||
--pin-offset: 8px;
|
||||
}
|
||||
|
||||
.md.custom-knobs-range {
|
||||
--pin-offset: 4px;
|
||||
}
|
||||
|
||||
.ios.custom-knobs-range {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.ios.custom-knobs-range::part(pin) {
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-blue);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Move the pin up so it sits above the taller custom knob when pressed/focused */
|
||||
.custom-knobs-range::part(pin pressed),
|
||||
.custom-knobs-range::part(pin focused) {
|
||||
transform: translate3d(0, calc(-100% - (var(--custom-knob-height) / 2) + var(--pin-offset)), 0) scale(1);
|
||||
}
|
||||
|
||||
.custom-knobs-range [slot='start'] {
|
||||
margin-inline-end: 36px;
|
||||
}
|
||||
|
||||
.custom-knobs-range [slot='end'] {
|
||||
margin-inline-start: 36px;
|
||||
}
|
||||
|
||||
/**
|
||||
* 2nd Custom Range: Single Knob
|
||||
* -----------------------------------
|
||||
*/
|
||||
|
||||
/* Hovered knob */
|
||||
#single-knob-range::part(knob hover) {
|
||||
background: rgba(var(--color-blue-rgb), 0.5);
|
||||
}
|
||||
|
||||
/* Pressed knob */
|
||||
#single-knob-range::part(knob pressed) {
|
||||
background: var(--color-blue);
|
||||
}
|
||||
|
||||
/* Activated knob */
|
||||
#single-knob-range::part(knob activated) {
|
||||
background: var(--color-purple);
|
||||
}
|
||||
|
||||
/**
|
||||
* 3rd Custom Range: Dual Knobs
|
||||
* Knobs Lower & Upper
|
||||
* -----------------------------------
|
||||
*/
|
||||
|
||||
/* Style the start slot knob value when the lower knob is pressed */
|
||||
#lower-upper-dual-knobs-range.range-pressed-lower [slot='start'] {
|
||||
background: var(--color-blue-light);
|
||||
border-color: var(--color-blue-light);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Style the end slot knob value when the upper knob is pressed */
|
||||
#lower-upper-dual-knobs-range.range-pressed-upper [slot='end'] {
|
||||
background: var(--color-blue-dark);
|
||||
border-color: var(--color-blue-dark);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Knob lower */
|
||||
#lower-upper-dual-knobs-range::part(knob-lower) {
|
||||
border-radius: 50% 0 0 50%;
|
||||
}
|
||||
|
||||
/* Knob upper */
|
||||
#lower-upper-dual-knobs-range::part(knob-upper) {
|
||||
border-radius: 0 50% 50% 0;
|
||||
}
|
||||
|
||||
/* Hovered knob lower */
|
||||
#lower-upper-dual-knobs-range::part(knob-lower hover) {
|
||||
background: rgba(var(--color-blue-light-rgb), 0.5);
|
||||
}
|
||||
|
||||
/* Pressed knob lower */
|
||||
#lower-upper-dual-knobs-range::part(knob-lower pressed) {
|
||||
background: var(--color-blue-light);
|
||||
}
|
||||
|
||||
/* Activated knob lower */
|
||||
#lower-upper-dual-knobs-range::part(knob-lower activated) {
|
||||
background: var(--color-green);
|
||||
}
|
||||
|
||||
/* Hovered knob upper */
|
||||
#lower-upper-dual-knobs-range::part(knob-upper hover) {
|
||||
background: rgba(var(--color-blue-dark-rgb), 0.5);
|
||||
}
|
||||
|
||||
/* Pressed knob upper */
|
||||
#lower-upper-dual-knobs-range::part(knob-upper pressed) {
|
||||
background: var(--color-blue-dark);
|
||||
}
|
||||
|
||||
/* Activated knob upper */
|
||||
#lower-upper-dual-knobs-range::part(knob-upper activated) {
|
||||
background: var(--color-purple);
|
||||
}
|
||||
|
||||
/* Pin lower */
|
||||
#lower-upper-dual-knobs-range.range-dual-knobs::part(pin-lower),
|
||||
#lower-upper-dual-knobs-range.range-dual-knobs::part(pin-lower):before {
|
||||
background: var(--color-blue-light);
|
||||
}
|
||||
|
||||
/* Pin upper */
|
||||
#lower-upper-dual-knobs-range.range-dual-knobs::part(pin-upper),
|
||||
#lower-upper-dual-knobs-range.range-dual-knobs::part(pin-upper):before {
|
||||
background: var(--color-blue-dark);
|
||||
}
|
||||
|
||||
/**
|
||||
* 4th Custom Range: Dual Knobs
|
||||
* Knobs A & B
|
||||
* -----------------------------------
|
||||
*/
|
||||
|
||||
/* Style the start slot knob value when the A knob is pressed */
|
||||
#a-b-dual-knobs-range.range-pressed-a [slot='start'] {
|
||||
background: var(--color-blue-light);
|
||||
border-color: var(--color-blue-light);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Style the end slot knob value when the B knob is pressed */
|
||||
#a-b-dual-knobs-range.range-pressed-b [slot='end'] {
|
||||
background: var(--color-blue-dark);
|
||||
border-color: var(--color-blue-dark);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Hovered knob A */
|
||||
#a-b-dual-knobs-range::part(knob-a hover) {
|
||||
background: rgba(var(--color-blue-light-rgb), 0.5);
|
||||
}
|
||||
|
||||
/* Pressed knob A */
|
||||
#a-b-dual-knobs-range::part(knob-a pressed) {
|
||||
background: var(--color-blue-light);
|
||||
}
|
||||
|
||||
/* Activated knob A */
|
||||
#a-b-dual-knobs-range::part(knob-a activated) {
|
||||
background: var(--color-green);
|
||||
}
|
||||
|
||||
/* Hovered knob B */
|
||||
#a-b-dual-knobs-range::part(knob-b hover) {
|
||||
background: rgba(var(--color-blue-dark-rgb), 0.5);
|
||||
}
|
||||
|
||||
/* Pressed knob B */
|
||||
#a-b-dual-knobs-range::part(knob-b pressed) {
|
||||
background: var(--color-blue-dark);
|
||||
}
|
||||
|
||||
/* Activated knob B */
|
||||
#a-b-dual-knobs-range::part(knob-b activated) {
|
||||
background: var(--color-purple);
|
||||
}
|
||||
|
||||
/* Pin A */
|
||||
#a-b-dual-knobs-range.range-dual-knobs::part(pin-a),
|
||||
#a-b-dual-knobs-range.range-dual-knobs::part(pin-a):before {
|
||||
background: var(--color-blue-light);
|
||||
}
|
||||
|
||||
/* Pin B */
|
||||
#a-b-dual-knobs-range.range-dual-knobs::part(pin-b),
|
||||
#a-b-dual-knobs-range.range-dual-knobs::part(pin-b):before {
|
||||
background: var(--color-blue-dark);
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<ion-app>
|
||||
@@ -61,6 +340,8 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<!-- 1st Custom Range -->
|
||||
<h2>Custom Range</h2>
|
||||
<ion-range
|
||||
class="range-part"
|
||||
min="-200"
|
||||
@@ -71,6 +352,55 @@
|
||||
dual-knobs="true"
|
||||
aria-label="Custom Range"
|
||||
></ion-range>
|
||||
|
||||
<!-- 2nd Custom Range: Single Knob -->
|
||||
<h2>Custom Range: Single Knob</h2>
|
||||
<ion-range
|
||||
id="single-knob-range"
|
||||
class="custom-knobs-range"
|
||||
step="5"
|
||||
snaps="true"
|
||||
pin="true"
|
||||
value="50"
|
||||
aria-label="Custom Range"
|
||||
>
|
||||
<span slot="start">
|
||||
<ion-icon name="volume-off" size="large" color="primary"></ion-icon>
|
||||
</span>
|
||||
<span slot="end">
|
||||
<ion-icon name="volume-high" size="large" color="primary"></ion-icon>
|
||||
</span>
|
||||
</ion-range>
|
||||
|
||||
<!-- 3rd Custom Range: Dual Knobs with Knob Lower & Upper -->
|
||||
<h2>Custom Range: Dual Knobs with Knob Lower & Upper</h2>
|
||||
<ion-range
|
||||
id="lower-upper-dual-knobs-range"
|
||||
class="custom-knobs-range range-lower-upper-knobs"
|
||||
step="5"
|
||||
snaps="true"
|
||||
pin="true"
|
||||
dual-knobs="true"
|
||||
aria-label="Custom Lower & Upper Knobs Range"
|
||||
>
|
||||
<div slot="start" class="knob-value"></div>
|
||||
<div slot="end" class="knob-value"></div>
|
||||
</ion-range>
|
||||
|
||||
<!-- 4th Custom Range: Dual Knobs with Knob A & B -->
|
||||
<h2>Custom Range: Dual Knobs with Knob A & B</h2>
|
||||
<ion-range
|
||||
id="a-b-dual-knobs-range"
|
||||
class="custom-knobs-range"
|
||||
step="5"
|
||||
snaps="true"
|
||||
pin="true"
|
||||
dual-knobs="true"
|
||||
aria-label="Custom Dual Knobs Range"
|
||||
>
|
||||
<div slot="start" class="knob-value"></div>
|
||||
<div slot="end" class="knob-value"></div>
|
||||
</ion-range>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
@@ -80,6 +410,36 @@
|
||||
lower: '-100',
|
||||
upper: '100',
|
||||
};
|
||||
|
||||
const lowerUpperKnobs = document.getElementById('lower-upper-dual-knobs-range');
|
||||
lowerUpperKnobs.value = {
|
||||
lower: '40',
|
||||
upper: '60',
|
||||
};
|
||||
|
||||
const abDualKnobs = document.getElementById('a-b-dual-knobs-range');
|
||||
abDualKnobs.value = {
|
||||
lower: '25',
|
||||
upper: '75',
|
||||
};
|
||||
|
||||
function updateDisplayedKnobValues() {
|
||||
document.querySelector('#lower-upper-dual-knobs-range [slot="start"]').textContent =
|
||||
lowerUpperKnobs.value.lower;
|
||||
document.querySelector('#lower-upper-dual-knobs-range [slot="end"]').textContent = lowerUpperKnobs.value.upper;
|
||||
document.querySelector('#a-b-dual-knobs-range [slot="start"]').textContent = abDualKnobs.value.lower;
|
||||
document.querySelector('#a-b-dual-knobs-range [slot="end"]').textContent = abDualKnobs.value.upper;
|
||||
}
|
||||
|
||||
updateDisplayedKnobValues();
|
||||
|
||||
lowerUpperKnobs.addEventListener('ionChange', () => {
|
||||
updateDisplayedKnobValues();
|
||||
});
|
||||
|
||||
abDualKnobs.addEventListener('ionChange', () => {
|
||||
updateDisplayedKnobValues();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,13 +1,425 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
import { configs, dragElementBy, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('range: customization'), () => {
|
||||
test('should be customizable', async ({ page }) => {
|
||||
await page.goto(`/src/components/range/test/custom`, config);
|
||||
/**
|
||||
* This behavior does not vary across modes/directions
|
||||
*/
|
||||
configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('range: custom'), () => {
|
||||
test.describe(title('CSS shadow parts'), () => {
|
||||
test('should be able to customize label part', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-range::part(label) {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
const range = page.locator('ion-range');
|
||||
await expect(range).toHaveScreenshot(screenshot(`range-custom`));
|
||||
<ion-range label="Label" value="50"></ion-range>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const range = page.locator('ion-range');
|
||||
|
||||
const labelColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const label = shadowRoot?.querySelector('.label-text-wrapper');
|
||||
return label ? window.getComputedStyle(label).color : '';
|
||||
});
|
||||
expect(labelColor).toBe('rgb(255, 0, 0)');
|
||||
});
|
||||
|
||||
test('should be able to customize bar parts', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-range::part(bar) {
|
||||
background-color: red;
|
||||
}
|
||||
ion-range::part(bar-active) {
|
||||
background-color: green;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-range label="Label" value="50"></ion-range>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const range = page.locator('ion-range');
|
||||
|
||||
const barBackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const bar = shadowRoot?.querySelector('.range-bar');
|
||||
return bar ? window.getComputedStyle(bar).backgroundColor : '';
|
||||
});
|
||||
expect(barBackgroundColor).toBe('rgb(255, 0, 0)');
|
||||
|
||||
const activeBarBackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const activeBar = shadowRoot?.querySelector('.range-bar-active');
|
||||
return activeBar ? window.getComputedStyle(activeBar).backgroundColor : '';
|
||||
});
|
||||
expect(activeBarBackgroundColor).toBe('rgb(0, 128, 0)');
|
||||
});
|
||||
|
||||
test('should be able to customize pin parts', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-range::part(pin) {
|
||||
background-color: red;
|
||||
}
|
||||
ion-range::part(pin)::before {
|
||||
background-color: green;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-range label="Label" value="50" pin="true"></ion-range>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const range = page.locator('ion-range');
|
||||
|
||||
const pinBackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const pin = shadowRoot?.querySelector('.range-pin');
|
||||
return pin ? window.getComputedStyle(pin).backgroundColor : '';
|
||||
});
|
||||
expect(pinBackgroundColor).toBe('rgb(255, 0, 0)');
|
||||
|
||||
const pinBeforeBackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const pin = shadowRoot?.querySelector('.range-pin');
|
||||
if (!pin) return '';
|
||||
return window.getComputedStyle(pin, '::before').backgroundColor;
|
||||
});
|
||||
expect(pinBeforeBackgroundColor).toBe('rgb(0, 128, 0)');
|
||||
});
|
||||
|
||||
test('should be able to customize tick parts', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-range::part(tick) {
|
||||
background-color: red;
|
||||
}
|
||||
ion-range::part(tick-active) {
|
||||
background-color: green;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-range label="Label" value="50" snaps="true" ticks="true"></ion-range>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const range = page.locator('ion-range');
|
||||
|
||||
const tickBackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const tick = shadowRoot?.querySelector('.range-tick:not(.range-tick-active)');
|
||||
return tick ? window.getComputedStyle(tick).backgroundColor : '';
|
||||
});
|
||||
expect(tickBackgroundColor).toBe('rgb(255, 0, 0)');
|
||||
|
||||
const activeTickBackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const activeTick = shadowRoot?.querySelector('.range-tick-active');
|
||||
return activeTick ? window.getComputedStyle(activeTick).backgroundColor : '';
|
||||
});
|
||||
expect(activeTickBackgroundColor).toBe('rgb(0, 128, 0)');
|
||||
});
|
||||
|
||||
test('should be able to customize knob parts', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-range::part(knob-handle) {
|
||||
background-color: red;
|
||||
}
|
||||
ion-range::part(knob) {
|
||||
background-color: green;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-range label="Label" value="50"></ion-range>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const range = page.locator('ion-range');
|
||||
|
||||
const knobHandleBackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const knobHandle = shadowRoot?.querySelector('.range-knob-handle');
|
||||
return knobHandle ? window.getComputedStyle(knobHandle).backgroundColor : '';
|
||||
});
|
||||
expect(knobHandleBackgroundColor).toBe('rgb(255, 0, 0)');
|
||||
|
||||
const knobBackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const knob = shadowRoot?.querySelector('.range-knob');
|
||||
return knob ? window.getComputedStyle(knob).backgroundColor : '';
|
||||
});
|
||||
expect(knobBackgroundColor).toBe('rgb(0, 128, 0)');
|
||||
});
|
||||
|
||||
test('should be able to customize dual knob a & b parts', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-range::part(knob-handle-a) {
|
||||
background-color: red;
|
||||
}
|
||||
ion-range::part(knob-handle-b) {
|
||||
background-color: green;
|
||||
}
|
||||
ion-range::part(knob-a) {
|
||||
background-color: blue;
|
||||
}
|
||||
ion-range::part(knob-b) {
|
||||
background-color: yellow;
|
||||
}
|
||||
ion-range::part(pin-a) {
|
||||
background-color: orange;
|
||||
}
|
||||
ion-range::part(pin-b) {
|
||||
background-color: purple;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-range label="Label" dual-knobs="true" pin="true"></ion-range>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const range = page.locator('ion-range');
|
||||
await range.evaluate((el) => {
|
||||
(el as any).value = {
|
||||
lower: 25,
|
||||
upper: 75,
|
||||
};
|
||||
});
|
||||
await page.waitForChanges();
|
||||
|
||||
const knobHandleABackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const knobHandleA = shadowRoot?.querySelector('.range-knob-handle-a');
|
||||
return knobHandleA ? window.getComputedStyle(knobHandleA).backgroundColor : '';
|
||||
});
|
||||
expect(knobHandleABackgroundColor).toBe('rgb(255, 0, 0)');
|
||||
|
||||
const knobHandleBBackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const knobHandleB = shadowRoot?.querySelector('.range-knob-handle-b');
|
||||
return knobHandleB ? window.getComputedStyle(knobHandleB).backgroundColor : '';
|
||||
});
|
||||
expect(knobHandleBBackgroundColor).toBe('rgb(0, 128, 0)');
|
||||
|
||||
// We query for the knob inside knob-handle-a because the knob
|
||||
// does not get a class for the knob name, only a part.
|
||||
const knobABackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const knobA = shadowRoot?.querySelector('.range-knob-handle-a .range-knob');
|
||||
return knobA ? window.getComputedStyle(knobA).backgroundColor : '';
|
||||
});
|
||||
expect(knobABackgroundColor).toBe('rgb(0, 0, 255)');
|
||||
|
||||
// We query for the knob inside knob-handle-b because the knob
|
||||
// does not get a class for the knob name, only a part.
|
||||
const knobBBackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const knobB = shadowRoot?.querySelector('.range-knob-handle-b .range-knob');
|
||||
return knobB ? window.getComputedStyle(knobB).backgroundColor : '';
|
||||
});
|
||||
expect(knobBBackgroundColor).toBe('rgb(255, 255, 0)');
|
||||
|
||||
// We query for the pin inside knob-handle-a because the pin
|
||||
// does not get a class for the knob name, only a part.
|
||||
const pinABackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const pinA = shadowRoot?.querySelector('.range-knob-handle-a .range-pin');
|
||||
return pinA ? window.getComputedStyle(pinA).backgroundColor : '';
|
||||
});
|
||||
expect(pinABackgroundColor).toBe('rgb(255, 165, 0)');
|
||||
|
||||
// We query for the pin inside knob-handle-b because the pin
|
||||
// does not get a class for the knob name, only a part.
|
||||
const pinBBackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const pinB = shadowRoot?.querySelector('.range-knob-handle-b .range-pin');
|
||||
return pinB ? window.getComputedStyle(pinB).backgroundColor : '';
|
||||
});
|
||||
expect(pinBBackgroundColor).toBe('rgb(128, 0, 128)');
|
||||
});
|
||||
|
||||
test('should be able to customize dual knob lower & upper parts', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-range::part(knob-handle-lower) {
|
||||
background-color: red;
|
||||
}
|
||||
ion-range::part(knob-handle-upper) {
|
||||
background-color: green;
|
||||
}
|
||||
ion-range::part(knob-lower) {
|
||||
background-color: blue;
|
||||
}
|
||||
ion-range::part(knob-upper) {
|
||||
background-color: yellow;
|
||||
}
|
||||
ion-range::part(pin-lower) {
|
||||
background-color: orange;
|
||||
}
|
||||
ion-range::part(pin-upper) {
|
||||
background-color: purple;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-range label="Label" dual-knobs="true" pin="true"></ion-range>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const range = page.locator('ion-range');
|
||||
await range.evaluate((el) => {
|
||||
(el as any).value = {
|
||||
lower: 25,
|
||||
upper: 75,
|
||||
};
|
||||
});
|
||||
await page.waitForChanges();
|
||||
|
||||
// Lower & upper are added as shadow parts but not CSS classes, so we
|
||||
// have to query by their shadow parts.
|
||||
const knobHandleLowerBackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const knobHandleLower = shadowRoot?.querySelector('[part~="knob-handle-lower"]');
|
||||
return knobHandleLower ? window.getComputedStyle(knobHandleLower).backgroundColor : '';
|
||||
});
|
||||
expect(knobHandleLowerBackgroundColor).toBe('rgb(255, 0, 0)');
|
||||
|
||||
const knobHandleUpperBackgroundColor = await range.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const knobHandleUpper = shadowRoot?.querySelector('[part~="knob-handle-upper"]');
|
||||
return knobHandleUpper ? window.getComputedStyle(knobHandleUpper).backgroundColor : '';
|
||||
});
|
||||
expect(knobHandleUpperBackgroundColor).toBe('rgb(0, 128, 0)');
|
||||
|
||||
const knobLowerBackgroundColor = await range.evaluate((el) => {
|
||||
const knobLower = el.shadowRoot?.querySelector('[part~="knob-lower"]');
|
||||
return knobLower ? window.getComputedStyle(knobLower).backgroundColor : '';
|
||||
});
|
||||
expect(knobLowerBackgroundColor).toBe('rgb(0, 0, 255)');
|
||||
|
||||
const knobUpperBackgroundColor = await range.evaluate((el) => {
|
||||
const knobUpper = el.shadowRoot?.querySelector('[part~="knob-upper"]');
|
||||
return knobUpper ? window.getComputedStyle(knobUpper).backgroundColor : '';
|
||||
});
|
||||
expect(knobUpperBackgroundColor).toBe('rgb(255, 255, 0)');
|
||||
|
||||
const pinLowerBackgroundColor = await range.evaluate((el) => {
|
||||
const pinLower = el.shadowRoot?.querySelector('[part~="pin-lower"]');
|
||||
return pinLower ? window.getComputedStyle(pinLower).backgroundColor : '';
|
||||
});
|
||||
expect(pinLowerBackgroundColor).toBe('rgb(255, 165, 0)');
|
||||
|
||||
const pinUpperBackgroundColor = await range.evaluate((el) => {
|
||||
const pinUpper = el.shadowRoot?.querySelector('[part~="pin-upper"]');
|
||||
return pinUpper ? window.getComputedStyle(pinUpper).backgroundColor : '';
|
||||
});
|
||||
expect(pinUpperBackgroundColor).toBe('rgb(128, 0, 128)');
|
||||
});
|
||||
|
||||
test('should keep a & b parts on same elements and swap lower & upper parts when values swap', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-range label="Label" dual-knobs="true" pin="true"></ion-range>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const range = page.locator('ion-range');
|
||||
await range.evaluate((el) => {
|
||||
(el as any).value = { lower: 25, upper: 75 };
|
||||
});
|
||||
await page.waitForChanges();
|
||||
|
||||
// On load: query each a & b part to check lower & upper
|
||||
const handleAHasLowerOnLoad = await range.evaluate((el) => {
|
||||
const handleA = el.shadowRoot?.querySelector('[part~="knob-handle-a"]');
|
||||
const part = handleA?.getAttribute('part') ?? '';
|
||||
return part.includes('knob-handle-lower');
|
||||
});
|
||||
const handleAHasUpperOnLoad = await range.evaluate((el) => {
|
||||
const handleA = el.shadowRoot?.querySelector('[part~="knob-handle-a"]');
|
||||
const part = handleA?.getAttribute('part') ?? '';
|
||||
return part.includes('knob-handle-upper');
|
||||
});
|
||||
const handleBHasLowerOnLoad = await range.evaluate((el) => {
|
||||
const handleB = el.shadowRoot?.querySelector('[part~="knob-handle-b"]');
|
||||
const part = handleB?.getAttribute('part') ?? '';
|
||||
return part.includes('knob-handle-lower');
|
||||
});
|
||||
const handleBHasUpperOnLoad = await range.evaluate((el) => {
|
||||
const handleB = el.shadowRoot?.querySelector('[part~="knob-handle-b"]');
|
||||
const part = handleB?.getAttribute('part') ?? '';
|
||||
return part.includes('knob-handle-upper');
|
||||
});
|
||||
|
||||
// The lower knob is assigned to the a part and the
|
||||
// upper knob is assigned to the b part on load
|
||||
expect(handleAHasLowerOnLoad).toBe(true);
|
||||
expect(handleAHasUpperOnLoad).toBe(false);
|
||||
expect(handleBHasLowerOnLoad).toBe(false);
|
||||
expect(handleBHasUpperOnLoad).toBe(true);
|
||||
|
||||
// Drag the lower knob right so the two knobs swap positions
|
||||
const box = await range.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
const startX = box!.x + box!.width * 0.25;
|
||||
const startY = box!.y + box!.height / 2;
|
||||
const dragDistance = Math.round(box!.width * 0.55);
|
||||
await dragElementBy(range, page, dragDistance, 0, startX, startY);
|
||||
await page.waitForChanges();
|
||||
|
||||
// After swap: the same elements have parts A and B
|
||||
// but lower and upper have swapped positions
|
||||
const handleAHasLowerAfterSwap = await range.evaluate((el) => {
|
||||
const handleA = el.shadowRoot?.querySelector('[part~="knob-handle-a"]');
|
||||
const part = handleA?.getAttribute('part') ?? '';
|
||||
return part.includes('knob-handle-lower');
|
||||
});
|
||||
const handleAHasUpperAfterSwap = await range.evaluate((el) => {
|
||||
const handleA = el.shadowRoot?.querySelector('[part~="knob-handle-a"]');
|
||||
const part = handleA?.getAttribute('part') ?? '';
|
||||
return part.includes('knob-handle-upper');
|
||||
});
|
||||
const handleBHasLowerAfterSwap = await range.evaluate((el) => {
|
||||
const handleB = el.shadowRoot?.querySelector('[part~="knob-handle-b"]');
|
||||
const part = handleB?.getAttribute('part') ?? '';
|
||||
return part.includes('knob-handle-lower');
|
||||
});
|
||||
const handleBHasUpperAfterSwap = await range.evaluate((el) => {
|
||||
const handleB = el.shadowRoot?.querySelector('[part~="knob-handle-b"]');
|
||||
const part = handleB?.getAttribute('part') ?? '';
|
||||
return part.includes('knob-handle-upper');
|
||||
});
|
||||
|
||||
// After swap: the lower knob is assigned to the b part and the
|
||||
// upper knob is assigned to the a part
|
||||
expect(handleAHasLowerAfterSwap).toBe(false);
|
||||
expect(handleAHasUpperAfterSwap).toBe(true);
|
||||
expect(handleBHasLowerAfterSwap).toBe(true);
|
||||
expect(handleBHasUpperAfterSwap).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 751 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 782 B |
@@ -3,9 +3,12 @@ import { newSpecPage } from '@stencil/core/testing';
|
||||
import { Item } from '../../item/item';
|
||||
import { Range } from '../range';
|
||||
|
||||
const waitForEvent = (el: HTMLElement, eventName: string) =>
|
||||
new Promise<void>((resolve) => el.addEventListener(eventName, () => resolve(), { once: true }));
|
||||
|
||||
let sharedRange: Range;
|
||||
|
||||
describe('Range', () => {
|
||||
describe('range: values', () => {
|
||||
beforeEach(() => {
|
||||
sharedRange = new Range();
|
||||
});
|
||||
@@ -87,7 +90,7 @@ describe('Range', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('range id', () => {
|
||||
describe('range: id', () => {
|
||||
it('should render custom id if passed', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
@@ -234,120 +237,632 @@ describe('range: item adjustments', () => {
|
||||
expect(range.classList.contains('range-item-start-adjustment')).toBe(false);
|
||||
expect(range.classList.contains('range-item-end-adjustment')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shadow parts', () => {
|
||||
it('should have shadow parts', async () => {
|
||||
describe('range: css classes', () => {
|
||||
describe('value state classes', () => {
|
||||
it('should apply range-value-min class when value is at min', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range pin="true" snaps="true" value="50" label="Label"></ion-range>`,
|
||||
html: `<ion-range min="0" max="100" value="0"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
expect(range.classList.contains('range-value-min')).toBe(true);
|
||||
expect(range.classList.contains('range-value-max')).toBe(false);
|
||||
});
|
||||
|
||||
it('should apply range-value-max class when value is at max', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range min="0" max="100" value="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
expect(range.classList.contains('range-value-max')).toBe(true);
|
||||
expect(range.classList.contains('range-value-min')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not apply range-value-min or range-value-max classes when value is in the middle', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range min="0" max="100" value="50"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
expect(range.classList.contains('range-value-min')).toBe(false);
|
||||
expect(range.classList.contains('range-value-max')).toBe(false);
|
||||
});
|
||||
|
||||
it('should apply range-value-min class when lower knob is at min in dual knobs', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" min="0" max="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
range.value = { lower: 0, upper: 50 };
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(range.classList.contains('range-value-min')).toBe(true);
|
||||
expect(range.classList.contains('range-value-max')).toBe(false);
|
||||
});
|
||||
|
||||
it('should apply range-value-max class when upper knob is at max in dual knobs', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" min="0" max="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
range.value = { lower: 50, upper: 100 };
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(range.classList.contains('range-value-max')).toBe(true);
|
||||
expect(range.classList.contains('range-value-min')).toBe(false);
|
||||
});
|
||||
|
||||
it('should apply range-value-min and range-value-max classes for dual knobs when both are at boundaries', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" min="0" max="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
range.value = { lower: 0, upper: 100 };
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(range.classList.contains('range-value-min')).toBe(true);
|
||||
expect(range.classList.contains('range-value-max')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not apply range-value-min or range-value-max classes for dual knobs when neither is at boundaries', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" min="0" max="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
range.value = { lower: 25, upper: 75 };
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(range.classList.contains('range-value-min')).toBe(false);
|
||||
expect(range.classList.contains('range-value-max')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('boolean property classes', () => {
|
||||
it('should not have any boolean classes by default', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
expect(range.classList.contains('range-disabled')).toBe(false);
|
||||
expect(range.classList.contains('range-dual-knobs')).toBe(false);
|
||||
expect(range.classList.contains('range-has-pin')).toBe(false);
|
||||
});
|
||||
|
||||
it('should have range-disabled class when disabled is true', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range disabled="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
expect(range.classList.contains('range-disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('should have range-dual-knobs class when dual-knobs is true true', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
expect(range.classList.contains('range-dual-knobs')).toBe(true);
|
||||
});
|
||||
|
||||
it('should have range-has-pin class when pin is true', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range pin="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
expect(range.classList.contains('range-has-pin')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pressed state classes', () => {
|
||||
it('should have range-pressed class when knob is pressed', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range min="0" max="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
// Simulate a pressed knob A by setting state on component instance
|
||||
const component = page.rootInstance;
|
||||
component.pressedKnob = 'A';
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(range.classList.contains('range-pressed')).toBe(true);
|
||||
expect(range.classList.contains('range-pressed-a')).toBe(false);
|
||||
expect(range.classList.contains('range-pressed-lower')).toBe(false);
|
||||
expect(range.classList.contains('range-pressed-b')).toBe(false);
|
||||
expect(range.classList.contains('range-pressed-upper')).toBe(false);
|
||||
});
|
||||
|
||||
it('should have range-pressed-lower class when lower knob is pressed', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" min="0" max="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
// Simulate a pressed knob A by setting state on component instance
|
||||
const component = page.rootInstance;
|
||||
component.pressedKnob = 'A';
|
||||
|
||||
component.ratioA = 0.5;
|
||||
component.ratioB = 0.8;
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(range.classList.contains('range-pressed-lower')).toBe(true);
|
||||
});
|
||||
|
||||
it('should have range-pressed-upper class when upper knob is pressed', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" min="0" max="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
// Simulate a pressed knob B by setting state on component instance
|
||||
const component = page.rootInstance;
|
||||
component.pressedKnob = 'B';
|
||||
|
||||
component.ratioA = 0.5;
|
||||
component.ratioB = 0.8;
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(range.classList.contains('range-pressed-upper')).toBe(true);
|
||||
});
|
||||
|
||||
it('should have range-pressed-a class when knob A is pressed', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" min="0" max="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
// Simulate a pressed knob A by setting state on component instance
|
||||
const component = page.rootInstance;
|
||||
component.pressedKnob = 'A';
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(range.classList.contains('range-pressed-a')).toBe(true);
|
||||
});
|
||||
|
||||
it('should have range-pressed-b class when knob B is pressed', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" min="0" max="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
// Simulate a pressed knob B by setting state on component instance
|
||||
const component = page.rootInstance;
|
||||
component.pressedKnob = 'B';
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(range.classList.contains('range-pressed-b')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('range: shadow parts', () => {
|
||||
describe('static shadow parts', () => {
|
||||
it('should have default shadow parts', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
expect(range).toHaveShadowPart('label');
|
||||
expect(range).toHaveShadowPart('pin');
|
||||
expect(range).toHaveShadowPart('knob');
|
||||
// bar and bar-active parts always exist
|
||||
expect(range).toHaveShadowPart('bar');
|
||||
expect(range).toHaveShadowPart('bar-active');
|
||||
|
||||
// label part always exists
|
||||
expect(range).toHaveShadowPart('label');
|
||||
|
||||
// knob-handle and knob parts always exist
|
||||
expect(range).toHaveShadowPart('knob-handle');
|
||||
expect(range).toHaveShadowPart('knob');
|
||||
|
||||
// knob-handle a, b, lower, and upper parts only exist when dualKnobs is true
|
||||
expect(range).not.toHaveShadowPart('knob-handle-a');
|
||||
expect(range).not.toHaveShadowPart('knob-handle-b');
|
||||
expect(range).not.toHaveShadowPart('knob-handle-lower');
|
||||
expect(range).not.toHaveShadowPart('knob-handle-upper');
|
||||
|
||||
// knob a, b, lower, and upper parts only exist when dualKnobs is true
|
||||
expect(range).not.toHaveShadowPart('knob-a');
|
||||
expect(range).not.toHaveShadowPart('knob-b');
|
||||
expect(range).not.toHaveShadowPart('knob-lower');
|
||||
expect(range).not.toHaveShadowPart('knob-upper');
|
||||
|
||||
// pin only exists when pin is true
|
||||
expect(range).not.toHaveShadowPart('pin');
|
||||
|
||||
// pin a, b, lower, and upper only exist when both pin and dualKnobs are true
|
||||
expect(range).not.toHaveShadowPart('pin-a');
|
||||
expect(range).not.toHaveShadowPart('pin-b');
|
||||
expect(range).not.toHaveShadowPart('pin-lower');
|
||||
expect(range).not.toHaveShadowPart('pin-upper');
|
||||
|
||||
// ticks only exist when ticks is true
|
||||
expect(range).not.toHaveShadowPart('tick');
|
||||
expect(range).not.toHaveShadowPart('tick-active');
|
||||
});
|
||||
|
||||
it('should have tick shadow parts', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range snaps="true" ticks="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
// ticks only exist when both snaps and ticks are true
|
||||
expect(range).toHaveShadowPart('tick');
|
||||
expect(range).toHaveShadowPart('tick-active');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('range: value state classes', () => {
|
||||
it('should apply range-value-min class when value is at min', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range min="0" max="100" value="0"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
expect(range.classList.contains('range-value-min')).toBe(true);
|
||||
expect(range.classList.contains('range-value-max')).toBe(false);
|
||||
});
|
||||
|
||||
it('should apply range-value-max class when value is at max', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range min="0" max="100" value="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
expect(range.classList.contains('range-value-max')).toBe(true);
|
||||
expect(range.classList.contains('range-value-min')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not apply range-value-min or range-value-max classes when value is in the middle', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range min="0" max="100" value="50"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
expect(range.classList.contains('range-value-min')).toBe(false);
|
||||
expect(range.classList.contains('range-value-max')).toBe(false);
|
||||
});
|
||||
|
||||
it('should apply range-value-min class when lower knob is at min in dual knobs', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" min="0" max="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
range.value = { lower: 0, upper: 50 };
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(range.classList.contains('range-value-min')).toBe(true);
|
||||
expect(range.classList.contains('range-value-max')).toBe(false);
|
||||
});
|
||||
|
||||
it('should apply range-value-max class when upper knob is at max in dual knobs', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" min="0" max="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
range.value = { lower: 50, upper: 100 };
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(range.classList.contains('range-value-max')).toBe(true);
|
||||
expect(range.classList.contains('range-value-min')).toBe(false);
|
||||
});
|
||||
|
||||
it('should apply range-value-min and range-value-max classes for dual knobs when both are at boundaries', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" min="0" max="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
range.value = { lower: 0, upper: 100 };
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(range.classList.contains('range-value-min')).toBe(true);
|
||||
expect(range.classList.contains('range-value-max')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not apply range-value-min or range-value-max classes for dual knobs when neither is at boundaries', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" min="0" max="100"></ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
range.value = { lower: 25, upper: 75 };
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(range.classList.contains('range-value-min')).toBe(false);
|
||||
expect(range.classList.contains('range-value-max')).toBe(false);
|
||||
|
||||
it('should have pin shadow part', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range pin="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
// pin only exists when pin is true
|
||||
expect(range).toHaveShadowPart('pin');
|
||||
|
||||
// pin a, b, lower, and upper only exist when both pin and dualKnobs are true
|
||||
expect(range).not.toHaveShadowPart('pin-a');
|
||||
expect(range).not.toHaveShadowPart('pin-b');
|
||||
expect(range).not.toHaveShadowPart('pin-lower');
|
||||
expect(range).not.toHaveShadowPart('pin-upper');
|
||||
});
|
||||
|
||||
it('should have dual knob shadow parts', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" pin="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
|
||||
// knob-handle a, b, lower, and upper parts only exist when dualKnobs is true
|
||||
expect(range).toHaveShadowPart('knob-handle-a');
|
||||
expect(range).toHaveShadowPart('knob-handle-b');
|
||||
expect(range).toHaveShadowPart('knob-handle-lower');
|
||||
expect(range).toHaveShadowPart('knob-handle-upper');
|
||||
|
||||
// knob a, b, lower, and upper parts only exist when dualKnobs is true
|
||||
expect(range).toHaveShadowPart('knob-a');
|
||||
expect(range).toHaveShadowPart('knob-b');
|
||||
expect(range).toHaveShadowPart('knob-lower');
|
||||
expect(range).toHaveShadowPart('knob-upper');
|
||||
|
||||
// pin only exists when pin is true
|
||||
expect(range).toHaveShadowPart('pin');
|
||||
|
||||
// pin a, b, lower, and upper only exist when both pin and dualKnobs are true
|
||||
expect(range).toHaveShadowPart('pin-a');
|
||||
expect(range).toHaveShadowPart('pin-b');
|
||||
expect(range).toHaveShadowPart('pin-lower');
|
||||
expect(range).toHaveShadowPart('pin-upper');
|
||||
});
|
||||
});
|
||||
|
||||
describe('state shadow parts', () => {
|
||||
it('should have pressed shadow part when pressed', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range pin="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
const shadowRoot = range.shadowRoot!;
|
||||
|
||||
// The pressed part should not exist on the knob by default
|
||||
expect(shadowRoot.querySelector('[part~="knob"][part~="pressed"]')).toBeNull();
|
||||
|
||||
// Simulate a pressed knob by setting state on component instance
|
||||
const component = page.rootInstance;
|
||||
component.pressedKnob = 'A';
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The pressed part should exist on the knob when pressed
|
||||
expect(shadowRoot.querySelector('[part~="knob"][part~="pressed"]')).not.toBeNull();
|
||||
|
||||
// Clear the pressed knob
|
||||
component.pressedKnob = undefined;
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The pressed part should not exist after clearing the pressed knob
|
||||
expect(shadowRoot.querySelector('[part~="knob"][part~="pressed"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('should have focused shadow part when focused', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range pin="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
const shadowRoot = range.shadowRoot!;
|
||||
|
||||
// The focused part should not exist on the knob by default
|
||||
expect(shadowRoot.querySelector('[part~="knob"][part~="focused"]')).toBeNull();
|
||||
|
||||
// Focus the knob handle
|
||||
const knobHandle = shadowRoot.querySelector('.range-knob-handle') as HTMLElement;
|
||||
knobHandle.focus();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The focused part should exist on the knob when focused
|
||||
expect(shadowRoot.querySelector('[part~="knob"][part~="focused"]')).not.toBeNull();
|
||||
|
||||
// Blur the knob handle
|
||||
knobHandle.blur();
|
||||
await waitForEvent(range, 'ionBlur');
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The focused part should not exist after blur
|
||||
expect(shadowRoot.querySelector('[part~="knob"][part~="focused"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('should have activated shadow part when activated', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range pin="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
const shadowRoot = range.shadowRoot!;
|
||||
|
||||
// The activated part should not exist on the knob by default
|
||||
expect(shadowRoot.querySelector('[part~="knob"][part~="activated"]')).toBeNull();
|
||||
|
||||
// Simulate an activated knob by setting state on component instance
|
||||
const component = page.rootInstance;
|
||||
component.activatedKnob = 'A';
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The activated part should exist on the knob when activated
|
||||
expect(shadowRoot.querySelector('[part~="knob"][part~="activated"]')).not.toBeNull();
|
||||
|
||||
// Clear the activated knob
|
||||
component.activatedKnob = undefined;
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The activated part should not exist after clearing the activated knob
|
||||
expect(shadowRoot.querySelector('[part~="knob"][part~="activated"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('should have hover shadow part when hovered', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range pin="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
const shadowRoot = range.shadowRoot!;
|
||||
|
||||
// The hover part should not exist on the knob by default
|
||||
expect(shadowRoot.querySelector('[part~="knob"][part~="hover"]')).toBeNull();
|
||||
|
||||
// Simulate a hovered knob by setting state on component instance
|
||||
const component = page.rootInstance;
|
||||
component.hoveredKnob = 'A';
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The hover part should exist on the knob when hovered
|
||||
expect(shadowRoot.querySelector('[part~="knob"][part~="hover"]')).not.toBeNull();
|
||||
|
||||
// Clear the hovered knob
|
||||
component.hoveredKnob = undefined;
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The hover part should not exist after clearing the hovered knob
|
||||
expect(shadowRoot.querySelector('[part~="knob"][part~="hover"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('should have pressed shadow part on only one knob when dual-knobs is true', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" pin="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
const shadowRoot = range.shadowRoot!;
|
||||
|
||||
// The pressed part should not exist on either knob by default
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="pressed"]')).toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="pressed"]')).toBeNull();
|
||||
|
||||
// Simulate a pressed knob A by setting state on component instance
|
||||
const component = page.rootInstance;
|
||||
component.pressedKnob = 'A';
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The pressed part should exist on knob A only
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="pressed"]')).not.toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="pressed"]')).toBeNull();
|
||||
|
||||
// Simulate a pressed knob B by setting state on component instance
|
||||
component.pressedKnob = 'B';
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The pressed part should now exist on knob B only
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="pressed"]')).toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="pressed"]')).not.toBeNull();
|
||||
|
||||
// Clear the pressed knob
|
||||
component.pressedKnob = undefined;
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The pressed part should not exist after clearing the pressed knob
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="pressed"]')).toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="pressed"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('should have focused shadow part on only one knob when dual-knobs is true', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" pin="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
const shadowRoot = range.shadowRoot!;
|
||||
|
||||
// The focused part should not exist on either knob by default
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="focused"]')).toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="focused"]')).toBeNull();
|
||||
|
||||
// Focus knob handle A
|
||||
const knobHandleA = shadowRoot.querySelector('.range-knob-handle-a') as HTMLElement;
|
||||
knobHandleA.focus();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The focused part should exist on knob A only
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="focused"]')).not.toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="focused"]')).toBeNull();
|
||||
|
||||
// Focus knob handle B
|
||||
const knobHandleB = shadowRoot.querySelector('.range-knob-handle-b') as HTMLElement;
|
||||
knobHandleB.focus();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The focused part should now exist on knob B only
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="focused"]')).toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="focused"]')).not.toBeNull();
|
||||
|
||||
// Blur knob handle B
|
||||
knobHandleB.blur();
|
||||
await waitForEvent(range, 'ionBlur');
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The focused part should not exist after blurring the knob handle
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="focused"]')).toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="focused"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('should have activated shadow part on only one knob when dual-knobs is true', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" pin="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
const shadowRoot = range.shadowRoot!;
|
||||
|
||||
// The activated part should not exist on either knob by default
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="activated"]')).toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="activated"]')).toBeNull();
|
||||
|
||||
// Simulate an activated knob A by setting state on component instance
|
||||
const component = page.rootInstance;
|
||||
component.activatedKnob = 'A';
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The activated part should exist on knob A only
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="activated"]')).not.toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="activated"]')).toBeNull();
|
||||
|
||||
// Simulate an activated knob B by setting state on component instance
|
||||
component.activatedKnob = 'B';
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The activated part should now exist on knob B only
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="activated"]')).toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="activated"]')).not.toBeNull();
|
||||
|
||||
// Clear the activated knob
|
||||
component.activatedKnob = undefined;
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The activated part should not exist after clearing the activated knob
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="activated"]')).toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="activated"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('should have hover shadow part on only one knob when dual-knobs is true', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range dual-knobs="true" pin="true" value="50" label="Label"></ion-range>`,
|
||||
});
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
const shadowRoot = range.shadowRoot!;
|
||||
|
||||
// The hover part should not exist on either knob by default
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="hover"]')).toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="hover"]')).toBeNull();
|
||||
|
||||
// Simulate a hovered knob A by setting state on component instance
|
||||
const component = page.rootInstance;
|
||||
component.hoveredKnob = 'A';
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The hover part should exist on knob A only
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="hover"]')).not.toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="hover"]')).toBeNull();
|
||||
|
||||
// Simulate a hovered knob B by setting state on component instance
|
||||
component.hoveredKnob = 'B';
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The hover part should now exist on knob B only
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="hover"]')).toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="hover"]')).not.toBeNull();
|
||||
|
||||
// Clear the hovered knob
|
||||
component.hoveredKnob = undefined;
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// The hover part should not exist after clearing the hovered knob
|
||||
expect(shadowRoot.querySelector('[part~="knob-a"][part~="hover"]')).toBeNull();
|
||||
expect(shadowRoot.querySelector('[part~="knob-b"][part~="hover"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user