From 5ac29dd97a31a25d196629a5770cb2d712cb51f3 Mon Sep 17 00:00:00 2001 From: mmalerba Date: Wed, 30 Nov 2016 11:46:14 -0800 Subject: [PATCH] fix(slider): support for RTL and invert (#1794) * Addressed comments. * PercentPipe was adding extra space before '%', so replaced it. * remove CommonModule from imports. * fix(slider): keyboard support. * prevent keyboard interaction with disabled slider. * fix(slider): support for rtl and inverted sliders. * clean up demo html file * fixed tests and lint issues * added tests * fix comment * switch to event.keyCode * added tests * x-browserify keydown event dispatch * swap left/right arrow behavior in rtl * comment why default: return; * fixed lint issues --- src/demo-app/slider/slider-demo.html | 3 + src/lib/core/rtl/dir.ts | 14 +-- src/lib/slider/slider.html | 7 +- src/lib/slider/slider.scss | 14 +++ src/lib/slider/slider.spec.ts | 132 ++++++++++++++++++++++++++- src/lib/slider/slider.ts | 60 +++++++++--- 6 files changed, 204 insertions(+), 26 deletions(-) diff --git a/src/demo-app/slider/slider-demo.html b/src/demo-app/slider/slider-demo.html index fc848033cec9..4b6c65478563 100644 --- a/src/demo-app/slider/slider-demo.html +++ b/src/demo-app/slider/slider-demo.html @@ -34,6 +34,9 @@

Slider with two-way binding

+

Inverted slider

+ + diff --git a/src/lib/core/rtl/dir.ts b/src/lib/core/rtl/dir.ts index bd026fd35e0a..16923164b542 100644 --- a/src/lib/core/rtl/dir.ts +++ b/src/lib/core/rtl/dir.ts @@ -1,11 +1,11 @@ import { - NgModule, - ModuleWithProviders, - Directive, - HostBinding, - Output, - Input, - EventEmitter + NgModule, + ModuleWithProviders, + Directive, + HostBinding, + Output, + Input, + EventEmitter } from '@angular/core'; export type LayoutDirection = 'ltr' | 'rtl'; diff --git a/src/lib/slider/slider.html b/src/lib/slider/slider.html index 688ea4bb348a..e39f29ed400a 100644 --- a/src/lib/slider/slider.html +++ b/src/lib/slider/slider.html @@ -1,8 +1,7 @@
-
-
-
+
+
+
diff --git a/src/lib/slider/slider.scss b/src/lib/slider/slider.scss index a8ae53908b05..2b2c3f36f860 100644 --- a/src/lib/slider/slider.scss +++ b/src/lib/slider/slider.scss @@ -47,6 +47,15 @@ md-slider { box-shadow: inset (-2 * $md-slider-tick-size) 0 0 (-$md-slider-tick-size) $md-slider-tick-color; } +[dir='rtl'] .md-slider-has-ticks.md-slider-active .md-slider-track, +[dir='rtl'] .md-slider-has-ticks:hover .md-slider-track { + box-shadow: inset (2 * $md-slider-tick-size) 0 0 (-$md-slider-tick-size) $md-slider-tick-color; +} + +.md-slider-inverted .md-slider-track { + flex-direction: row-reverse; +} + .md-slider-track-fill { flex: 0 0 50%; height: $md-slider-track-thickness; @@ -66,6 +75,11 @@ md-slider { overflow: hidden; } +[dir='rtl'] .md-slider-ticks-container { + // translateZ(0) prevents chrome bug where overflow: hidden; doesn't work. + transform: translateZ(0) rotate(180deg); +} + .md-slider-ticks { background: repeating-linear-gradient(to right, $md-slider-tick-color, $md-slider-tick-color $md-slider-tick-size, transparent 0, transparent) repeat; diff --git a/src/lib/slider/slider.spec.ts b/src/lib/slider/slider.spec.ts index ef635cd58e56..b22ebc66c76d 100644 --- a/src/lib/slider/slider.spec.ts +++ b/src/lib/slider/slider.spec.ts @@ -4,6 +4,7 @@ import {Component, DebugElement} from '@angular/core'; import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; import {MdSlider, MdSliderModule} from './slider'; import {TestGestureConfig} from './test-gesture-config'; +import {RtlModule} from '../core/rtl/dir'; import { UP_ARROW, RIGHT_ARROW, @@ -11,7 +12,8 @@ import { PAGE_DOWN, PAGE_UP, END, - HOME, LEFT_ARROW + HOME, + LEFT_ARROW } from '../core/keyboard/keycodes'; @@ -20,7 +22,7 @@ describe('MdSlider', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [MdSliderModule.forRoot(), ReactiveFormsModule], + imports: [MdSliderModule.forRoot(), RtlModule.forRoot(), ReactiveFormsModule], declarations: [ StandardSlider, DisabledSlider, @@ -35,6 +37,7 @@ describe('MdSlider', () => { SliderWithValueSmallerThanMin, SliderWithValueGreaterThanMax, SliderWithChangeHandler, + SliderWithDirAndInvert, ], providers: [ {provide: HAMMER_GESTURE_CONFIG, useFactory: () => { @@ -838,6 +841,122 @@ describe('MdSlider', () => { expect(sliderInstance.value).toBe(0); }); }); + + describe('slider with direction and invert', () => { + let fixture: ComponentFixture; + let sliderDebugElement: DebugElement; + let sliderNativeElement: HTMLElement; + let sliderTrackElement: HTMLElement; + let sliderInstance: MdSlider; + let testComponent: SliderWithDirAndInvert; + + beforeEach(() => { + fixture = TestBed.createComponent(SliderWithDirAndInvert); + fixture.detectChanges(); + + testComponent = fixture.debugElement.componentInstance; + sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider)); + sliderInstance = sliderDebugElement.injector.get(MdSlider); + sliderNativeElement = sliderDebugElement.nativeElement; + sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track'); + }); + + it('works in inverted mode', () => { + testComponent.invert = true; + fixture.detectChanges(); + + dispatchClickEventSequence(sliderNativeElement, 0.3); + fixture.detectChanges(); + + expect(sliderInstance.value).toBe(70); + }); + + it('works in RTL languages', () => { + testComponent.dir = 'rtl'; + fixture.detectChanges(); + + dispatchClickEventSequence(sliderNativeElement, 0.3); + fixture.detectChanges(); + + expect(sliderInstance.value).toBe(70); + }); + + it('works in RTL languages in inverted mode', () => { + testComponent.dir = 'rtl'; + testComponent.invert = true; + fixture.detectChanges(); + + dispatchClickEventSequence(sliderNativeElement, 0.3); + fixture.detectChanges(); + + expect(sliderInstance.value).toBe(30); + }); + + it('should decrement inverted slider by 1 on right arrow pressed', () => { + testComponent.invert = true; + sliderInstance.value = 100; + fixture.detectChanges(); + + dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW); + fixture.detectChanges(); + + expect(sliderInstance.value).toBe(99); + }); + + it('should increment inverted slider by 1 on left arrow pressed', () => { + testComponent.invert = true; + fixture.detectChanges(); + + dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW); + fixture.detectChanges(); + + expect(sliderInstance.value).toBe(1); + }); + + it('should decrement RTL slider by 1 on right arrow pressed', () => { + testComponent.dir = 'rtl'; + sliderInstance.value = 100; + fixture.detectChanges(); + + dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW); + fixture.detectChanges(); + + expect(sliderInstance.value).toBe(99); + }); + + it('should increment RTL slider by 1 on left arrow pressed', () => { + testComponent.dir = 'rtl'; + fixture.detectChanges(); + + dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW); + fixture.detectChanges(); + + expect(sliderInstance.value).toBe(1); + }); + + it('should increment inverted RTL slider by 1 on right arrow pressed', () => { + testComponent.dir = 'rtl'; + testComponent.invert = true; + fixture.detectChanges(); + + dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW); + fixture.detectChanges(); + + expect(sliderInstance.value).toBe(1); + }); + + it('should decrement inverted RTL slider by 1 on left arrow pressed', () => { + testComponent.dir = 'rtl'; + testComponent.invert = true; + sliderInstance.value = 100; + fixture.detectChanges(); + + dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW); + fixture.detectChanges(); + + expect(sliderInstance.value).toBe(99); + }); + }); }); // Disable animations and make the slider an even 100px (+ 8px padding on either side) @@ -934,6 +1053,15 @@ class SliderWithChangeHandler { onChange() { } } +@Component({ + template: `
`, + styles: [styles], +}) +class SliderWithDirAndInvert { + dir = 'ltr'; + invert = false; +} + /** * Dispatches a click event sequence (consisting of moueseenter, click) from an element. * Note: The mouse event truncates the position for the click. diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index 21394dd3b481..e44638151165 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -7,12 +7,15 @@ import { Output, ViewEncapsulation, forwardRef, - EventEmitter + EventEmitter, + Optional } from '@angular/core'; import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms'; import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; import {MdGestureConfig, coerceBooleanProperty, coerceNumberProperty} from '../core'; import {Input as HammerInput} from 'hammerjs'; +import {Dir} from '../core/rtl/dir'; +import {CommonModule} from '@angular/common'; import { PAGE_UP, PAGE_DOWN, @@ -67,6 +70,7 @@ export class MdSliderChange { '[class.md-slider-active]': '_isActive', '[class.md-slider-disabled]': 'disabled', '[class.md-slider-has-ticks]': 'tickInterval', + '[class.md-slider-inverted]': 'invert', '[class.md-slider-sliding]': '_isSliding', '[class.md-slider-thumb-label-showing]': 'thumbLabel', }, @@ -189,25 +193,47 @@ export class MdSlider implements ControlValueAccessor { this._percent = this._calculatePercentage(this.value); } - get trackFillFlexBasis() { - return this.percent * 100 + '%'; + /** Whether the slider is inverted. */ + @Input() + get invert() { return this._invert; } + set invert(value: boolean) { this._invert = coerceBooleanProperty(value); } + private _invert = false; + + /** CSS styles for the track fill element. */ + get trackFillStyles(): { [key: string]: string } { + return { + 'flexBasis': `${this.percent * 100}%` + }; } - get ticksMarginLeft() { - return this.tickIntervalPercent / 2 * 100 + '%'; + /** CSS styles for the ticks container element. */ + get ticksContainerStyles(): { [key: string]: string } { + return { + 'marginLeft': `${this.direction == 'rtl' ? '' : '-'}${this.tickIntervalPercent / 2 * 100}%` + }; } - get ticksContainerMarginLeft() { - return '-' + this.ticksMarginLeft; + /** CSS styles for the ticks element. */ + get ticksStyles() { + let styles: { [key: string]: string } = { + 'backgroundSize': `${this.tickIntervalPercent * 100}% 2px` + }; + if (this.direction == 'rtl') { + styles['marginRight'] = `-${this.tickIntervalPercent / 2 * 100}%`; + } else { + styles['marginLeft'] = `${this.tickIntervalPercent / 2 * 100}%`; + } + return styles; } - get ticksBackgroundSize() { - return this.tickIntervalPercent * 100 + '% 2px'; + /** The language direction for this slider element. */ + get direction() { + return (this._dir && this._dir.value == 'rtl') ? 'rtl' : 'ltr'; } @Output() change = new EventEmitter(); - constructor(elementRef: ElementRef) { + constructor(@Optional() private _dir: Dir, elementRef: ElementRef) { this._renderer = new SliderRenderer(elementRef); } @@ -283,13 +309,13 @@ export class MdSlider implements ControlValueAccessor { this.value = this.min; break; case LEFT_ARROW: - this._increment(-1); + this._increment(this._isLeftMin() ? -1 : 1); break; case UP_ARROW: this._increment(1); break; case RIGHT_ARROW: - this._increment(1); + this._increment(this._isLeftMin() ? 1 : -1); break; case DOWN_ARROW: this._increment(-1); @@ -303,6 +329,11 @@ export class MdSlider implements ControlValueAccessor { event.preventDefault(); } + /** Whether the left side of the slider is the minimum value. */ + private _isLeftMin() { + return (this.direction == 'rtl') == this.invert; + } + /** Increments the slider by the given number of steps (negative number decrements). */ private _increment(numSteps: number) { this.value = this._clamp(this.value + this.step * numSteps, this.min, this.max); @@ -321,6 +352,9 @@ export class MdSlider implements ControlValueAccessor { // The exact value is calculated from the event and used to find the closest snap value. let percent = this._clamp((pos - offset) / size); + if (!this._isLeftMin()) { + percent = 1 - percent; + } let exactValue = this._calculateValue(percent); // This calculation finds the closest step by finding the closest whole number divisible by the @@ -441,7 +475,7 @@ export class SliderRenderer { @NgModule({ - imports: [FormsModule], + imports: [CommonModule, FormsModule], exports: [MdSlider], declarations: [MdSlider], providers: [