Skip to content

Commit

Permalink
fix(slider): support for RTL and invert (#1794)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mmalerba authored and tinayuangao committed Nov 30, 2016
1 parent cde9ab3 commit 5ac29dd
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 26 deletions.
3 changes: 3 additions & 0 deletions src/demo-app/slider/slider-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ <h1>Slider with two-way binding</h1>
<md-slider [(ngModel)]="demo" step="40"></md-slider>
<input [(ngModel)]="demo">

<h1>Inverted slider</h1>
<md-slider invert value="50"></md-slider>

<md-tab-group>
<md-tab label="One">
<md-slider min="1" max="5" value="3"></md-slider>
Expand Down
14 changes: 7 additions & 7 deletions src/lib/core/rtl/dir.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
7 changes: 3 additions & 4 deletions src/lib/slider/slider.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<div class="md-slider-track">
<div class="md-slider-track-fill" [style.flexBasis]="trackFillFlexBasis"></div>
<div class="md-slider-ticks-container" [style.marginLeft]="ticksContainerMarginLeft">
<div class="md-slider-ticks" [style.marginLeft]="ticksMarginLeft"
[style.backgroundSize]="ticksBackgroundSize"></div>
<div class="md-slider-track-fill" [ngStyle]="trackFillStyles"></div>
<div class="md-slider-ticks-container" [ngStyle]="ticksContainerStyles">
<div class="md-slider-ticks" [ngStyle]="ticksStyles"></div>
</div>
<div class="md-slider-thumb-container">
<div class="md-slider-thumb"></div>
Expand Down
14 changes: 14 additions & 0 deletions src/lib/slider/slider.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
132 changes: 130 additions & 2 deletions src/lib/slider/slider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ 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,
DOWN_ARROW,
PAGE_DOWN,
PAGE_UP,
END,
HOME, LEFT_ARROW
HOME,
LEFT_ARROW
} from '../core/keyboard/keycodes';


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

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdSliderModule.forRoot(), ReactiveFormsModule],
imports: [MdSliderModule.forRoot(), RtlModule.forRoot(), ReactiveFormsModule],
declarations: [
StandardSlider,
DisabledSlider,
Expand All @@ -35,6 +37,7 @@ describe('MdSlider', () => {
SliderWithValueSmallerThanMin,
SliderWithValueGreaterThanMax,
SliderWithChangeHandler,
SliderWithDirAndInvert,
],
providers: [
{provide: HAMMER_GESTURE_CONFIG, useFactory: () => {
Expand Down Expand Up @@ -838,6 +841,122 @@ describe('MdSlider', () => {
expect(sliderInstance.value).toBe(0);
});
});

describe('slider with direction and invert', () => {
let fixture: ComponentFixture<SliderWithDirAndInvert>;
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 = <HTMLElement>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)
Expand Down Expand Up @@ -934,6 +1053,15 @@ class SliderWithChangeHandler {
onChange() { }
}

@Component({
template: `<div [dir]="dir"><md-slider [invert]="invert"></md-slider></div>`,
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.
Expand Down
60 changes: 47 additions & 13 deletions src/lib/slider/slider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
},
Expand Down Expand Up @@ -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<MdSliderChange>();

constructor(elementRef: ElementRef) {
constructor(@Optional() private _dir: Dir, elementRef: ElementRef) {
this._renderer = new SliderRenderer(elementRef);
}

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -441,7 +475,7 @@ export class SliderRenderer {


@NgModule({
imports: [FormsModule],
imports: [CommonModule, FormsModule],
exports: [MdSlider],
declarations: [MdSlider],
providers: [
Expand Down

0 comments on commit 5ac29dd

Please sign in to comment.