Skip to content

Commit 7ab37e4

Browse files
feat(slider): adds explicit multi-value support via range=true, valueStart, valueEnd
PiperOrigin-RevId: 533264226
1 parent 017d2a9 commit 7ab37e4

File tree

4 files changed

+158
-156
lines changed

4 files changed

+158
-156
lines changed

slider/demo/demo.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@ import './index.js';
88
import './material-collection.js';
99

1010
import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js';
11-
import {boolInput, Knob, numberInput, textInput} from './index.js';
11+
import {boolInput, Knob, numberInput} from './index.js';
1212

1313
import {stories, StoryKnobs} from './stories.js';
1414

1515
const collection =
1616
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Slider', [
17-
new Knob('value', {ui: numberInput(), defaultValue: 5}),
18-
new Knob('multivalue.value', {ui: textInput(), defaultValue: '5, 10'}),
17+
new Knob('value', {ui: numberInput(), defaultValue: 50}),
18+
new Knob('range', {ui: boolInput(), defaultValue: false}),
19+
new Knob('valueStart', {ui: numberInput(), defaultValue: 30}),
20+
new Knob('valueEnd', {ui: numberInput(), defaultValue: 70}),
1921
new Knob('min', {ui: numberInput(), defaultValue: 0}),
20-
new Knob('max', {ui: numberInput(), defaultValue: 25}),
22+
new Knob('max', {ui: numberInput(), defaultValue: 100}),
2123
new Knob('step', {ui: numberInput(), defaultValue: 1}),
2224
new Knob('withTickMarks', {ui: boolInput(), defaultValue: false}),
2325
new Knob('withLabel', {ui: boolInput(), defaultValue: false}),

