Skip to content

Commit 5ac29dd

Browse files
mmalerbatinayuangao
authored andcommitted
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
1 parent cde9ab3 commit 5ac29dd

File tree

6 files changed

+204
-26
lines changed

6 files changed

+204
-26
lines changed

src/demo-app/slider/slider-demo.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ <h1>Slider with two-way binding</h1>
3434
<md-slider [(ngModel)]="demo" step="40"></md-slider>
3535
<input [(ngModel)]="demo">
3636

37+
<h1>Inverted slider</h1>
38+
<md-slider invert value="50"></md-slider>
39+
3740
<md-tab-group>
3841
<md-tab label="One">
3942
<md-slider min="1" max="5" value="3"></md-slider>

src/lib/core/rtl/dir.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {
2-
NgModule,
3-
ModuleWithProviders,
4-
Directive,
5-
HostBinding,
6-
Output,
7-
Input,
8-
EventEmitter
2+
NgModule,
3+
ModuleWithProviders,
4+
Directive,
5+
HostBinding,
6+
Output,
7+
Input,
8+
EventEmitter
99
} from '@angular/core';
1010

1111
export type LayoutDirection = 'ltr' | 'rtl';

src/lib/slider/slider.html

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
<div class="md-slider-track">
2-
<div class="md-slider-track-fill" [style.flexBasis]="trackFillFlexBasis"></div>
3-
<div class="md-slider-ticks-container" [style.marginLeft]="ticksContainerMarginLeft">
4-
<div class="md-slider-ticks" [style.marginLeft]="ticksMarginLeft"
5-
[style.backgroundSize]="ticksBackgroundSize"></div>
2+
<div class="md-slider-track-fill" [ngStyle]="trackFillStyles"></div>
3+
<div class="md-slider-ticks-container" [ngStyle]="ticksContainerStyles">
4+
<div class="md-slider-ticks" [ngStyle]="ticksStyles"></div>
65
</div>
76
<div class="md-slider-thumb-container">
87
<div class="md-slider-thumb"></div>

src/lib/slider/slider.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ md-slider {
4747
box-shadow: inset (-2 * $md-slider-tick-size) 0 0 (-$md-slider-tick-size) $md-slider-tick-color;
4848
}
4949

