Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(slider): support for RTL and invert #1794

Merged
merged 16 commits into from
Nov 30, 2016
Merged
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a link to the Chrome bug?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't, I would file one except its really elusive. If a try replicating this HTML/CSS in a jsfiddle it doesn't happen.

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