Skip to content

Commit 839122c

Browse files
iveysaurhansl
authored andcommitted
Slider step (#904)
* Remove transition when dragging * Add tests for thumb and track position on drag * Add snapping to slider * Thumb snaps to value when dragging * Add tests and demo for slider with defined step * Fix snapping on drag tests * Small comment fixes * Add comments * Value should snap to the edges of the slider * Create snap to value function
1 parent 47448cb commit 839122c

File tree

4 files changed

+200
-49
lines changed

4 files changed

+200
-49
lines changed

src/components/slider/slider.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ $md-slider-disabled-color: rgba(black, 0.26);
3131
@mixin slider-thumb-position($width: $md-slider-thumb-size, $height: $md-slider-thumb-size) {
3232
position: absolute;
3333
top: center-vertically($md-slider-thickness, $height);
34+
// This makes it so that the center of the thumb aligns with where the click was.
35+
// This is not affected by the movement of the thumb.
36+
left: (-$width / 2);
3437
width: $width;
3538
height: $height;
3639
border-radius: max($width, $height);

src/components/slider/slider.spec.ts

Lines changed: 163 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ describe('MdSlider', () => {
3838
let sliderDimensions: ClientRect;
3939
let thumbElement: HTMLElement;
4040
let thumbDimensions: ClientRect;
41-
let thumbWidth: number;
4241

4342
beforeEach(async(() => {
4443
builder.createAsync(StandardSlider).then(f => {
@@ -56,8 +55,6 @@ describe('MdSlider', () => {
5655

5756
thumbElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-thumb-position');
5857
thumbDimensions = thumbElement.getBoundingClientRect();
59-
thumbWidth =
60-
sliderNativeElement.querySelector('.md-slider-thumb').getBoundingClientRect().width;
6158
});
6259
}));
6360

@@ -71,16 +68,14 @@ describe('MdSlider', () => {
7168
expect(sliderInstance.value).toBe(0);
7269
dispatchClickEvent(sliderTrackElement, 0.19);
7370
// The expected value is 19 from: percentage * difference of max and min.
74-
let difference = Math.abs(sliderInstance.value - 19);
75-
expect(difference).toBeLessThan(1);
71+
expect(sliderInstance.value).toBe(19);
7672
});
7773

7874
it('should update the value on a drag', () => {
7975
expect(sliderInstance.value).toBe(0);
8076
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.89, gestureConfig);
8177
// The expected value is 89 from: percentage * difference of max and min.
82-
let difference = Math.abs(sliderInstance.value - 89);
83-
expect(difference).toBeLessThan(1);
78+
expect(sliderInstance.value).toBe(89);
8479
});
8580

8681
it('should set the value as min when dragging before the track', () => {
@@ -100,40 +95,49 @@ describe('MdSlider', () => {
10095
dispatchClickEvent(sliderTrackElement, 0.39);
10196

10297
trackFillDimensions = trackFillElement.getBoundingClientRect();
103-
// The fill should be close to the slider's width * the percentage from the click.
104-
let difference = Math.abs(trackFillDimensions.width - (sliderDimensions.width * 0.39));
105-
expect(difference).toBeLessThan(1);
98+
thumbDimensions = thumbElement.getBoundingClientRect();
99+
100+
// The thumb and track fill positions are relative to the viewport, so to get the thumb's
101+
// offset relative to the track, subtract the offset on the track fill.
102+
let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
103+
// The track fill width should be equal to the thumb's position.
104+
expect(Math.round(trackFillDimensions.width)).toBe(Math.round(thumbPosition));
106105
});
107106

108107
it('should update the thumb position on click', () => {
109-
expect(thumbDimensions.left).toBe(sliderDimensions.left - (thumbWidth / 2));
110-
dispatchClickEvent(sliderTrackElement, 0.16);
108+
expect(thumbDimensions.left).toBe(sliderDimensions.left);
109+
// 50% is used here because the click event that is dispatched truncates the position and so
110+
// a value had to be used that would not be truncated.
111+
dispatchClickEvent(sliderTrackElement, 0.5);
111112

112113
thumbDimensions = thumbElement.getBoundingClientRect();
113-
// The thumb's offset is expected to be equal to the slider's offset + 0.16 * the slider's
114-
// width - half the thumb width (to center the thumb).
115-
let offset = sliderDimensions.left + (sliderDimensions.width * 0.16) - (thumbWidth / 2);
116-
let difference = Math.abs(thumbDimensions.left - offset);
117-
expect(difference).toBeLessThan(1);
114+
// The thumb position should be at 50% of the slider's width + the offset of the slider.
115+
// Both the thumb and the slider are affected by this offset.
116+
expect(thumbDimensions.left).toBe(sliderDimensions.width * 0.5 + sliderDimensions.left);
118117
});
119118

120119
it('should update the track fill on drag', () => {
121120
expect(trackFillDimensions.width).toBe(0);
122121
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.86, gestureConfig);
123122

124123
trackFillDimensions = trackFillElement.getBoundingClientRect();
125-
let difference = Math.abs(trackFillDimensions.width - (sliderDimensions.width * 0.86));
126-
expect(difference).toBeLessThan(1);
124+
thumbDimensions = thumbElement.getBoundingClientRect();
125+
126+
// The thumb and track fill positions are relative to the viewport, so to get the thumb's
127+
// offset relative to the track, subtract the offset on the track fill.
128+
let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
129+
// The track fill width should be equal to the thumb's position.
130+
expect(Math.round(trackFillDimensions.width)).toBe(Math.round(thumbPosition));
127131
});
128132

129133
it('should update the thumb position on drag', () => {
130-
expect(thumbDimensions.left).toBe(sliderDimensions.left - (thumbWidth / 2));
131-
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.27, gestureConfig);
134+
expect(thumbDimensions.left).toBe(sliderDimensions.left);
135+
// The drag event also truncates the position passed in, so 50% is used here as well to
136+
// ensure the ability to calculate the expected position.
137+
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.5, gestureConfig);
132138

133139
thumbDimensions = thumbElement.getBoundingClientRect();
134-
let offset = sliderDimensions.left + (sliderDimensions.width * 0.27) - (thumbWidth / 2);
135-
let difference = Math.abs(thumbDimensions.left - offset);
136-
expect(difference).toBeLessThan(1);
140+
expect(thumbDimensions.left).toBe(sliderDimensions.width * 0.5 + sliderDimensions.left);
137141
});
138142

139143
it('should add the md-slider-active class on click', () => {
@@ -237,6 +241,9 @@ describe('MdSlider', () => {
237241
let sliderNativeElement: HTMLElement;
238242
let sliderInstance: MdSlider;
239243
let sliderTrackElement: HTMLElement;
244+
let sliderDimensions: ClientRect;
245+
let trackFillElement: HTMLElement;
246+
let thumbElement: HTMLElement;
240247

241248
beforeEach(async(() => {
242249
builder.createAsync(SliderWithMinAndMax).then(f => {
@@ -247,31 +254,62 @@ describe('MdSlider', () => {
247254
sliderNativeElement = sliderDebugElement.nativeElement;
248255
sliderInstance = sliderDebugElement.injector.get(MdSlider);
249256
sliderTrackElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track');
257+
sliderDimensions = sliderTrackElement.getBoundingClientRect();
258+
trackFillElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track-fill');
259+
thumbElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-thumb-position');
250260
});
251261
}));
252262

253263
it('should set the default values from the attributes', () => {
254-
expect(sliderInstance.value).toBe(5);
255-
expect(sliderInstance.min).toBe(5);
256-
expect(sliderInstance.max).toBe(15);
264+
expect(sliderInstance.value).toBe(4);
265+
expect(sliderInstance.min).toBe(4);
266+
expect(sliderInstance.max).toBe(6);
257267
});
258268

259269
it('should set the correct value on click', () => {
260270
dispatchClickEvent(sliderTrackElement, 0.09);
261271
// Computed by multiplying the difference between the min and the max by the percentage from
262272
// the click and adding that to the minimum.
263-
let value = 5 + (0.09 * (15 - 5));
264-
let difference = Math.abs(sliderInstance.value - value);
265-
expect(difference).toBeLessThan(1);
273+
let value = Math.round(4 + (0.09 * (6 - 4)));
274+
expect(sliderInstance.value).toBe(value);
266275
});
267276

268277
it('should set the correct value on drag', () => {
269278
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.62, gestureConfig);
270279
// Computed by multiplying the difference between the min and the max by the percentage from
271280
// the click and adding that to the minimum.
272-
let value = 5 + (0.62 * (15 - 5));
273-
let difference = Math.abs(sliderInstance.value - value);
274-
expect(difference).toBeLessThan(1);
281+
let value = Math.round(4 + (0.62 * (6 - 4)));
282+
expect(sliderInstance.value).toBe(value);
283+
});
284+
285+
it('should snap the thumb and fill to the nearest value on click', () => {
286+
dispatchClickEvent(sliderTrackElement, 0.68);
287+
fixture.detectChanges();
288+
289+
let trackFillDimensions = trackFillElement.getBoundingClientRect();
290+
let thumbDimensions = thumbElement.getBoundingClientRect();
291+
let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
292+
293+
// The closest snap is halfway on the slider.
294+
expect(thumbDimensions.left).toBe(sliderDimensions.width * 0.5 + sliderDimensions.left);
295+
expect(Math.round(trackFillDimensions.width)).toBe(Math.round(thumbPosition));
296+
});
297+
298+
it('should snap the thumb and fill to the nearest value on drag', () => {
299+
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.74, gestureConfig);
300+
fixture.detectChanges();
301+
302+
dispatchDragEndEvent(sliderNativeElement, 0.74, gestureConfig);
303+
fixture.detectChanges();
304+
305+
let trackFillDimensions = trackFillElement.getBoundingClientRect();
306+
let thumbDimensions = thumbElement.getBoundingClientRect();
307+
let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
308+
309+
// The closest snap is at the halfway point on the slider.
310+
expect(thumbDimensions.left).toBe(sliderDimensions.left + sliderDimensions.width * 0.5);
311+
expect(Math.round(trackFillDimensions.width)).toBe(Math.round(thumbPosition));
312+
275313
});
276314
});
277315

@@ -302,16 +340,85 @@ describe('MdSlider', () => {
302340
dispatchClickEvent(sliderTrackElement, 0.92);
303341
// On a slider with default max and min the value should be approximately equal to the
304342
// percentage clicked. This should be the case regardless of what the original set value was.
305-
let value = 92;
306-
let difference = Math.abs(sliderInstance.value - value);
307-
expect(difference).toBeLessThan(1);
343+
expect(sliderInstance.value).toBe(92);
308344
});
309345

310346
it('should set the correct value on drag', () => {
311347
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.32, gestureConfig);
312348
expect(sliderInstance.value).toBe(32);
313349
});
314350
});
351+
352+
describe('slider with set step', () => {
353+
let fixture: ComponentFixture<SliderWithStep>;
354+
let sliderDebugElement: DebugElement;
355+
let sliderNativeElement: HTMLElement;
356+
let sliderInstance: MdSlider;
357+
let sliderTrackElement: HTMLElement;
358+
let sliderDimensions: ClientRect;
359+
let trackFillElement: HTMLElement;
360+
let thumbElement: HTMLElement;
361+
362+
beforeEach(async(() => {
363+
builder.createAsync(SliderWithStep).then(f => {
364+
fixture = f;
365+
fixture.detectChanges();
366+
367+
sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
368+
sliderNativeElement = sliderDebugElement.nativeElement;
369+
sliderInstance = sliderDebugElement.injector.get(MdSlider);
370+
sliderTrackElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track');
371+
sliderDimensions = sliderTrackElement.getBoundingClientRect();
372+
trackFillElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track-fill');
373+
thumbElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-thumb-position');
374+
});
375+
}));
376+
377+
it('should set the correct step value on click', () => {
378+
expect(sliderInstance.value).toBe(0);
379+
380+
dispatchClickEvent(sliderTrackElement, 0.13);
381+
fixture.detectChanges();
382+
383+
expect(sliderInstance.value).toBe(25);
384+
});
385+
386+
it('should snap the thumb and fill to a step on click', () => {
387+
dispatchClickEvent(sliderNativeElement, 0.66);
388+
fixture.detectChanges();
389+
390+
let trackFillDimensions = trackFillElement.getBoundingClientRect();
391+
let thumbDimensions = thumbElement.getBoundingClientRect();
392+
let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
393+
394+
// The closest step is at 75% of the slider.
395+
expect(thumbDimensions.left).toBe(sliderDimensions.width * 0.75 + sliderDimensions.left);
396+
expect(Math.round(trackFillDimensions.width)).toBe(Math.round(thumbPosition));
397+
});
398+
399+
it('should set the correct step value on drag', () => {
400+
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.07, gestureConfig);
401+
fixture.detectChanges();
402+
403+
expect(sliderInstance.value).toBe(0);
404+
});
405+
406+
it('should snap the thumb and fill to a step on drag', () => {
407+
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.88, gestureConfig);
408+
fixture.detectChanges();
409+
410+
dispatchDragEndEvent(sliderNativeElement, 0.88, gestureConfig);
411+
fixture.detectChanges();
412+
413+
let trackFillDimensions = trackFillElement.getBoundingClientRect();
414+
let thumbDimensions = thumbElement.getBoundingClientRect();
415+
let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
416+
417+
// The closest snap is at the end of the slider.
418+
expect(thumbDimensions.left).toBe(sliderDimensions.width + sliderDimensions.left);
419+
expect(Math.round(trackFillDimensions.width)).toBe(Math.round(thumbPosition));
420+
});
421+
});
315422
});
316423

