mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(slider): support for tick marks (#924)
* Initial drawing of ticks without calculations * Calculate the optimal distance between ticks * Add support for tick interval with numbers * Add comment to perf test * Always add a tick on the end and hide the second to last one if too close * Add tests for tick marks * Comment to clarify tick interval is relative to steps. * Remove fdescribe * Move 30 into a constant for minimum auto tick separation * Remove comment that claims tick interval defaults to auto * Simplify tick interval input * Add missing parenthesis * Use toContain for background gradient tests * Improve comments * Use 'black' rather than '#000000' for tick color * Calculate auto tick separation without loop * Better explain the calculations for the auto tick location. * Merge with NgModules
This commit is contained in:
committed by
Jeremy Elbourn
parent
d1fdcfaa06
commit
ae5717c3a4
@@ -5,6 +5,8 @@
|
||||
<div class="md-slider-track-container">
|
||||
<div class="md-slider-track"></div>
|
||||
<div class="md-slider-track md-slider-track-fill"></div>
|
||||
<div class="md-slider-tick-container"></div>
|
||||
<div class="md-slider-last-tick-container"></div>
|
||||
</div>
|
||||
<div class="md-slider-thumb-container">
|
||||
<div class="md-slider-thumb-position">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<SliderWithAutoTickInterval>;
|
||||
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 = <HTMLElement>sliderNativeElement.querySelector('.md-slider-tick-container');
|
||||
lastTickContainer =
|
||||
<HTMLElement>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<SliderWithSetTickInterval>;
|
||||
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 = <HTMLElement>sliderNativeElement.querySelector('.md-slider-tick-container');
|
||||
lastTickContainer =
|
||||
<HTMLElement>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: `<md-slider step="5" tick-interval="auto"></md-slider>`
|
||||
})
|
||||
class SliderWithAutoTickInterval { }
|
||||
|
||||
@Component({
|
||||
template: `<md-slider step="3" tick-interval="6"></md-slider>`
|
||||
})
|
||||
class SliderWithSetTickInterval { }
|
||||
|
||||
/**
|
||||
* Dispatches a click event from an element.
|
||||
* Note: The mouse event truncates the position for the click.
|
||||
|
||||
@@ -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 = <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 = <HTMLElement>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 =
|
||||
<HTMLElement>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];
|
||||
|
||||
@@ -26,3 +26,7 @@
|
||||
<md-slider min="1" max="100" step="20" #slider5></md-slider>
|
||||
{{slider5.value}}
|
||||
</section>
|
||||
|
||||
<h1>Slider with set tick interval</h1>
|
||||
<md-slider tick-interval="auto"></md-slider>
|
||||
<md-slider tick-interval="9"></md-slider>
|
||||
|
||||
Reference in New Issue
Block a user