slider/demo/stories.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import {css, html} from 'lit';
1313
/** Knob types for slider stories. */
1414
export interface StoryKnobs {
1515
value: number;
16-
'multivalue.value': string;
16+
valueStart: number;
17+
valueEnd: number;
1718
min: number;
1819
max: number;
1920
step: number;
21+
range: boolean;
2022
withTickMarks: boolean;
2123
withLabel: boolean;
2224
disabled: boolean;
@@ -36,10 +38,13 @@ const standard: MaterialStoryInit<StoryKnobs> = {
3638
return html`
3739
<label>label
3840
<md-slider
39-
value=${knobs.value}
41+
.value=${knobs.value}
42+
.valueStart=${knobs.valueStart}
43+
.valueEnd=${knobs.valueEnd}
4044
.min=${knobs.min}
4145
.max=${knobs.max}
4246
.step=${knobs.step ?? 1}
47+
.range=${knobs.range}
4348
.withTickMarks=${knobs.withTickMarks}
4449
.withLabel=${knobs.withLabel ?? false}
4550
.disabled=${knobs.disabled ?? false}
@@ -54,12 +59,10 @@ const multiValue: MaterialStoryInit<StoryKnobs> = {
5459
render(knobs) {
5560
return html`
5661
<label>label
57-
<!--
58-
Value can be a [number, number]|number but can convert string
59-
attribtues separated by commas
60-
-->
6162
<md-slider
62-
value=${(knobs['multivalue.value']) as unknown as number}
63+
range
64+
.valueStart=${(knobs.valueStart)}
65+
.valueEnd=${(knobs.valueEnd)}
6366
.min=${knobs.min}
6467
.max=${knobs.max}
6568
.step=${knobs.step ?? 1}
@@ -117,21 +120,21 @@ const customStyling: MaterialStoryInit<StoryKnobs> = {
117120
}
118121
function updateLabel(event: Event) {
119122
const target = event.target as MdSlider;
120-
const {valueAsFraction} = target;
121-
const hasValueRange = Array.isArray(valueAsFraction);
122-
target.valueLabel = hasValueRange ?
123-
[labelFor(valueAsFraction[0]), labelFor(valueAsFraction[1])] :
124-
labelFor(valueAsFraction);
123+
const {min, max, valueStart, valueEnd} = target;
124+
const range = max - min;
125+
const fractionStart = valueStart / range;
126+
const fractionEnd = valueEnd / range;
127+
target.valueStartLabel = labelFor(fractionStart);
128+
target.valueEndLabel = labelFor(fractionEnd);
125129
}
126130
return html`
127131
<label>label
128-
<!--
129-
Value can be a [number, number]|number but can convert string
130-
attribtues separated by commas
131-
-->
132132
<md-slider
133-
value=${(knobs['multivalue.value']) as unknown as number}
134-
valueLabel=${`😔, 😌`}
133+
range
134+
.valueStart=${(knobs.valueStart)}
135+
.valueEnd=${(knobs.valueEnd)}
136+
.valueStartLabel=${'😔'}
137+
.valueEndLabel=${'😌'}
135138
withTickMarks
136139
withLabel
137140
.min=${knobs.min}

slider/lib/slider.ts

Lines changed: 67 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,6 @@ function inBounds({x, y}: PointerEvent, element?: HTMLElement|null) {
3434
return x >= left && x <= right && y >= top && y <= bottom;
3535
}
3636

37-
// parse values like: foo or foo,bar
38-
function tupleConverter(attr: string|null) {
39-
const [, v, e] =
40-
attr?.match(/\s*\[?\s*([^,]+)(?:(?:\s*$)|(?:\s*,\s*(.*)\s*))/) ?? [];
41-
return e !== undefined ? [v, e] : v;
42-
}
43-
44-
function toNumber(value: string) {
45-
return Number(value) || 0;
46-
}
47-
48-
function tupleAsString(value: unknown|[unknown, unknown]) {
49-
return Array.isArray(value) ? value.join() : String(value ?? '');
50-
}
51-
52-
function valueConverter(attr: string|null) {
53-
const value = tupleConverter(attr);
54-
return Array.isArray(value) ? value.map(i => toNumber(i)) : toNumber(value);
55-
}
5637

5738
function clamp(value: number, min: number, max: number) {
5839
return Math.max(min, Math.min(max, value));
@@ -98,21 +79,40 @@ export class Slider extends LitElement {
9879
/**
9980
* The slider maximum value
10081
*/
101-
@property({type: Number}) max = 10;
82+
@property({type: Number}) max = 100;
83+
84+
/**
85+
* The slider value displayed when range is false.
86+
*/
87+
@property({type: Number}) value = 50;
88+
89+
/**
90+
* The slider start value displayed when range is true.
91+
*/
92+
@property({type: Number}) valueStart = 25;
93+
94+
/**
95+
* The slider end value displayed when range is true.
96+
*/
97+
@property({type: Number}) valueEnd = 75;
98+
99+
/**
100+
* An optional label for the slider's value displayed when range is
101+
* false; if not set, the label is the value itself.
102+
*/
103+
@property() valueLabel?: string|undefined;
102104

103105
/**
104-
* The slider value, can be a single number, or an array tuple indicating
105-
* a start and end value.
106+
* An optional label for the slider's start value displayed when
107+
* range is true; if not set, the label is the valueStart itself.
106108
*/
107-
@property({converter: valueConverter}) value: number|[number, number] = 0;
109+
@property() valueStartLabel?: string|undefined;
108110

109111
/**
110-
* An optinoal label for the slider's value; if not set, the label is the
111-
* value itself. This can be a string or string tuple when start and end
112-
* values are used.
112+
* An optional label for the slider's end value displayed when
113+
* range is true; if not set, the label is the valueEnd itself.
113114
*/
114-
@property({converter: tupleConverter})
115-
valueLabel?: string|[string, string]|undefined;
115+
@property() valueEndLabel?: string|undefined;
116116

117117
/**
118118
* The step between values.
@@ -129,6 +129,13 @@ export class Slider extends LitElement {
129129
*/
130130
@property({type: Boolean}) withLabel = false;
131131

132+
/**
133+
* Whether or not to show a value range. When false, the slider displays
134+
* a slideable handle for the value property; when true, it displays
135+
* slideable handles for the valueStart and valueEnd properties.
136+
*/
137+
@property({type: Boolean}) range = false;
138+
132139
/**
133140
* The HTML name to use in form submission.
134141
*/
@@ -141,17 +148,6 @@ export class Slider extends LitElement {
141148
return this.closest('form');
142149
}
143150

144-
145-
/**
146-
* Read only computed value representing the fraction between 0 and 1
147-
* respresenting the value's position between min and max. This is a
148-
* single fraction or a tuple if the value specifies start and end values.
149-
*/
150-
get valueAsFraction() {
151-
const {lowerFraction, upperFraction} = this.getMetrics();
152-
return this.allowRange ? [lowerFraction, upperFraction] : upperFraction;
153-
}
154-
155151
private getMetrics() {
156152
const step = Math.max(this.step, 1);
157153
const range = Math.max(this.max - this.min, step);
@@ -210,38 +206,26 @@ export class Slider extends LitElement {
210206
this.inputB?.focus();
211207
}
212208

213-
get valueAsString() {
214-
return tupleAsString(this.value);
215-
}
216-
217209
// value coerced to a string
218210
[getFormValue]() {
219-
return this.valueAsString;
211+
return this.range ? `${this.valueStart}, ${this.valueEnd}` :
212+
`${this.value}`;
220213
}
221214

222-
// If range should be allowed (detected via value format).
223-
private allowRange = false;
224-
225215
// indicates input values are crossed over each other from initial rendering.
226216
private isFlipped() {
227217
return this.valueA > this.valueB;
228218
}
229219

230220
protected override willUpdate(changed: PropertyValues) {
231-
if (changed.has('value') || changed.has('min') || changed.has('max') ||
232-
changed.has('step')) {
233-
this.allowRange = Array.isArray(this.value);
234-
const step = Math.max(this.step, 1);
235-
let lower =
236-
this.allowRange ? (this.value as [number, number])[0] : this.min;
237-
lower = clamp(lower - (lower % step), this.min, this.max);
238-
let upper = this.allowRange ? (this.value as [number, number])[1] :
239-
this.value as number;
240-
upper = clamp(upper - (upper % step), this.min, this.max);
241-
const isFlipped = this.isFlipped() && this.allowRange;
242-
this.valueA = isFlipped ? upper : lower;
243-
this.valueB = isFlipped ? lower : upper;
244-
}
221+
const step = Math.max(this.step, 1);
222+
let lower = this.range ? this.valueStart : this.min;
223+
lower = clamp(lower - (lower % step), this.min, this.max);
224+
let upper = this.range ? this.valueEnd : this.value;
225+
upper = clamp(upper - (upper % step), this.min, this.max);
226+
const isFlipped = this.isFlipped() && this.range;
227+
this.valueA = isFlipped ? upper : lower;
228+
this.valueB = isFlipped ? lower : upper;
245229

246230
// manually handle ripple hover state since the handle is pointer events
247231
// none.
@@ -255,7 +239,7 @@ export class Slider extends LitElement {
255239
}
256240

257241
protected override async updated(changed: PropertyValues) {
258-
if (changed.has('value') || changed.has('valueA') ||
242+
if (changed.has('range') || changed.has('valueA') ||
259243
changed.has('valueB')) {
260244
await this.updateComplete;
261245
this.handlesOverlapping = isOverlapping(this.handleA, this.handleB);
@@ -272,14 +256,19 @@ export class Slider extends LitElement {
272256
// for generating tick marks
273257
'--slider-tick-count': String(range / step),
274258
};
275-
const containerClasses = {ranged: this.allowRange};
259+
const containerClasses = {ranged: this.range};
276260

277261
// optional label values to show in place of the value.
278-
const labelA = String(this.valueLabel?.[isFlipped ? 1 : 0] ?? this.valueA);
279-
const labelB = String(
280-
(this.allowRange ? this.valueLabel?.[isFlipped ? 0 : 1] :
281-
this.valueLabel) ??
282-
this.valueB);
262+
let labelA = String(this.valueA);
263+
let labelB = String(this.valueB);
264+
if (this.range) {
265+
const a = isFlipped ? this.valueEndLabel : this.valueStartLabel;
266+
const b = isFlipped ? this.valueStartLabel : this.valueEndLabel;
267+
labelA = a ?? labelA;
268+
labelB = b ?? labelB;
269+
} else {
270+
labelB = this.valueLabel ?? labelB;
271+
}
283272

284273
const inputAProps = {
285274
id: 'a',
@@ -322,13 +311,14 @@ export class Slider extends LitElement {
322311
class="container ${classMap(containerClasses)}"
323312
style=${styleMap(containerStyles)}
324313
>
325-
${when(this.allowRange, () => this.renderInput(inputAProps))}
314+
${when(this.range, () => this.renderInput(inputAProps))}
326315
${this.renderInput(inputBProps)}
327316
${this.renderTrack()}
328317
<div class="handleContainerPadded">
329318
<div class="handleContainerBlock">
330319
<div class="handleContainer ${classMap(handleContainerClasses)}">
331-
${when(this.allowRange, () => this.renderHandle(handleAProps))}
320+
${
321+
when(this.range, () => this.renderHandle(handleAProps))}
332322
${this.renderHandle(handleBProps)}
333323
</div>
334324
</div>
@@ -379,7 +369,7 @@ export class Slider extends LitElement {
379369
}) {
380370
// when ranged, ensure announcement includes value info.
381371
const ariaLabelDescriptor =
382-
this.allowRange ? ` - ${lesser ? `start` : `end`} handle` : '';
372+
this.range ? ` - ${lesser ? `start` : `end`} handle` : '';
383373
// Needed for closure conformance
384374
const {ariaLabel} = this as ARIAMixinStrict;
385375
return html`<input type="range"
@@ -507,7 +497,12 @@ export class Slider extends LitElement {
507497
// update value only on interaction
508498
const lower = Math.min(this.valueA, this.valueB);
509499
const upper = Math.max(this.valueA, this.valueB);
510-
this.value = this.allowRange ? [lower, upper] : this.valueB;
500+
if (this.range) {
501+
this.valueStart = lower;
502+
this.valueEnd = upper;
503+
} else {
504+
this.value = this.valueB;
505+
}
511506
}
512507

513508
private handleChange(event: Event) {

0 commit comments

Comments
 (0)