diff --git a/src/components/slider/slider.html b/src/components/slider/slider.html
index 0d126665dc3d..caf03c84addb 100644
--- a/src/components/slider/slider.html
+++ b/src/components/slider/slider.html
@@ -5,6 +5,8 @@
diff --git a/src/components/slider/slider.scss b/src/components/slider/slider.scss
index e575fcb612a6..fb57d8fad810 100644
--- a/src/components/slider/slider.scss
+++ b/src/components/slider/slider.scss
@@ -95,6 +95,13 @@ md-slider *::after {
background-color: md-color($md-accent);
}
+.md-slider-tick-container, .md-slider-last-tick-container {
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: 100%;
+}
+
.md-slider-thumb-container {
position: absolute;
left: 0;
diff --git a/src/components/slider/slider.spec.ts b/src/components/slider/slider.spec.ts
index 8185540f294d..b22bd0a96ed5 100644
--- a/src/components/slider/slider.spec.ts
+++ b/src/components/slider/slider.spec.ts
@@ -25,6 +25,8 @@ describe('MdSlider', () => {
SliderWithMinAndMax,
SliderWithValue,
SliderWithStep,
+ SliderWithAutoTickInterval,
+ SliderWithSetTickInterval
],
});
@@ -434,6 +436,86 @@ describe('MdSlider', () => {
expect(Math.round(trackFillDimensions.width)).toBe(Math.round(thumbPosition));
});
});
+
+ describe('slider with auto ticks', () => {
+ let fixture: ComponentFixture;
+ let sliderDebugElement: DebugElement;
+ let sliderNativeElement: HTMLElement;
+ let tickContainer: HTMLElement;
+ let lastTickContainer: HTMLElement;
+
+ beforeEach(async(() => {
+ builder.createAsync(SliderWithAutoTickInterval).then(f => {
+ fixture = f;
+ fixture.detectChanges();
+
+ sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
+ sliderNativeElement = sliderDebugElement.nativeElement;
+ tickContainer = sliderNativeElement.querySelector('.md-slider-tick-container');
+ lastTickContainer =
+ sliderNativeElement.querySelector('.md-slider-last-tick-container');
+ });
+ }));
+
+ it('should set the correct tick separation', () => {
+ // The first tick mark is going to be at value 30 as it is the first step after 30px. The
+ // width of the slider is 112px because the minimum width is 128px with padding of 8px on
+ // both sides. The value 30 will be located at the position 33.6px, and 1px is removed from
+ // the tick mark location in order to center the tick. Therefore, the tick separation should
+ // be 32.6px.
+ // toContain is used rather than toBe because FireFox adds 'transparent' to the beginning
+ // of the background before the repeating linear gradient.
+ expect(tickContainer.style.background).toContain('repeating-linear-gradient(to right, ' +
+ 'black, black 2px, transparent 2px, transparent 32.6px)');
+ });
+
+ it('should draw a tick mark on the end of the track', () => {
+ expect(lastTickContainer.style.background).toContain('linear-gradient(to left, black, black' +
+ ' 2px, transparent 2px, transparent)');
+ });
+
+ it('should not draw the second to last tick when it is too close to the last tick', () => {
+ // When the second to last tick is too close (less than half the tick separation) to the last
+ // one, the tick container width is cut by the tick separation, which removes the second to
+ // last tick. Since the width of the slider is 112px and the tick separation is 33.6px, the
+ // tick container width should be 78.4px (112 - 33.6).
+ expect(tickContainer.style.width).toBe('78.4px');
+ });
+ });
+
+ describe('slider with set tick interval', () => {
+ let fixture: ComponentFixture;
+ let sliderDebugElement: DebugElement;
+ let sliderNativeElement: HTMLElement;
+ let tickContainer: HTMLElement;
+ let lastTickContainer: HTMLElement;
+
+ beforeEach(async(() => {
+ builder.createAsync(SliderWithSetTickInterval).then(f => {
+ fixture = f;
+ fixture.detectChanges();
+
+ sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
+ sliderNativeElement = sliderDebugElement.nativeElement;
+ tickContainer = sliderNativeElement.querySelector('.md-slider-tick-container');
+ lastTickContainer =
+ sliderNativeElement.querySelector('.md-slider-last-tick-container');
+ });
+ }));
+
+ it('should set the correct tick separation', () => {
+ // The slider width is 112px, the first step is at value 18 (step of 3 * tick interval of 6),
+ // which is at the position 20.16px and 1px is subtracted to center, giving a tick
+ // separation of 19.16px.
+ expect(tickContainer.style.background).toContain('repeating-linear-gradient(to right, ' +
+ 'black, black 2px, transparent 2px, transparent 19.16px)');
+ });
+
+ it('should draw a tick mark on the end of the track', () => {
+ expect(lastTickContainer.style.background).toContain('linear-gradient(to left, black, '
+ + 'black 2px, transparent 2px, transparent)');
+ });
+ });
});
// The transition has to be removed in order to test the updated positions without setTimeout.
@@ -480,6 +562,16 @@ class SliderWithValue { }
})
class SliderWithStep { }
+@Component({
+ template: ``
+})
+class SliderWithAutoTickInterval { }
+
+@Component({
+ template: ``
+})
+class SliderWithSetTickInterval { }
+
/**
* Dispatches a click event from an element.
* Note: The mouse event truncates the position for the click.
diff --git a/src/components/slider/slider.ts b/src/components/slider/slider.ts
index 10ea4cc7469f..317249f5b6a2 100644
--- a/src/components/slider/slider.ts
+++ b/src/components/slider/slider.ts
@@ -12,6 +12,12 @@ import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value
import {applyCssTransform} from '@angular2-material/core/style/apply-transform';
import {MdGestureConfig} from '@angular2-material/core/core';
+/**
+ * Visually, a 30px separation between tick marks looks best. This is very subjective but it is
+ * the default separation we chose.
+ */
+const MIN_AUTO_TICK_SEPARATION = 30;
+
@Component({
moduleId: module.id,
selector: 'md-slider',
@@ -53,6 +59,12 @@ export class MdSlider implements AfterContentInit {
/** The values at which the thumb will snap. */
@Input() step: number = 1;
+ /**
+ * How often to show ticks. Relative to the step so that a tick always appears on a step.
+ * Ex: Tick interval of 4 with a step of 3 will draw a tick every 4 steps (every 12 values).
+ */
+ @Input('tick-interval') private _tickInterval: 'auto' | number;
+
/**
* Whether or not the thumb is sliding.
* Used to determine if there should be a transition for the thumb and fill track.
@@ -122,6 +134,7 @@ export class MdSlider implements AfterContentInit {
ngAfterContentInit() {
this._sliderDimensions = this._renderer.getSliderDimensions();
this.snapToValue();
+ this._updateTickSeparation();
}
/** TODO: internal */
@@ -186,7 +199,7 @@ export class MdSlider implements AfterContentInit {
* This is also used to move the thumb to a snapped value once sliding is done.
*/
updatePercentFromValue() {
- this._percent = (this.value - this.min) / (this.max - this.min);
+ this._percent = this.calculatePercentage(this.value);
}
/**
@@ -198,7 +211,7 @@ export class MdSlider implements AfterContentInit {
// The exact value is calculated from the event and used to find the closest snap value.
this._percent = this.clamp((pos - offset) / size);
- let exactValue = this.min + (this._percent * (this.max - this.min));
+ let exactValue = this.calculateValue(this._percent);
// This calculation finds the closest step by finding the closest whole number divisible by the
// step relative to the min.
@@ -217,6 +230,80 @@ export class MdSlider implements AfterContentInit {
this._renderer.updateThumbAndFillPosition(this._percent, this._sliderDimensions.width);
}
+ /**
+ * Calculates the separation in pixels of tick marks. If there is no tick interval or the interval
+ * is set to something other than a number or 'auto', nothing happens.
+ */
+ private _updateTickSeparation() {
+ if (this._tickInterval == 'auto') {
+ this._updateAutoTickSeparation();
+ } else if (Number(this._tickInterval)) {
+ this._updateTickSeparationFromInterval();
+ }
+ }
+
+ /**
+ * Calculates the optimal separation in pixels of tick marks based on the minimum auto tick
+ * separation constant.
+ */
+ private _updateAutoTickSeparation() {
+ // We're looking for the multiple of step for which the separation between is greater than the
+ // minimum tick separation.
+ let sliderWidth = this._sliderDimensions.width;
+
+ // This is the total "width" of the slider in terms of values.
+ let valueWidth = this.max - this.min;
+
+ // Calculate how many values exist within 1px on the slider.
+ let valuePerPixel = valueWidth / sliderWidth;
+
+ // Calculate how many values exist in the minimum tick separation (px).
+ let valuePerSeparation = valuePerPixel * MIN_AUTO_TICK_SEPARATION;
+
+ // Calculate how many steps exist in this separation. This will be the lowest value you can
+ // multiply step by to get a separation that is greater than or equal to the minimum tick
+ // separation.
+ let stepsPerSeparation = Math.ceil(valuePerSeparation / this.step);
+
+ // Get the percentage of the slider for which this tick would be located so we can then draw
+ // it on the slider.
+ let tickPercentage = this.calculatePercentage((this.step * stepsPerSeparation) + this.min);
+
+ // The pixel value of the tick is the percentage * the width of the slider. Use this to draw
+ // the ticks on the slider.
+ this._renderer.drawTicks(sliderWidth * tickPercentage);
+ }
+
+ /**
+ * Calculates the separation of tick marks by finding the pixel value of the tickInterval.
+ */
+ private _updateTickSeparationFromInterval() {
+ // Force tickInterval to be a number so it can be used in calculations.
+ let interval: number = this._tickInterval;
+ // Calculate the first value a tick will be located at by getting the step at which the interval
+ // lands and adding that to the min.
+ let tickValue = (this.step * interval) + this.min;
+
+ // The percentage of the step on the slider is needed in order to calculate the pixel offset
+ // from the beginning of the slider. This offset is the tick separation.
+ let tickPercentage = this.calculatePercentage(tickValue);
+ this._renderer.drawTicks(this._sliderDimensions.width * tickPercentage);
+ }
+
+ /**
+ * Calculates the percentage of the slider that a value is.
+ */
+ calculatePercentage(value: number) {
+ return (value - this.min) / (this.max - this.min);
+ }
+
+ /**
+ * Calculates the value a percentage of the slider corresponds to.
+ */
+ calculateValue(percentage: number) {
+ return this.min + (percentage * (this.max - this.min));
+ }
+
/**
* Return a number between two numbers.
*/
@@ -267,6 +354,32 @@ export class SliderRenderer {
addFocus() {
this._sliderElement.focus();
}
+
+ /**
+ * Draws ticks onto the tick container.
+ */
+ drawTicks(tickSeparation: number) {
+ let tickContainer = this._sliderElement.querySelector('.md-slider-tick-container');
+ let tickContainerWidth = tickContainer.getBoundingClientRect().width;
+ // An extra element for the last tick is needed because the linear gradient cannot be told to
+ // always draw a tick at the end of the gradient. To get around this, there is a second
+ // container for ticks that has a single tick mark on the very right edge.
+ let lastTickContainer =
+ this._sliderElement.querySelector('.md-slider-last-tick-container');
+ // Subtract 1 from the tick separation to center the tick.
+ // TODO: Evaluate the rendering performance of using repeating background gradients.
+ tickContainer.style.background = `repeating-linear-gradient(to right, black, black 2px, ` +
+ `transparent 2px, transparent ${tickSeparation - 1}px)`;
+ // Add a tick to the very end by starting on the right side and adding a 2px black line.
+ lastTickContainer.style.background = `linear-gradient(to left, black, black 2px, transparent ` +
+ `2px, transparent)`;
+
+ // If the second to last tick is too close (a separation of less than half the normal
+ // separation), don't show it by decreasing the width of the tick container element.
+ if (tickContainerWidth % tickSeparation < (tickSeparation / 2)) {
+ tickContainer.style.width = tickContainerWidth - tickSeparation + 'px';
+ }
+ }
}
export const MD_SLIDER_DIRECTIVES = [MdSlider];
diff --git a/src/demo-app/slider/slider-demo.html b/src/demo-app/slider/slider-demo.html
index a71fd84a34e8..e4c39cf435c1 100644
--- a/src/demo-app/slider/slider-demo.html
+++ b/src/demo-app/slider/slider-demo.html
@@ -26,3 +26,7 @@ Slider with step defined
{{slider5.value}}
+
+Slider with set tick interval
+
+