50+
[dir='rtl'] .md-slider-has-ticks.md-slider-active .md-slider-track,
51+
[dir='rtl'] .md-slider-has-ticks:hover .md-slider-track {
52+
box-shadow: inset (2 * $md-slider-tick-size) 0 0 (-$md-slider-tick-size) $md-slider-tick-color;
53+
}
54+
55+
.md-slider-inverted .md-slider-track {
56+
flex-direction: row-reverse;
57+
}
58+
5059
.md-slider-track-fill {
5160
flex: 0 0 50%;
5261
height: $md-slider-track-thickness;
@@ -66,6 +75,11 @@ md-slider {
6675
overflow: hidden;
6776
}
6877

78+
[dir='rtl'] .md-slider-ticks-container {
79+
// translateZ(0) prevents chrome bug where overflow: hidden; doesn't work.
80+
transform: translateZ(0) rotate(180deg);
81+
}
82+
6983
.md-slider-ticks {
7084
background: repeating-linear-gradient(to right, $md-slider-tick-color,
7185
$md-slider-tick-color $md-slider-tick-size, transparent 0, transparent) repeat;

src/lib/slider/slider.spec.ts

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import {Component, DebugElement} from '@angular/core';
44
import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
55
import {MdSlider, MdSliderModule} from './slider';
66
import {TestGestureConfig} from './test-gesture-config';
7+
import {RtlModule} from '../core/rtl/dir';
78
import {
89
UP_ARROW,
910
RIGHT_ARROW,
1011
DOWN_ARROW,
1112
PAGE_DOWN,
1213
PAGE_UP,
1314
END,
14-
HOME, LEFT_ARROW
15+
HOME,
16+
LEFT_ARROW
1517
} from '../core/keyboard/keycodes';
1618

1719

@@ -20,7 +22,7 @@ describe('MdSlider', () => {
2022

2123
beforeEach(async(() => {
2224
TestBed.configureTestingModule({
23-
imports: [MdSliderModule.forRoot(), ReactiveFormsModule],
25+
imports: [MdSliderModule.forRoot(), RtlModule.forRoot(), ReactiveFormsModule],
2426
declarations: [
2527
StandardSlider,
2628
DisabledSlider,
@@ -35,6 +37,7 @@ describe('MdSlider', () => {
3537
SliderWithValueSmallerThanMin,
3638
SliderWithValueGreaterThanMax,
3739
SliderWithChangeHandler,
40+
SliderWithDirAndInvert,
3841
],
3942
providers: [
4043
{provide: HAMMER_GESTURE_CONFIG, useFactory: () => {
@@ -838,6 +841,122 @@ describe('MdSlider', () => {
838841
expect(sliderInstance.value).toBe(0);
839842
});
840843
});
844+
845+
describe('slider with direction and invert', () => {
846+
let fixture: ComponentFixture<SliderWithDirAndInvert>;
847+
let sliderDebugElement: DebugElement;
848+
let sliderNativeElement: HTMLElement;
849+
let sliderTrackElement: HTMLElement;
850+
let sliderInstance: MdSlider;
851+
let testComponent: SliderWithDirAndInvert;
852+
853+
beforeEach(() => {
854+
fixture = TestBed.createComponent(SliderWithDirAndInvert);
855+
fixture.detectChanges();
856+
857+
testComponent = fixture.debugElement.componentInstance;
858+
sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
859+
sliderInstance = sliderDebugElement.injector.get(MdSlider);
860+
sliderNativeElement = sliderDebugElement.nativeElement;
861+
sliderTrackElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track');
862+
});
863+
864+
it('works in inverted mode', () => {
865+
testComponent.invert = true;
866+
fixture.detectChanges();
867+
868+
dispatchClickEventSequence(sliderNativeElement, 0.3);
869+
fixture.detectChanges();
870+
871+
expect(sliderInstance.value).toBe(70);
872+
});
873+
874+
it('works in RTL languages', () => {
875+
testComponent.dir = 'rtl';
876+
fixture.detectChanges();
877+
878+
dispatchClickEventSequence(sliderNativeElement, 0.3);
879+
fixture.detectChanges();
880+
881+
expect(sliderInstance.value).toBe(70);
882+
});
883+
884+
it('works in RTL languages in inverted mode', () => {
885+
testComponent.dir = 'rtl';
886+
testComponent.invert = true;
887+
fixture.detectChanges();
888+
889+
dispatchClickEventSequence(sliderNativeElement, 0.3);
890+
fixture.detectChanges();
891+
892+
expect(sliderInstance.value).toBe(30);
893+
});
894+
895+
it('should decrement inverted slider by 1 on right arrow pressed', () => {
896+
testComponent.invert = true;
897+
sliderInstance.value = 100;
898+
fixture.detectChanges();
899+
900+
dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW);
901+
fixture.detectChanges();
902+
903+
expect(sliderInstance.value).toBe(99);
904+
});
905+
906+
it('should increment inverted slider by 1 on left arrow pressed', () => {
907+
testComponent.invert = true;
908+
fixture.detectChanges();
909+
910+
dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW);
911+
fixture.detectChanges();
912+
913+
expect(sliderInstance.value).toBe(1);
914+
});
915+
916+
it('should decrement RTL slider by 1 on right arrow pressed', () => {
917+
testComponent.dir = 'rtl';
918+
sliderInstance.value = 100;
919+
fixture.detectChanges();
920+
921+
dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW);
922+
fixture.detectChanges();
923+
924+
expect(sliderInstance.value).toBe(99);
925+
});
926+
927+
it('should increment RTL slider by 1 on left arrow pressed', () => {
928+
testComponent.dir = 'rtl';
929+
fixture.detectChanges();
930+
931+
dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW);
932+
fixture.detectChanges();
933+
934+
expect(sliderInstance.value).toBe(1);
935+
});
936+
937+
it('should increment inverted RTL slider by 1 on right arrow pressed', () => {
938+
testComponent.dir = 'rtl';
939+
testComponent.invert = true;
940+
fixture.detectChanges();
941+
942+
dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW);
943+
fixture.detectChanges();
944+
945+
expect(sliderInstance.value).toBe(1);
946+
});
947+
948+
it('should decrement inverted RTL slider by 1 on left arrow pressed', () => {
949+
testComponent.dir = 'rtl';
950+
testComponent.invert = true;
951+
sliderInstance.value = 100;
952+
fixture.detectChanges();
953+
954+
dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW);
955+
fixture.detectChanges();
956+
957+
expect(sliderInstance.value).toBe(99);
958+
});
959+
});
841960
});
842961

843962
// Disable animations and make the slider an even 100px (+ 8px padding on either side)
@@ -934,6 +1053,15 @@ class SliderWithChangeHandler {
9341053
onChange() { }
9351054
}
9361055

1056+
@Component({
1057+
template: `<div [dir]="dir"><md-slider [invert]="invert"></md-slider></div>`,
1058+
styles: [styles],
1059+
})
1060+
class SliderWithDirAndInvert {
1061+
dir = 'ltr';
1062+
invert = false;
1063+
}
1064+
9371065
/**
9381066
* Dispatches a click event sequence (consisting of moueseenter, click) from an element.
9391067
* Note: The mouse event truncates the position for the click.

src/lib/slider/slider.ts

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import {
77
Output,
88
ViewEncapsulation,
99
forwardRef,
10-
EventEmitter
10+
EventEmitter,
11+
Optional
1112
} from '@angular/core';
1213
import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms';
1314
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
1415
import {MdGestureConfig, coerceBooleanProperty, coerceNumberProperty} from '../core';
1516
import {Input as HammerInput} from 'hammerjs';
17+
import {Dir} from '../core/rtl/dir';
18+
import {CommonModule} from '@angular/common';
1619
import {
1720
PAGE_UP,
1821
PAGE_DOWN,
@@ -67,6 +70,7 @@ export class MdSliderChange {
6770
'[class.md-slider-active]': '_isActive',
6871
'[class.md-slider-disabled]': 'disabled',
6972
'[class.md-slider-has-ticks]': 'tickInterval',
73+
'[class.md-slider-inverted]': 'invert',
7074
'[class.md-slider-sliding]': '_isSliding',
7175
'[class.md-slider-thumb-label-showing]': 'thumbLabel',
7276
},
@@ -189,25 +193,47 @@ export class MdSlider implements ControlValueAccessor {
189193
this._percent = this._calculatePercentage(this.value);
190194
}
191195

192-
get trackFillFlexBasis() {
193-
return this.percent * 100 + '%';
196+
/** Whether the slider is inverted. */
197+
@Input()
198+
get invert() { return this._invert; }
199+
set invert(value: boolean) { this._invert = coerceBooleanProperty(value); }
200+
private _invert = false;
201+
202+
/** CSS styles for the track fill element. */
203+
get trackFillStyles(): { [key: string]: string } {
204+
return {
205+
'flexBasis': `${this.percent * 100}%`
206+
};
194207
}
195208

196-
get ticksMarginLeft() {
197-
return this.tickIntervalPercent / 2 * 100 + '%';
209+
/** CSS styles for the ticks container element. */
210+
get ticksContainerStyles(): { [key: string]: string } {
211+
return {
212+
'marginLeft': `${this.direction == 'rtl' ? '' : '-'}${this.tickIntervalPercent / 2 * 100}%`
213+
};
198214
}
199215

200-
get ticksContainerMarginLeft() {
201-
return '-' + this.ticksMarginLeft;
216+
/** CSS styles for the ticks element. */
217+
get ticksStyles() {
218+
let styles: { [key: string]: string } = {
219+
'backgroundSize': `${this.tickIntervalPercent * 100}% 2px`
220+
};
221+
if (this.direction == 'rtl') {
222+
styles['marginRight'] = `-${this.tickIntervalPercent / 2 * 100}%`;
223+
} else {
224+
styles['marginLeft'] = `${this.tickIntervalPercent / 2 * 100}%`;
225+
}
226+
return styles;
202227
}
203228

204-
get ticksBackgroundSize() {
205-
return this.tickIntervalPercent * 100 + '% 2px';
229+
/** The language direction for this slider element. */
230+
get direction() {
231+
return (this._dir && this._dir.value == 'rtl') ? 'rtl' : 'ltr';
206232
}
207233

208234
@Output() change = new EventEmitter<MdSliderChange>();
209235

210-
constructor(elementRef: ElementRef) {
236+
constructor(@Optional() private _dir: Dir, elementRef: ElementRef) {
211237
this._renderer = new SliderRenderer(elementRef);
212238
}
213239

@@ -283,13 +309,13 @@ export class MdSlider implements ControlValueAccessor {
283309
this.value = this.min;
284310
break;
285311
case LEFT_ARROW:
286-
this._increment(-1);
312+
this._increment(this._isLeftMin() ? -1 : 1);
287313
break;
288314
case UP_ARROW:
289315
this._increment(1);
290316
break;
291317
case RIGHT_ARROW:
292-
this._increment(1);
318+
this._increment(this._isLeftMin() ? 1 : -1);
293319
break;
294320
case DOWN_ARROW:
295321
this._increment(-1);
@@ -303,6 +329,11 @@ export class MdSlider implements ControlValueAccessor {
303329
event.preventDefault();
304330
}
305331

332+
/** Whether the left side of the slider is the minimum value. */
333+
private _isLeftMin() {
334+
return (this.direction == 'rtl') == this.invert;
335+
}
336+
306337
/** Increments the slider by the given number of steps (negative number decrements). */
307338
private _increment(numSteps: number) {
308339
this.value = this._clamp(this.value + this.step * numSteps, this.min, this.max);
@@ -321,6 +352,9 @@ export class MdSlider implements ControlValueAccessor {
321352

322353
// The exact value is calculated from the event and used to find the closest snap value.
323354
let percent = this._clamp((pos - offset) / size);
355+
if (!this._isLeftMin()) {
356+
percent = 1 - percent;
357+
}
324358
let exactValue = this._calculateValue(percent);
325359

326360
// This calculation finds the closest step by finding the closest whole number divisible by the
@@ -441,7 +475,7 @@ export class SliderRenderer {
441475

442476

443477
@NgModule({
444-
imports: [FormsModule],
478+
imports: [CommonModule, FormsModule],
445479
exports: [MdSlider],
446480
declarations: [MdSlider],
447481
providers: [

0 commit comments

Comments
 (0)