Skip to content

Add discrete steps and snapping to the slider #904

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/components/slider/slider.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ $md-slider-disabled-color: rgba(black, 0.26);
@mixin slider-thumb-position($width: $md-slider-thumb-size, $height: $md-slider-thumb-size) {
position: absolute;
top: center-vertically($md-slider-thickness, $height);
// This makes it so that the center of the thumb aligns with where the click was.
// This is not affected by the movement of the thumb.
left: (-$width / 2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some commenting here that explains how the thumb is positioned?

width: $width;
height: $height;
border-radius: max($width, $height);
Expand Down
200 changes: 163 additions & 37 deletions src/components/slider/slider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ describe('MdSlider', () => {
let sliderDimensions: ClientRect;
let thumbElement: HTMLElement;
let thumbDimensions: ClientRect;
let thumbWidth: number;

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

thumbElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-thumb-position');
thumbDimensions = thumbElement.getBoundingClientRect();
thumbWidth =
sliderNativeElement.querySelector('.md-slider-thumb').getBoundingClientRect().width;
});
}));

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

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

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

trackFillDimensions = trackFillElement.getBoundingClientRect();
// The fill should be close to the slider's width * the percentage from the click.
let difference = Math.abs(trackFillDimensions.width - (sliderDimensions.width * 0.39));
expect(difference).toBeLessThan(1);
thumbDimensions = thumbElement.getBoundingClientRect();

// The thumb and track fill positions are relative to the viewport, so to get the thumb's
// offset relative to the track, subtract the offset on the track fill.
let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
// The track fill width should be equal to the thumb's position.
expect(Math.round(trackFillDimensions.width)).toBe(Math.round(thumbPosition));
});

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

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

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

trackFillDimensions = trackFillElement.getBoundingClientRect();
let difference = Math.abs(trackFillDimensions.width - (sliderDimensions.width * 0.86));
expect(difference).toBeLessThan(1);
thumbDimensions = thumbElement.getBoundingClientRect();

// The thumb and track fill positions are relative to the viewport, so to get the thumb's
// offset relative to the track, subtract the offset on the track fill.
let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
// The track fill width should be equal to the thumb's position.
expect(Math.round(trackFillDimensions.width)).toBe(Math.round(thumbPosition));
});

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

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

it('should add the md-slider-active class on click', () => {
Expand Down Expand Up @@ -237,6 +241,9 @@ describe('MdSlider', () => {
let sliderNativeElement: HTMLElement;
let sliderInstance: MdSlider;
let sliderTrackElement: HTMLElement;
let sliderDimensions: ClientRect;
let trackFillElement: HTMLElement;
let thumbElement: HTMLElement;

beforeEach(async(() => {
builder.createAsync(SliderWithMinAndMax).then(f => {
Expand All @@ -247,31 +254,62 @@ describe('MdSlider', () => {
sliderNativeElement = sliderDebugElement.nativeElement;
sliderInstance = sliderDebugElement.injector.get(MdSlider);
sliderTrackElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track');
sliderDimensions = sliderTrackElement.getBoundingClientRect();
trackFillElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track-fill');
thumbElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-thumb-position');
});
}));

it('should set the default values from the attributes', () => {
expect(sliderInstance.value).toBe(5);
expect(sliderInstance.min).toBe(5);
expect(sliderInstance.max).toBe(15);
expect(sliderInstance.value).toBe(4);
expect(sliderInstance.min).toBe(4);
expect(sliderInstance.max).toBe(6);
});

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

it('should set the correct value on drag', () => {
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.62, gestureConfig);
// Computed by multiplying the difference between the min and the max by the percentage from
// the click and adding that to the minimum.
let value = 5 + (0.62 * (15 - 5));
let difference = Math.abs(sliderInstance.value - value);
expect(difference).toBeLessThan(1);
let value = Math.round(4 + (0.62 * (6 - 4)));
expect(sliderInstance.value).toBe(value);
});

it('should snap the thumb and fill to the nearest value on click', () => {
dispatchClickEvent(sliderTrackElement, 0.68);
fixture.detectChanges();

let trackFillDimensions = trackFillElement.getBoundingClientRect();
let thumbDimensions = thumbElement.getBoundingClientRect();
let thumbPosition = thumbDimensions.left - trackFillDimensions.left;

// The closest snap is halfway on the slider.
expect(thumbDimensions.left).toBe(sliderDimensions.width * 0.5 + sliderDimensions.left);
expect(Math.round(trackFillDimensions.width)).toBe(Math.round(thumbPosition));
});

it('should snap the thumb and fill to the nearest value on drag', () => {
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.74, gestureConfig);
fixture.detectChanges();

dispatchDragEndEvent(sliderNativeElement, 0.74, gestureConfig);
fixture.detectChanges();

let trackFillDimensions = trackFillElement.getBoundingClientRect();
let thumbDimensions = thumbElement.getBoundingClientRect();
let thumbPosition = thumbDimensions.left - trackFillDimensions.left;

// The closest snap is at the halfway point on the slider.
expect(thumbDimensions.left).toBe(sliderDimensions.left + sliderDimensions.width * 0.5);
expect(Math.round(trackFillDimensions.width)).toBe(Math.round(thumbPosition));

});
});

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

it('should set the correct value on drag', () => {
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.32, gestureConfig);
expect(sliderInstance.value).toBe(32);
});
});

