feat(md-slider): initial version for md-slider (#779)

This commit is contained in:
Ivey Padgett
2016-07-14 14:43:00 -07:00
committed by Kara
parent e324e4750e
commit 8354750a45
11 changed files with 912 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
<div class="md-slider-wrapper">
<div class="md-slider-container"
[class.md-slider-dragging]="isDragging"
[class.md-slider-active]="isActive">
<div class="md-slider-track-container">
<div class="md-slider-track"></div>
<div class="md-slider-track md-slider-track-fill"></div>
</div>
<div class="md-slider-thumb-container">
<div class="md-slider-thumb-position">
<div class="md-slider-thumb"></div>
</div>
</div>
</div>
</div>
+143
View File
@@ -0,0 +1,143 @@
@import 'default-theme';
@import '_variables';
// This refers to the thickness of the slider. On a horizontal slider this is the height, on a
// vertical slider this is the width.
$md-slider-thickness: 20px !default;
$md-slider-min-size: 128px !default;
$md-slider-padding: 8px !default;
$md-slider-track-height: 2px !default;
$md-slider-thumb-size: 20px !default;
$md-slider-thumb-default-scale: 0.7 !default;
$md-slider-thumb-focus-scale: 1 !default;
// TODO(iveysaur): Find an implementation to hide the track under a disabled thumb.
$md-slider-off-color: rgba(black, 0.26);
$md-slider-focused-color: rgba(black, 0.38);
$md-slider-disabled-color: rgba(black, 0.26);
/**
* Uses a container height and an item height to center an item vertically within the container.
*/
@function center-vertically($containerHeight, $itemHeight) {
@return ($containerHeight / 2) - ($itemHeight / 2);
}
/**
* Positions the thumb based on its width and height.
*/
@mixin slider-thumb-position($width: $md-slider-thumb-size, $height: $md-slider-thumb-size) {
position: absolute;
top: center-vertically($md-slider-thickness, $height);
width: $width;
height: $height;
border-radius: max($width, $height);
}
md-slider {
height: $md-slider-thickness;
min-width: $md-slider-min-size;
position: relative;
padding: 0;
display: inline-block;
outline: none;
vertical-align: middle;
}
md-slider *,
md-slider *::after {
box-sizing: border-box;
}
/**
* Exists in order to pad the slider and keep everything positioned correctly.
* Cannot be merged with the .md-slider-container.
*/
.md-slider-wrapper {
width: 100%;
height: 100%;
padding-left: $md-slider-padding;
padding-right: $md-slider-padding;
}
/**
* Holds the isActive and isDragging classes as well as helps with positioning the children.
* Cannot be merged with .md-slider-wrapper.
*/
.md-slider-container {
position: relative;
}
.md-slider-track-container {
width: 100%;
position: absolute;
top: center-vertically($md-slider-thickness, $md-slider-track-height);
height: $md-slider-track-height;
}
.md-slider-track {
position: absolute;
left: 0;
right: 0;
height: 100%;
background-color: $md-slider-off-color;
}
.md-slider-track-fill {
transition-duration: $swift-ease-out-duration;
transition-timing-function: $swift-ease-out-timing-function;
transition-property: width, height;
background-color: md-color($md-accent);
}
.md-slider-thumb-container {
position: absolute;
left: 0;
top: 50%;
transform: translate3d(-50%, -50%, 0);
transition-duration: $swift-ease-out-duration;
transition-timing-function: $swift-ease-out-timing-function;
transition-property: left, bottom;
}
.md-slider-thumb-position {
transition: transform $swift-ease-out-duration $swift-ease-out-timing-function;
}
.md-slider-thumb {
z-index: 1;
@include slider-thumb-position($md-slider-thumb-size, $md-slider-thumb-size);
transform: scale($md-slider-thumb-default-scale);
transition: transform $swift-ease-out-duration $swift-ease-out-timing-function;
}
.md-slider-thumb::after {
content: '';
position: absolute;
width: $md-slider-thumb-size;
height: $md-slider-thumb-size;
border-radius: max($md-slider-thumb-size, $md-slider-thumb-size);
border-width: 3px;
border-style: solid;
transition: inherit;
background-color: md-color($md-accent);
border-color: md-color($md-accent);
}
.md-slider-dragging .md-slider-thumb-position,
.md-slider-dragging .md-slider-track-fill {
transition: none;
cursor: default;
}
.md-slider-active .md-slider-thumb {
transform: scale($md-slider-thumb-focus-scale);
}
.md-slider-disabled .md-slider-thumb::after {
background-color: $md-slider-disabled-color;
border-color: $md-slider-disabled-color;
}
+425
View File
@@ -0,0 +1,425 @@
import {
addProviders,
inject,
async,
} from '@angular/core/testing';
import {TestComponentBuilder, ComponentFixture} from '@angular/compiler/testing';
import {Component, DebugElement, ViewEncapsulation} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MdSlider, MD_SLIDER_DIRECTIVES} from './slider';
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {TestGestureConfig} from './test-gesture-config';
describe('MdSlider', () => {
let builder: TestComponentBuilder;
let gestureConfig: TestGestureConfig;
beforeEach(() => {
addProviders([
{provide: HAMMER_GESTURE_CONFIG, useFactory: () => {
gestureConfig = new TestGestureConfig();
return gestureConfig;
}}
]);
});
beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
builder = tcb;
}));
describe('standard slider', () => {
let fixture: ComponentFixture<StandardSlider>;
let sliderDebugElement: DebugElement;
let sliderNativeElement: HTMLElement;
let sliderInstance: MdSlider;
let trackFillElement: HTMLElement;
let trackFillDimensions: ClientRect;
let sliderTrackElement: HTMLElement;
let sliderDimensions: ClientRect;
let thumbElement: HTMLElement;
let thumbDimensions: ClientRect;
let thumbWidth: number;
beforeEach(async(() => {
builder.createAsync(StandardSlider).then(f => {
fixture = f;
fixture.detectChanges();
sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
sliderNativeElement = sliderDebugElement.nativeElement;
sliderInstance = sliderDebugElement.componentInstance;
trackFillElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track-fill');
trackFillDimensions = trackFillElement.getBoundingClientRect();
sliderTrackElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track');
sliderDimensions = sliderTrackElement.getBoundingClientRect();
thumbElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-thumb-position');
thumbDimensions = thumbElement.getBoundingClientRect();
thumbWidth =
sliderNativeElement.querySelector('.md-slider-thumb').getBoundingClientRect().width;
});
}));
it('should set the default values', () => {
expect(sliderInstance.value).toBe(0);
expect(sliderInstance.min).toBe(0);
expect(sliderInstance.max).toBe(100);
});
it('should update the value on a click', () => {
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);
});
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);
});
it('should set the value as min when dragging before the track', () => {
expect(sliderInstance.value).toBe(0);
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, -1.33, gestureConfig);
expect(sliderInstance.value).toBe(0);
});
it('should set the value as max when dragging past the track', () => {
expect(sliderInstance.value).toBe(0);
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 1.75, gestureConfig);
expect(sliderInstance.value).toBe(100);
});
it('should update the track fill on click', () => {
expect(trackFillDimensions.width).toBe(0);
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);
});
it('should update the thumb position on click', () => {
expect(thumbDimensions.left).toBe(sliderDimensions.left - (thumbWidth / 2));
dispatchClickEvent(sliderTrackElement, 0.16);
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);
});
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);
});
it('should update the thumb position on drag', () => {
expect(thumbDimensions.left).toBe(sliderDimensions.left - (thumbWidth / 2));
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.27, 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);
});
it('should add the md-slider-active class on click', () => {
let containerElement = sliderNativeElement.querySelector('.md-slider-container');
expect(containerElement.classList).not.toContain('md-slider-active');
dispatchClickEvent(sliderNativeElement, 0.23);
fixture.detectChanges();
expect(containerElement.classList).toContain('md-slider-active');
});
it('should remove the md-slider-active class on blur', () => {
let containerElement = sliderNativeElement.querySelector('.md-slider-container');
dispatchClickEvent(sliderNativeElement, 0.95);
fixture.detectChanges();
expect(containerElement.classList).toContain('md-slider-active');
// Call the `onBlur` handler directly because we cannot simulate a focus event in unit tests.
sliderInstance.onBlur();
fixture.detectChanges();
expect(containerElement.classList).not.toContain('md-slider-active');
});
it('should add and remove the md-slider-dragging class when dragging', () => {
let containerElement = sliderNativeElement.querySelector('.md-slider-container');
expect(containerElement.classList).not.toContain('md-slider-dragging');
dispatchDragStartEvent(sliderNativeElement, 0, gestureConfig);
fixture.detectChanges();
expect(containerElement.classList).toContain('md-slider-dragging');
dispatchDragEndEvent(sliderNativeElement, 0.34, gestureConfig);
fixture.detectChanges();
expect(containerElement.classList).not.toContain('md-slider-dragging');
});
});
describe('disabled slider', () => {
let fixture: ComponentFixture<StandardSlider>;
let sliderDebugElement: DebugElement;
let sliderNativeElement: HTMLElement;
let sliderInstance: MdSlider;
beforeEach(async(() => {
builder.createAsync(DisabledSlider).then(f => {
fixture = f;
fixture.detectChanges();
sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
sliderNativeElement = sliderDebugElement.nativeElement;
sliderInstance = sliderDebugElement.componentInstance;
});
}));
it('should be disabled', () => {
expect(sliderInstance.disabled).toBeTruthy();
});
it('should not change the value on click when disabled', () => {
expect(sliderInstance.value).toBe(0);
dispatchClickEvent(sliderNativeElement, 0.63);
expect(sliderInstance.value).toBe(0);
});
it('should not change the value on drag when disabled', () => {
expect(sliderInstance.value).toBe(0);
dispatchDragEvent(sliderNativeElement, sliderNativeElement, 0, 0.5, gestureConfig);
expect(sliderInstance.value).toBe(0);
});
it('should not add the md-slider-active class on click when disabled', () => {
let containerElement = sliderNativeElement.querySelector('.md-slider-container');
expect(containerElement.classList).not.toContain('md-slider-active');
dispatchClickEvent(sliderNativeElement, 0.43);
fixture.detectChanges();
expect(containerElement.classList).not.toContain('md-slider-active');
});
it('should not add the md-slider-dragging class on drag when disabled', () => {
let containerElement = sliderNativeElement.querySelector('.md-slider-container');
expect(containerElement.classList).not.toContain('md-slider-dragging');
dispatchDragStartEvent(sliderNativeElement, 0.46, gestureConfig);
fixture.detectChanges();
expect(containerElement.classList).not.toContain('md-slider-dragging');
});
});
describe('slider with set min and max', () => {
let fixture: ComponentFixture<SliderWithMinAndMax>;
let sliderDebugElement: DebugElement;
let sliderNativeElement: HTMLElement;
let sliderInstance: MdSlider;
let sliderTrackElement: HTMLElement;
beforeEach(async(() => {
builder.createAsync(SliderWithMinAndMax).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');
});
}));
it('should set the default values from the attributes', () => {
expect(sliderInstance.value).toBe(5);
expect(sliderInstance.min).toBe(5);
expect(sliderInstance.max).toBe(15);
});
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);
});
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);
});
});
describe('slider with set value', () => {
let fixture: ComponentFixture<SliderWithValue>;
let sliderDebugElement: DebugElement;
let sliderNativeElement: HTMLElement;
let sliderInstance: MdSlider;
let sliderTrackElement: HTMLElement;
beforeEach(async(() => {
builder.createAsync(SliderWithValue).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');
});
}));
it('should set the default value from the attribute', () => {
expect(sliderInstance.value).toBe(26);
});
it('should set the correct value on click', () => {
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);
});
it('should set the correct value on drag', () => {
dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.32, gestureConfig);
expect(sliderInstance.value).toBe(32);
});
});
});
// The transition has to be removed in order to test the updated positions without setTimeout.
@Component({
directives: [MD_SLIDER_DIRECTIVES],
template: `<md-slider></md-slider>`,
styles: [`
.md-slider-track-fill, .md-slider-thumb-position {
transition: none !important;
}
`],
encapsulation: ViewEncapsulation.None
})
class StandardSlider { }
@Component({
directives: [MD_SLIDER_DIRECTIVES],
template: `<md-slider disabled></md-slider>`
})
class DisabledSlider { }
@Component({
directives: [MD_SLIDER_DIRECTIVES],
template: `<md-slider min="5" max="15"></md-slider>`
})
class SliderWithMinAndMax { }
@Component({
directives: [MD_SLIDER_DIRECTIVES],
template: `<md-slider value="26"></md-slider>`
})
class SliderWithValue { }
/**
* Dispatches a click event from an element.
* @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.
*/
function dispatchClickEvent(element: HTMLElement, percentage: number): void {
let dimensions = element.getBoundingClientRect();
let y = dimensions.top;
let x = dimensions.left + (dimensions.width * percentage);
let event = document.createEvent('MouseEvent');
event.initMouseEvent(
'click', true, true, window, 0, x, y, x, y, false, false, false, false, 0, null);
element.dispatchEvent(event);
}
/**
* Dispatches a drag event from an element.
* @param trackElement The track element from which the event location will be calculated.
* @param containerElement The container element from which the event will be dispatched.
* @param startPercent The percentage of the slider where the drag will begin.
* @param endPercent The percentage of the slider where the drag will end.
* @param gestureConfig The gesture config for the test to handle emitting the drag events.
*/
function dispatchDragEvent(trackElement: HTMLElement, containerElement: HTMLElement,
startPercent: number, endPercent: number,
gestureConfig: TestGestureConfig): void {
let dimensions = trackElement.getBoundingClientRect();
let startX = dimensions.left + (dimensions.width * startPercent);
let endX = dimensions.left + (dimensions.width * endPercent);
gestureConfig.emitEventForElement('dragstart', containerElement, {
// The actual event has a center with an x value that the drag listener is looking for.
center: { x: startX },
// The event needs a source event with a prevent default so we fake one.
srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
});
gestureConfig.emitEventForElement('drag', containerElement, {
center: { x: endX },
srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
});
}
/**
* Dispatches a dragstart event from an element.
* @param element The element from which the event will be dispatched.
* @param startPercent The percentage of the slider where the drag will begin.
* @param gestureConfig The gesture config for the test to handle emitting the drag events.
*/
function dispatchDragStartEvent(element: HTMLElement, startPercent: number,
gestureConfig: TestGestureConfig): void {
let dimensions = element.getBoundingClientRect();
let x = dimensions.left + (dimensions.width * startPercent);
gestureConfig.emitEventForElement('dragstart', element, {
center: { x: x },
srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
});
}
/**
* Dispatches a dragend event from an element.
* @param element The element from which the event will be dispatched.
* @param endPercent The percentage of the slider where the drag will end.
* @param gestureConfig The gesture config for the test to handle emitting the drag events.
*/
function dispatchDragEndEvent(element: HTMLElement, endPercent: number,
gestureConfig: TestGestureConfig): void {
let dimensions = element.getBoundingClientRect();
let x = dimensions.left + (dimensions.width * endPercent);
gestureConfig.emitEventForElement('dragend', element, {
center: { x: x },
srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
});
}
+253
View File
@@ -0,0 +1,253 @@
import {
Component,
ElementRef,
HostBinding,
Input,
ViewEncapsulation,
AfterContentInit,
} from '@angular/core';
import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value';
import {applyCssTransform} from '@angular2-material/core/style/apply-transform';
@Component({
moduleId: module.id,
selector: 'md-slider',
host: {
'tabindex': '0',
'(click)': 'onClick($event)',
'(drag)': 'onDrag($event)',
'(dragstart)': 'onDragStart($event)',
'(dragend)': 'onDragEnd()',
'(window:resize)': 'onResize()',
'(blur)': 'onBlur()',
},
templateUrl: 'slider.html',
styleUrls: ['slider.css'],
encapsulation: ViewEncapsulation.None,
})
export class MdSlider implements AfterContentInit {
/** A renderer to handle updating the slider's thumb and fill track. */
private _renderer: SliderRenderer = null;
/** The dimensions of the slider. */
private _sliderDimensions: ClientRect = null;
@Input()
@BooleanFieldValue()
@HostBinding('class.md-slider-disabled')
@HostBinding('attr.aria-disabled')
disabled: boolean = false;
/** The miniumum value that the slider can have. */
private _min: number = 0;
/** The maximum value that the slider can have. */
private _max: number = 100;
/** The percentage of the slider that coincides with the value. */
private _percent: number = 0;
/**
* Whether or not the thumb is currently being dragged.
* Used to determine if there should be a transition for the thumb and fill track.
* TODO: internal
*/
isDragging: boolean = false;
/**
* Whether or not the slider is active (clicked or is being dragged).
* Used to shrink and grow the thumb as according to the Material Design spec.
* TODO: internal
*/
isActive: boolean = false;
/** Indicator for if the value has been set or not. */
private _isInitialized: boolean = false;
/** Value of the slider. */
private _value: number = 0;
@Input()
@HostBinding('attr.aria-valuemin')
get min() {
return this._min;
}
set min(v: number) {
// This has to be forced as a number to handle the math later.
this._min = Number(v);
// If the value wasn't explicitly set by the user, set it to the min.
if (!this._isInitialized) {
this.value = this._min;
}
}
@Input()
@HostBinding('attr.aria-valuemax')
get max() {
return this._max;
}
set max(v: number) {
this._max = Number(v);
}
@Input()
@HostBinding('attr.aria-valuenow')
get value() {
return this._value;
}
set value(v: number) {
this._value = Number(v);
this._isInitialized = true;
this.updatePercentFromValue();
}
constructor(elementRef: ElementRef) {
this._renderer = new SliderRenderer(elementRef);
}
/**
* Once the slider has rendered, grab the dimensions and update the position of the thumb and
* fill track.
* TODO: internal
*/
ngAfterContentInit() {
this._sliderDimensions = this._renderer.getSliderDimensions();
this._renderer.updateThumbAndFillPosition(this._percent, this._sliderDimensions.width);
}
/** TODO: internal */
onClick(event: MouseEvent) {
if (this.disabled) {
return;
}
this.isActive = true;
this.isDragging = false;
this._renderer.addFocus();
this.updateValueFromPosition(event.clientX);
}
/** TODO: internal */
onDrag(event: HammerInput) {
if (this.disabled) {
return;
}
// Prevent the drag from selecting anything else.
event.preventDefault();
this.updateValueFromPosition(event.center.x);
}
/** TODO: internal */
onDragStart(event: HammerInput) {
if (this.disabled) {
return;
}
event.preventDefault();
this.isDragging = true;
this.isActive = true;
this._renderer.addFocus();
this.updateValueFromPosition(event.center.x);
}
/** TODO: internal */
onDragEnd() {
this.isDragging = false;
}
/** TODO: internal */
onResize() {
this.isDragging = true;
this._sliderDimensions = this._renderer.getSliderDimensions();
// Skip updating the value and position as there is no new placement.
this._renderer.updateThumbAndFillPosition(this._percent, this._sliderDimensions.width);
}
/** TODO: internal */
onBlur() {
this.isActive = false;
}
/**
* When the value changes without a physical position, the percentage needs to be recalculated
* independent of the physical location.
*/
updatePercentFromValue() {
this._percent = (this.value - this.min) / (this.max - this.min);
}
/**
* Calculate the new value from the new physical location.
*/
updateValueFromPosition(pos: number) {
let offset = this._sliderDimensions.left;
let size = this._sliderDimensions.width;
this._percent = this.clamp((pos - offset) / size);
this.value = this.min + (this._percent * (this.max - this.min));
this._renderer.updateThumbAndFillPosition(this._percent, this._sliderDimensions.width);
}
/**
* Return a number between two numbers.
*/
clamp(value: number, min = 0, max = 1) {
return Math.max(min, Math.min(value, max));
}
}
/**
* Renderer class in order to keep all dom manipulation in one place and outside of the main class.
*/
export class SliderRenderer {
private _sliderElement: HTMLElement;
constructor(elementRef: ElementRef) {
this._sliderElement = elementRef.nativeElement;
}
/**
* Get the bounding client rect of the slider track element.
* The track is used rather than the native element to ignore the extra space that the thumb can
* take up.
*/
getSliderDimensions() {
let trackElement = this._sliderElement.querySelector('.md-slider-track');
return trackElement.getBoundingClientRect();
}
/**
* Update the physical position of the thumb and fill track on the slider.
*/
updateThumbAndFillPosition(percent: number, width: number) {
// The actual thumb element. Needed to get the exact width of the thumb for calculations.
let thumbElement = this._sliderElement.querySelector('.md-slider-thumb');
// A container element that is used to avoid overwriting the transform on the thumb itself.
let thumbPositionElement =
<HTMLElement>this._sliderElement.querySelector('.md-slider-thumb-position');
let fillTrackElement = <HTMLElement>this._sliderElement.querySelector('.md-slider-track-fill');
let thumbWidth = thumbElement.getBoundingClientRect().width;
let position = percent * width;
// The thumb needs to be shifted to the left by half of the width of itself so that it centers
// on the value.
let thumbPosition = position - (thumbWidth / 2);
fillTrackElement.style.width = `${position}px`;
applyCssTransform(thumbPositionElement, `translateX(${thumbPosition}px)`);
}
/**
* Focuses the native element.
* Currently only used to allow a blur event to fire but will be used with keyboard input later.
*/
addFocus() {
this._sliderElement.focus();
}
}
export const MD_SLIDER_DIRECTIVES = [MdSlider];
@@ -0,0 +1,39 @@
import {Injectable} from '@angular/core';
import {MdGestureConfig} from '@angular2-material/core/gestures/MdGestureConfig';
/**
* An extension of MdGestureConfig that exposes the underlying HammerManager instances.
* Tests can use these instances to emit fake gesture events.
*/
@Injectable()
export class TestGestureConfig extends MdGestureConfig {
/**
* A map of Hammer instances to element.
* Used to emit events over instances for an element.
*/
hammerInstances: Map<HTMLElement, HammerManager[]> = new Map<HTMLElement, HammerManager[]>();
/**
* Create a mapping of Hammer instances to element so that events can be emitted during testing.
*/
buildHammer(element: HTMLElement) {
let mc = super.buildHammer(element);
if (this.hammerInstances.get(element)) {
this.hammerInstances.get(element).push(mc);
} else {
this.hammerInstances.set(element, [mc]);
}
return mc;
}
/**
* The Angular event plugin for Hammer creates a new HammerManager instance for each listener,
* so we need to apply our event on all instances to hit the correct listener.
*/
emitEventForElement(eventType: string, element: HTMLElement, eventData: Object) {
let instances = this.hammerInstances.get(element);
instances.forEach(instance => instance.emit(eventType, eventData));
}
}
+1
View File
@@ -18,6 +18,7 @@
<a md-list-item [routerLink]="['progress-bar']">Progress Bar</a>
<a md-list-item [routerLink]="['radio']">Radio</a>
<a md-list-item [routerLink]="['sidenav']">Sidenav</a>
<a md-list-item [routerLink]="['slider']">Slider</a>
<a md-list-item [routerLink]="['slide-toggle']">Slide Toggle</a>
<a md-list-item [routerLink]="['toolbar']">Toolbar</a>
<a md-list-item [routerLink]="['tabs']">Tabs</a>
+2
View File
@@ -17,6 +17,7 @@ import {PortalDemo} from '../portal/portal-demo';
import {ProgressBarDemo} from '../progress-bar/progress-bar-demo';
import {ProgressCircleDemo} from '../progress-circle/progress-circle-demo';
import {SlideToggleDemo} from '../slide-toggle/slide-toggle-demo';
import {SliderDemo} from '../slider/slider-demo';
import {SidenavDemo} from '../sidenav/sidenav-demo';
import {RadioDemo} from '../radio/radio-demo';
import {CardDemo} from '../card/card-demo';
@@ -31,6 +32,7 @@ export const routes: RouterConfig = [
{path: 'radio', component: RadioDemo},
{path: 'sidenav', component: SidenavDemo},
{path: 'slide-toggle', component: SlideToggleDemo},
{path: 'slider', component: SliderDemo},
{path: 'progress-circle', component: ProgressCircleDemo},
{path: 'progress-bar', component: ProgressBarDemo},
{path: 'portal', component: PortalDemo},
+22
View File
@@ -0,0 +1,22 @@
<h1>Default Slider</h1>
<section class="demo-section">
Label <md-slider #slidey></md-slider>
{{slidey.value}}
</section>
<h1>Slider with Min and Max</h1>
<section class="demo-section">
<md-slider min="10" max="50" #slider2></md-slider>
{{slider2.value}}
</section>
<h1>Disabled Slider</h1>
<section class="demo-section">
<md-slider disabled #slider3></md-slider>
{{slider3.value}}
</section>
<h1>Slider with set value</h1>
<section class="demo-section">
<md-slider value="43" #slider4></md-slider>
</section>
+10
View File
@@ -0,0 +1,10 @@
import {Component} from '@angular/core';
import {MD_SLIDER_DIRECTIVES} from '@angular2-material/slider/slider';
@Component({
moduleId: module.id,
selector: 'slider-demo',
templateUrl: 'slider-demo.html',
directives: [MD_SLIDER_DIRECTIVES],
})
export class SliderDemo { }
+1
View File
@@ -15,6 +15,7 @@ const components = [
'progress-circle',
'radio',
'sidenav',
'slider',
'slide-toggle',
'button-toggle',
'tabs',
+1
View File
@@ -25,6 +25,7 @@ export function config(config) {
{pattern: 'dist/vendor/zone.js/dist/zone.js', included: true, watched: false},
{pattern: 'dist/vendor/zone.js/dist/async-test.js', included: true, watched: false},
{pattern: 'dist/vendor/zone.js/dist/fake-async-test.js', included: true, watched: false},
{pattern: 'dist/vendor/hammerjs/hammer.min.js', included: true, watched: false},
{pattern: 'test/karma-test-shim.js', included: true, watched: false},