feat(range): add reference point for start position of range slider (#25598)

Resolves #24348

Co-authored-by: Sachin Garg <sg@rawzor.com>
This commit is contained in:
Sean Perkins
2022-07-15 12:00:15 -04:00
committed by GitHub
parent 2e306122ef
commit c2781cc1c3
20 changed files with 243 additions and 8 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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).
*/

View File

@@ -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}%`;

View 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>

View 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`);
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -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);
};
/*

View File

@@ -587,6 +587,7 @@ export const IonRange = /*@__PURE__*/ defineContainer<JSX.IonRange>('ion-range',
'snaps',
'step',
'ticks',
'activeBarStart',
'disabled',
'value',
'ionChange',