describe('slider with set step', () => {
let fixture: ComponentFixture<SliderWithStep>;
let sliderDebugElement: DebugElement;
let sliderNativeElement: HTMLElement;
let sliderInstance: MdSlider;
let sliderTrackElement: HTMLElement;
let sliderDimensions: ClientRect;
let trackFillElement: HTMLElement;
let thumbElement: HTMLElement;

beforeEach(async(() => {
builder.createAsync(SliderWithStep).then(f => {
fixture = f;
fixture.detectChanges();

sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
sliderNativeElement = sliderDebugElement.nativeElement;
sliderInstance = sliderDebugElement.injector.get(MdSlider);
sliderTrackElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track');
sliderDimensions = sliderTrackElement.getBoundingClientRect();
trackFillElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track-fill');
thumbElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-thumb-position');
});
}));

it('should set the correct step value on click', () => {
expect(sliderInstance.value).toBe(0);

dispatchClickEvent(sliderTrackElement, 0.13);
fixture.detectChanges();

expect(sliderInstance.value).toBe(25);
});

it('should snap the thumb and fill to a step on click', () => {
dispatchClickEvent(sliderNativeElement, 0.66);
fixture.detectChanges();

let trackFillDimensions = trackFillElement.getBoundingClientRect();
let thumbDimensions = thumbElement.getBoundingClientRect();
let thumbPosition = thumbDimensions.left - trackFillDimensions.left;

// The closest step is at 75% of the slider.
expect(thumbDimensions.left).toBe(sliderDimensions.width * 0.75 + sliderDimensions.left);
expect(Math.round(trackFillDimensions.width)).toBe(Math.round(thumbPosition));
});

it('should set the correct step value on drag', () => {
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.07, gestureConfig);
fixture.detectChanges();

expect(sliderInstance.value).toBe(0);
});

it('should snap the thumb and fill to a step on drag', () => {
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.88, gestureConfig);
fixture.detectChanges();

dispatchDragEndEvent(sliderNativeElement, 0.88, gestureConfig);
fixture.detectChanges();

let trackFillDimensions = trackFillElement.getBoundingClientRect();
let thumbDimensions = thumbElement.getBoundingClientRect();
let thumbPosition = thumbDimensions.left - trackFillDimensions.left;

// The closest snap is at the end of the slider.
expect(thumbDimensions.left).toBe(sliderDimensions.width + sliderDimensions.left);
expect(Math.round(trackFillDimensions.width)).toBe(Math.round(thumbPosition));
});
});
});

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

@Component({
directives: [MD_SLIDER_DIRECTIVES],
template: `<md-slider min="5" max="15"></md-slider>`
template: `<md-slider min="4" max="6"></md-slider>`,
styles: [`
.md-slider-track-fill, .md-slider-thumb-position {
transition: none !important;
}
`],
encapsulation: ViewEncapsulation.None
})
class SliderWithMinAndMax { }

Expand All @@ -345,8 +458,21 @@ class SliderWithMinAndMax { }
})
class SliderWithValue { }

@Component({
directives: [MD_SLIDER_DIRECTIVES],
template: `<md-slider step="25"></md-slider>`,
styles: [`
.md-slider-track-fill, .md-slider-thumb-position {
transition: none !important;
}
`],
encapsulation: ViewEncapsulation.None
})
class SliderWithStep { }

/**
* Dispatches a click event from an element.
* Note: The mouse event truncates the position for the click.
* @param element The element from which the event will be dispatched.
* @param percentage The percentage of the slider where the click should occur. Used to find the
* physical location of the click.
Expand Down
Loading