feat(range): add reference point for start position of range slider (#25598)
Resolves #24348 Co-authored-by: Sachin Garg <sg@rawzor.com>
@@ -1304,13 +1304,13 @@ mouse drag, touch gesture, or keyboard interaction.
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: undefined,
|
||||
inputs: ['color', 'debounce', 'disabled', 'dualKnobs', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
|
||||
inputs: ['activeBarStart', 'color', 'debounce', 'disabled', 'dualKnobs', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-range',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
inputs: ['color', 'debounce', 'disabled', 'dualKnobs', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
|
||||
inputs: ['activeBarStart', 'color', 'debounce', 'disabled', 'dualKnobs', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
|
||||
})
|
||||
export class IonRange {
|
||||
protected el: HTMLElement;
|
||||
|
||||
@@ -971,6 +971,7 @@ ion-radio-group,prop,value,any,undefined,false,false
|
||||
ion-radio-group,event,ionChange,RadioGroupChangeEventDetail<any>,true
|
||||
|
||||
ion-range,shadow
|
||||
ion-range,prop,activeBarStart,number | undefined,undefined,false,false
|
||||
ion-range,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
|
||||
ion-range,prop,debounce,number,0,false,false
|
||||
ion-range,prop,disabled,boolean,false,false,false
|
||||
|
||||
8
core/src/components.d.ts
vendored
@@ -2076,6 +2076,10 @@ export namespace Components {
|
||||
"value"?: any | null;
|
||||
}
|
||||
interface IonRange {
|
||||
/**
|
||||
* The start position of the range active bar. This feature is only available with a single knob (dualKnobs="false"). Valid values are greater than or equal to the min value and less than or equal to the max value.
|
||||
*/
|
||||
"activeBarStart"?: number;
|
||||
/**
|
||||
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
|
||||
*/
|
||||
@@ -5961,6 +5965,10 @@ declare namespace LocalJSX {
|
||||
"value"?: any | null;
|
||||
}
|
||||
interface IonRange {
|
||||
/**
|
||||
* The start position of the range active bar. This feature is only available with a single knob (dualKnobs="false"). Valid values are greater than or equal to the min value and less than or equal to the max value.
|
||||
*/
|
||||
"activeBarStart"?: number;
|
||||
/**
|
||||
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from '../../utils/content';
|
||||
import type { Attributes } from '../../utils/helpers';
|
||||
import { inheritAriaAttributes, clamp, debounceEvent, getAriaLabel, renderHiddenInput } from '../../utils/helpers';
|
||||
import { printIonWarning } from '../../utils/logging';
|
||||
import { isRTL } from '../../utils/rtl';
|
||||
import { createColorClasses, hostContext } from '../../utils/theme';
|
||||
|
||||
@@ -142,6 +143,31 @@ export class Range implements ComponentInterface {
|
||||
*/
|
||||
@Prop() ticks = true;
|
||||
|
||||
/**
|
||||
* The start position of the range active bar. This feature is only available with a single knob (dualKnobs="false").
|
||||
* Valid values are greater than or equal to the min value and less than or equal to the max value.
|
||||
*/
|
||||
@Prop({ mutable: true }) activeBarStart?: number;
|
||||
@Watch('activeBarStart')
|
||||
protected activeBarStartChanged() {
|
||||
const { activeBarStart } = this;
|
||||
if (activeBarStart !== undefined) {
|
||||
if (activeBarStart > this.max) {
|
||||
printIonWarning(
|
||||
`Range: The value of activeBarStart (${activeBarStart}) is greater than the max (${this.max}). Valid values are greater than or equal to the min value and less than or equal to the max value.`,
|
||||
this.el
|
||||
);
|
||||
this.activeBarStart = this.max;
|
||||
} else if (activeBarStart < this.min) {
|
||||
printIonWarning(
|
||||
`Range: The value of activeBarStart (${activeBarStart}) is less than the min (${this.min}). Valid values are greater than or equal to the min value and less than or equal to the max value.`,
|
||||
this.el
|
||||
);
|
||||
this.activeBarStart = this.min;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If `true`, the user cannot interact with the range.
|
||||
*/
|
||||
@@ -252,6 +278,7 @@ export class Range implements ComponentInterface {
|
||||
this.updateRatio();
|
||||
this.debounceChanged();
|
||||
this.disabledChanged();
|
||||
this.activeBarStartChanged();
|
||||
|
||||
/**
|
||||
* If we have not yet rendered
|
||||
@@ -395,7 +422,11 @@ export class Range implements ComponentInterface {
|
||||
if (this.dualKnobs) {
|
||||
return Math.min(this.ratioA, this.ratioB);
|
||||
}
|
||||
return 0;
|
||||
const { activeBarStart } = this;
|
||||
if (activeBarStart == null) {
|
||||
return 0;
|
||||
}
|
||||
return valueToRatio(activeBarStart, this.min, this.max);
|
||||
}
|
||||
|
||||
private get ratioUpper() {
|
||||
@@ -484,8 +515,8 @@ export class Range implements ComponentInterface {
|
||||
labelText = inheritedAttributes['aria-label'];
|
||||
}
|
||||
const mode = getIonMode(this);
|
||||
const barStart = `${ratioLower * 100}%`;
|
||||
const barEnd = `${100 - ratioUpper * 100}%`;
|
||||
let barStart = `${ratioLower * 100}%`;
|
||||
let barEnd = `${100 - ratioUpper * 100}%`;
|
||||
|
||||
const rtl = isRTL(this.el);
|
||||
|
||||
@@ -498,6 +529,33 @@ export class Range implements ComponentInterface {
|
||||
};
|
||||
};
|
||||
|
||||
if (this.dualKnobs === false) {
|
||||
/**
|
||||
* When the value is less than the activeBarStart or the min value,
|
||||
* the knob will display at the start of the active bar.
|
||||
*/
|
||||
if (this.valA < (this.activeBarStart ?? this.min)) {
|
||||
/**
|
||||
* Sets the bar positions relative to the upper and lower limits.
|
||||
* Converts the ratio values into percentages, used as offsets for left/right styles.
|
||||
*
|
||||
* The ratioUpper refers to the knob position on the bar.
|
||||
* The ratioLower refers to the end position of the active bar (the value).
|
||||
*/
|
||||
barStart = `${ratioUpper * 100}%`;
|
||||
barEnd = `${100 - ratioLower * 100}%`;
|
||||
} else {
|
||||
/**
|
||||
* Otherwise, the knob will display at the end of the active bar.
|
||||
*
|
||||
* The ratioLower refers to the start position of the active bar (the value).
|
||||
* The ratioUpper refers to the knob position on the bar.
|
||||
*/
|
||||
barStart = `${ratioLower * 100}%`;
|
||||
barEnd = `${100 - ratioUpper * 100}%`;
|
||||
}
|
||||
}
|
||||
|
||||
const barStyle = {
|
||||
[start]: barStart,
|
||||
[end]: barEnd,
|
||||
@@ -508,9 +566,16 @@ export class Range implements ComponentInterface {
|
||||
for (let value = min; value <= max; value += step) {
|
||||
const ratio = valueToRatio(value, min, max);
|
||||
|
||||
const ratioMin = Math.min(ratioLower, ratioUpper);
|
||||
const ratioMax = Math.max(ratioLower, ratioUpper);
|
||||
|
||||
const tick: any = {
|
||||
ratio,
|
||||
active: ratio >= ratioLower && ratio <= ratioUpper,
|
||||
/**
|
||||
* Sets the tick mark as active when the tick is between the min bounds and the knob.
|
||||
* When using activeBarStart, the tick mark will be active between the knob and activeBarStart.
|
||||
*/
|
||||
active: ratio >= ratioMin && ratio <= ratioMax,
|
||||
};
|
||||
|
||||
tick[start] = `${ratio * 100}%`;
|
||||
|
||||
148
core/src/components/range/test/activeBarStart/index.html
Normal file
@@ -0,0 +1,148 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Range - activeBarStart</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/core.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
|
||||
<style>
|
||||
ion-range {
|
||||
/* End knob is being cut-off */
|
||||
width: 90%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Range - activeBarStart</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding" id="content">
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">activeBarStart is 0 and value is 0.</ion-label>
|
||||
<ion-range
|
||||
class="ion-no-padding"
|
||||
min="-100"
|
||||
max="100"
|
||||
snaps="10"
|
||||
step="10"
|
||||
pin="true"
|
||||
active-bar-start="0"
|
||||
value="0"
|
||||
>
|
||||
</ion-range>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked"> activeBarStart is 0 and value is 70. </ion-label>
|
||||
<ion-range
|
||||
class="ion-no-padding"
|
||||
min="-100"
|
||||
max="100"
|
||||
snaps="10"
|
||||
step="10"
|
||||
pin="true"
|
||||
active-bar-start="0"
|
||||
value="70"
|
||||
>
|
||||
</ion-range>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked"> activeBarStart is 0 and value is -70. </ion-label>
|
||||
<ion-range
|
||||
class="ion-no-padding"
|
||||
min="-100"
|
||||
max="100"
|
||||
snaps="10"
|
||||
step="10"
|
||||
pin="true"
|
||||
active-bar-start="0"
|
||||
value="-70"
|
||||
>
|
||||
</ion-range>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked"> activeBarStart is -30 and value is 0. </ion-label>
|
||||
<ion-range
|
||||
class="ion-no-padding"
|
||||
min="-100"
|
||||
max="100"
|
||||
snaps="10"
|
||||
step="10"
|
||||
pin="true"
|
||||
active-bar-start="-30"
|
||||
value="0"
|
||||
>
|
||||
</ion-range>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked"> activeBarStart is 30 and value is 0. </ion-label>
|
||||
<ion-range
|
||||
class="ion-no-padding"
|
||||
min="-100"
|
||||
max="100"
|
||||
snaps="10"
|
||||
step="10"
|
||||
pin="true"
|
||||
active-bar-start="30"
|
||||
value="0"
|
||||
>
|
||||
</ion-range>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">activeBarStart is between steps</ion-label>
|
||||
<ion-range
|
||||
class="ion-no-padding"
|
||||
min="-100"
|
||||
max="100"
|
||||
snaps="10"
|
||||
step="10"
|
||||
pin="true"
|
||||
active-bar-start="25"
|
||||
value="0"
|
||||
></ion-range>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">invalid activeBarStart value (less than min)</ion-label>
|
||||
<ion-range class="ion-no-padding" min="0" max="100" snaps="10" step="10" pin="true" active-bar-start="-30">
|
||||
</ion-range>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">invalid activeBarStart value (greater than max)</ion-label>
|
||||
<ion-range class="ion-no-padding" min="0" max="100" snaps="10" step="10" pin="true" active-bar-start="110">
|
||||
</ion-range>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked"> activeBarStart is ignored with dual knobs enabled. </ion-label>
|
||||
<ion-range
|
||||
class="ion-no-padding"
|
||||
min="-100"
|
||||
max="100"
|
||||
snaps="10"
|
||||
step="10"
|
||||
pin="true"
|
||||
active-bar-start="-30"
|
||||
dual-knobs="true"
|
||||
value="30"
|
||||
></ion-range>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
12
core/src/components/range/test/activeBarStart/range.e2e.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('range: activeBarStart', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto(`/src/components/range/test/activeBarStart`);
|
||||
|
||||
await page.setIonViewport();
|
||||
|
||||
expect(await page.screenshot()).toMatchSnapshot(`range-activeBarStart-diff-${page.getSnapshotSettings()}.png`);
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 97 KiB |
@@ -4,8 +4,8 @@
|
||||
*
|
||||
* @param message - The string message to be logged to the console.
|
||||
*/
|
||||
export const printIonWarning = (message: string) => {
|
||||
return console.warn(`[Ionic Warning]: ${message}`);
|
||||
export const printIonWarning = (message: string, ...params: any[]) => {
|
||||
return console.warn(`[Ionic Warning]: ${message}`, ...params);
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -587,6 +587,7 @@ export const IonRange = /*@__PURE__*/ defineContainer<JSX.IonRange>('ion-range',
|
||||
'snaps',
|
||||
'step',
|
||||
'ticks',
|
||||
'activeBarStart',
|
||||
'disabled',
|
||||
'value',
|
||||
'ionChange',
|
||||
|
||||