317424
// The transition has to be removed in order to test the updated positions without setTimeout.
@@ -335,7 +442,13 @@ class DisabledSlider { }
335442

336443
@Component({
337444
directives: [MD_SLIDER_DIRECTIVES],
338-
template: `<md-slider min="5" max="15"></md-slider>`
445+
template: `<md-slider min="4" max="6"></md-slider>`,
446+
styles: [`
447+
.md-slider-track-fill, .md-slider-thumb-position {
448+
transition: none !important;
449+
}
450+
`],
451+
encapsulation: ViewEncapsulation.None
339452
})
340453
class SliderWithMinAndMax { }
341454

@@ -345,8 +458,21 @@ class SliderWithMinAndMax { }
345458
})
346459
class SliderWithValue { }
347460

461+
@Component({
462+
directives: [MD_SLIDER_DIRECTIVES],
463+
template: `<md-slider step="25"></md-slider>`,
464+
styles: [`
465+
.md-slider-track-fill, .md-slider-thumb-position {
466+
transition: none !important;
467+
}
468+
`],
469+
encapsulation: ViewEncapsulation.None
470+
})
471+
class SliderWithStep { }
472+
348473
/**
349474
* Dispatches a click event from an element.
475+
* Note: The mouse event truncates the position for the click.
350476
* @param element The element from which the event will be dispatched.
351477
* @param percentage The percentage of the slider where the click should occur. Used to find the
352478
* physical location of the click.

0 commit comments

Comments
 (0)