diff --git a/src/lib/slider/_slider-theme.scss b/src/lib/slider/_slider-theme.scss
index 3bafc2b4e2cd..e7a84d4b05b3 100644
--- a/src/lib/slider/_slider-theme.scss
+++ b/src/lib/slider/_slider-theme.scss
@@ -10,6 +10,8 @@
$mat-slider-disabled-color: rgba(black, 0.26);
$mat-slider-labeled-min-value-thumb-color: black;
$mat-slider-labeled-min-value-thumb-label-color: rgba(black, 0.26);
+ $mat-slider-focus-ring-color: rgba(mat-color($accent), 0.2);
+ $mat-slider-focus-ring-min-value-color: rgba(black, 0.12);
.mat-slider-track-background {
background-color: $mat-slider-off-color;
@@ -19,6 +21,10 @@
background-color: mat-color($accent);
}
+ .mat-slider-focus-ring {
+ background-color: $mat-slider-focus-ring-color;
+ }
+
.mat-slider-thumb {
background-color: mat-color($accent);
}
@@ -32,7 +38,7 @@
}
.mat-slider:hover,
- .mat-slider-active {
+ .cdk-focused {
.mat-slider-track-background {
background-color: $mat-slider-off-focused-color;
}
@@ -53,13 +59,17 @@
}
.mat-slider-min-value {
+ .mat-slider-focus-ring {
+ background-color: $mat-slider-focus-ring-min-value-color;
+ }
+
&.mat-slider-thumb-label-showing {
.mat-slider-thumb,
.mat-slider-thumb-label {
background-color: $mat-slider-labeled-min-value-thumb-color;
}
- &.mat-slider-active {
+ &.cdk-focused {
.mat-slider-thumb,
.mat-slider-thumb-label {
background-color: $mat-slider-labeled-min-value-thumb-label-color;
@@ -74,7 +84,7 @@
}
&:hover,
- &.mat-slider-active {
+ &.cdk-focused {
.mat-slider-thumb {
border-color: $mat-slider-off-focused-color;
}
diff --git a/src/lib/slider/index.ts b/src/lib/slider/index.ts
index a37d19c4c8cf..7fccc53ea96e 100644
--- a/src/lib/slider/index.ts
+++ b/src/lib/slider/index.ts
@@ -1,13 +1,14 @@
-import {NgModule, ModuleWithProviders} from '@angular/core';
+import {ModuleWithProviders, NgModule} from '@angular/core';
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
-import {GestureConfig, CompatibilityModule} from '../core';
+import {CompatibilityModule, GestureConfig, StyleModule} from '../core';
import {MdSlider} from './slider';
+import {RtlModule} from '../core/rtl/dir';
@NgModule({
- imports: [CommonModule, FormsModule, CompatibilityModule],
+ imports: [CommonModule, FormsModule, CompatibilityModule, StyleModule, RtlModule],
exports: [MdSlider, CompatibilityModule],
declarations: [MdSlider],
providers: [{provide: HAMMER_GESTURE_CONFIG, useClass: GestureConfig}]
diff --git a/src/lib/slider/slider.html b/src/lib/slider/slider.html
index 58e85086d0e3..6b1b7be1fc11 100644
--- a/src/lib/slider/slider.html
+++ b/src/lib/slider/slider.html
@@ -7,6 +7,7 @@
+
{{displayValue}}
diff --git a/src/lib/slider/slider.scss b/src/lib/slider/slider.scss
index 7669e0905609..14af8dee7e42 100644
--- a/src/lib/slider/slider.scss
+++ b/src/lib/slider/slider.scss
@@ -24,6 +24,8 @@ $mat-slider-thumb-label-size: 28px !default;
$mat-slider-tick-color: rgba(0, 0, 0, 0.6) !default;
$mat-slider-tick-size: 2px !default;
+$mat-slider-focus-ring-size: 30px !default;
+
.mat-slider {
display: inline-block;
@@ -72,17 +74,29 @@ $mat-slider-tick-size: 2px !default;
transition: opacity $swift-ease-out-duration $swift-ease-out-timing-function;
}
-// TODO(mmalerba): Simplify css to avoid unnecessary selectors.
-.mat-slider-disabled .mat-slider-ticks {
- opacity: 0;
-}
-
.mat-slider-thumb-container {
position: absolute;
z-index: 1;
transition: transform $swift-ease-out-duration $swift-ease-out-timing-function;
}
+.mat-slider-focus-ring {
+ position: absolute;
+ width: $mat-slider-focus-ring-size;
+ height: $mat-slider-focus-ring-size;
+ border-radius: 50%;
+ transform: scale(0);
+ opacity: 0;
+ transition: transform $swift-ease-out-duration $swift-ease-out-timing-function,
+ background-color $swift-ease-out-duration $swift-ease-out-timing-function,
+ opacity $swift-ease-out-duration $swift-ease-out-timing-function;
+
+ .cdk-keyboard-focused & {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
.mat-slider-thumb {
position: absolute;
right: -$mat-slider-thumb-size / 2;
@@ -143,7 +157,7 @@ $mat-slider-tick-size: 2px !default;
transition: opacity $swift-ease-out-duration $swift-ease-out-timing-function;
}
- &.mat-slider-active,
+ &.cdk-focused,
&:hover {
&:not(.mat-slider-hide-last-tick) {
.mat-slider-wrapper::after {
@@ -151,7 +165,7 @@ $mat-slider-tick-size: 2px !default;
}
}
- .mat-slider-ticks {
+ &:not(.mat-slider-disabled) .mat-slider-ticks {
opacity: 1;
}
}
@@ -160,6 +174,11 @@ $mat-slider-tick-size: 2px !default;
// Slider with thumb label.
.mat-slider-thumb-label-showing {
+ .mat-slider-focus-ring {
+ transform: scale(0);
+ opacity: 0;
+ }
+
.mat-slider-thumb-label {
display: flex;
}
@@ -179,12 +198,7 @@ $mat-slider-tick-size: 2px !default;
// Active slider.
-.mat-slider-active {
- .mat-slider-thumb {
- border-width: $mat-slider-thumb-border-width-active;
- transform: scale($mat-slider-thumb-focus-scale);
- }
-
+.cdk-focused {
&.mat-slider-thumb-label-showing .mat-slider-thumb {
transform: scale(0);
}
@@ -198,9 +212,23 @@ $mat-slider-tick-size: 2px !default;
}
}
+.cdk-mouse-focused,
+.cdk-touch-focused,
+.cdk-program-focused {
+ .mat-slider-thumb {
+ border-width: $mat-slider-thumb-border-width-active;
+ transform: scale($mat-slider-thumb-focus-scale);
+ }
+}
+
// Disabled slider.
.mat-slider-disabled {
+ .mat-slider-focus-ring {
+ transform: scale(0);
+ opacity: 0;
+ }
+
.mat-slider-thumb {
border-width: $mat-slider-thumb-border-width-disabled;
transform: scale($mat-slider-thumb-disabled-scale);
@@ -271,6 +299,11 @@ $mat-slider-tick-size: 2px !default;
top: 50%;
}
+ .mat-slider-focus-ring {
+ top: -$mat-slider-focus-ring-size / 2;
+ right: -$mat-slider-focus-ring-size / 2;
+ }
+
.mat-slider-thumb-label {
right: -$mat-slider-thumb-label-size / 2;
top: -($mat-slider-thumb-label-size + $mat-slider-thumb-arrow-gap);
@@ -282,7 +315,7 @@ $mat-slider-tick-size: 2px !default;
transform: rotate(-45deg);
}
- &.mat-slider-active {
+ &.cdk-focused {
.mat-slider-thumb-label {
transform: rotate(45deg);
}
@@ -331,6 +364,11 @@ $mat-slider-tick-size: 2px !default;
height: 100%;
}
+ .mat-slider-focus-ring {
+ bottom: -$mat-slider-focus-ring-size / 2;
+ left: -$mat-slider-focus-ring-size / 2;
+ }
+
.mat-slider-ticks {
background: repeating-linear-gradient(to bottom, $mat-slider-tick-color,
$mat-slider-tick-color $mat-slider-tick-size, transparent 0, transparent) repeat;
@@ -356,7 +394,7 @@ $mat-slider-tick-size: 2px !default;
transform: rotate(45deg);
}
- &.mat-slider-active {
+ &.cdk-focused {
.mat-slider-thumb-label {
transform: rotate(-45deg);
}
diff --git a/src/lib/slider/slider.spec.ts b/src/lib/slider/slider.spec.ts
index 35ce2babed86..d29307ad289f 100644
--- a/src/lib/slider/slider.spec.ts
+++ b/src/lib/slider/slider.spec.ts
@@ -1,19 +1,19 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
-import {ReactiveFormsModule, FormControl, FormsModule} from '@angular/forms';
+import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {Component, DebugElement} from '@angular/core';
import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {MdSlider, MdSliderModule} from './index';
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
+ LEFT_ARROW,
+ PAGE_DOWN,
+ PAGE_UP,
+ RIGHT_ARROW,
+ UP_ARROW
} from '../core/keyboard/keycodes';
import {dispatchKeyboardEvent, dispatchMouseEvent} from '../core/testing/dispatch-events';
@@ -23,7 +23,7 @@ describe('MdSlider', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [MdSliderModule.forRoot(), RtlModule.forRoot(), ReactiveFormsModule, FormsModule],
+ imports: [MdSliderModule, ReactiveFormsModule, FormsModule, RtlModule],
declarations: [
StandardSlider,
DisabledSlider,
@@ -129,28 +129,6 @@ describe('MdSlider', () => {
expect(trackFillElement.style.transform).toContain('scaleX(0.86)');
});
- it('should add the mat-slider-active class on click', () => {
- expect(sliderNativeElement.classList).not.toContain('mat-slider-active');
-
- dispatchClickEventSequence(sliderNativeElement, 0.23);
- fixture.detectChanges();
-
- expect(sliderNativeElement.classList).toContain('mat-slider-active');
- });
-
- it('should remove the mat-slider-active class on blur', () => {
- dispatchClickEventSequence(sliderNativeElement, 0.95);
- fixture.detectChanges();
-
- expect(sliderNativeElement.classList).toContain('mat-slider-active');
-
- // Call the `onBlur` handler directly because we cannot simulate a focus event in unit tests.
- sliderInstance._onBlur();
- fixture.detectChanges();
-
- expect(sliderNativeElement.classList).not.toContain('mat-slider-active');
- });
-
it('should add and remove the mat-slider-sliding class when sliding', () => {
expect(sliderNativeElement.classList).not.toContain('mat-slider-sliding');
@@ -167,11 +145,6 @@ describe('MdSlider', () => {
it('should have thumb gap when at min value', () => {
expect(trackFillElement.style.transform).toContain('translateX(-7px)');
-
- dispatchClickEventSequence(sliderNativeElement, 0);
- fixture.detectChanges();
-
- expect(trackFillElement.style.transform).toContain('translateX(-10px)');
});
it('should not have thumb gap when not at min value', () => {
@@ -561,29 +534,6 @@ describe('MdSlider', () => {
// The thumb label text is set to the slider's value. These should always be the same.
expect(thumbLabelTextElement.textContent).toBe(`${sliderInstance.value}`);
});
-
- it('should show the thumb label on click', () => {
- expect(sliderNativeElement.classList).not.toContain('mat-slider-active');
- expect(sliderNativeElement.classList).toContain('mat-slider-thumb-label-showing');
-
- dispatchClickEventSequence(sliderNativeElement, 0.49);
- fixture.detectChanges();
-
- // The thumb label appears when the slider is active and the 'mat-slider-thumb-label-showing'
- // class is applied.
- expect(sliderNativeElement.classList).toContain('mat-slider-thumb-label-showing');
- expect(sliderNativeElement.classList).toContain('mat-slider-active');
- });
-
- it('should show the thumb label on slide', () => {
- expect(sliderNativeElement.classList).not.toContain('mat-slider-active');
-
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.91, gestureConfig);
- fixture.detectChanges();
-
- expect(sliderNativeElement.classList).toContain('mat-slider-thumb-label-showing');
- expect(sliderNativeElement.classList).toContain('mat-slider-active');
- });
});
describe('slider as a custom form control', () => {
diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts
index e3171277cb94..c559efc9a069 100644
--- a/src/lib/slider/slider.ts
+++ b/src/lib/slider/slider.ts
@@ -1,26 +1,29 @@
import {
Component,
ElementRef,
+ EventEmitter,
+ forwardRef,
Input,
+ OnDestroy,
+ Optional,
Output,
- ViewEncapsulation,
- forwardRef,
- EventEmitter,
- Optional
+ Renderer,
+ ViewEncapsulation
} from '@angular/core';
-import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
-import {HammerInput, coerceBooleanProperty, coerceNumberProperty} from '../core';
+import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {coerceBooleanProperty, coerceNumberProperty, HammerInput} from '../core';
import {Dir} from '../core/rtl/dir';
import {
- PAGE_UP,
- PAGE_DOWN,
+ DOWN_ARROW,
END,
HOME,
LEFT_ARROW,
- UP_ARROW,
+ PAGE_DOWN,
+ PAGE_UP,
RIGHT_ARROW,
- DOWN_ARROW
+ UP_ARROW
} from '../core/keyboard/keycodes';
+import {FocusOrigin, FocusOriginMonitor} from '../core/style/focus-origin-monitor';
/**
* Visually, a 30px separation between tick marks looks best. This is very subjective but it is
@@ -66,6 +69,7 @@ export class MdSliderChange {
providers: [MD_SLIDER_VALUE_ACCESSOR],
host: {
'[class.mat-slider]': 'true',
+ '(focus)': '_onFocus()',
'(blur)': '_onBlur()',
'(click)': '_onClick($event)',
'(keydown)': '_onKeydown($event)',
@@ -80,7 +84,6 @@ export class MdSliderChange {
'[attr.aria-valuemax]': 'max',
'[attr.aria-valuemin]': 'min',
'[attr.aria-valuenow]': 'value',
- '[class.mat-slider-active]': '_isActive',
'[class.mat-slider-disabled]': 'disabled',
'[class.mat-slider-has-ticks]': 'tickInterval',
'[class.mat-slider-horizontal]': '!vertical',
@@ -89,13 +92,13 @@ export class MdSliderChange {
'[class.mat-slider-thumb-label-showing]': 'thumbLabel',
'[class.mat-slider-vertical]': 'vertical',
'[class.mat-slider-min-value]': '_isMinValue',
- '[class.mat-slider-hide-last-tick]': '_isMinValue && _thumbGap && _invertAxis',
+ '[class.mat-slider-hide-last-tick]': 'disabled || _isMinValue && _thumbGap && _invertAxis',
},
templateUrl: 'slider.html',
styleUrls: ['slider.css'],
encapsulation: ViewEncapsulation.None,
})
-export class MdSlider implements ControlValueAccessor {
+export class MdSlider implements ControlValueAccessor, OnDestroy {
/** Whether or not the slider is disabled. */
@Input()
get disabled(): boolean { return this._disabled; }
@@ -363,8 +366,15 @@ export class MdSlider implements ControlValueAccessor {
return (this._dir && this._dir.value == 'rtl') ? 'rtl' : 'ltr';
}
- constructor(@Optional() private _dir: Dir, elementRef: ElementRef) {
- this._renderer = new SliderRenderer(elementRef);
+ constructor(renderer: Renderer, private _elementRef: ElementRef,
+ private _focusOriginMonitor: FocusOriginMonitor, @Optional() private _dir: Dir) {
+ this._focusOriginMonitor.monitor(this._elementRef.nativeElement, renderer, true)
+ .subscribe((origin: FocusOrigin) => this._isActive = !!origin && origin !== 'keyboard');
+ this._renderer = new SliderRenderer(this._elementRef);
+ }
+
+ ngOnDestroy() {
+ this._focusOriginMonitor.unmonitor(this._elementRef.nativeElement);
}
_onMouseenter() {
@@ -383,7 +393,6 @@ export class MdSlider implements ControlValueAccessor {
return;
}
- this._isActive = true;
this._isSliding = false;
this._renderer.addFocus();
this._updateValueFromPosition({x: event.clientX, y: event.clientY});
@@ -416,7 +425,6 @@ export class MdSlider implements ControlValueAccessor {
event.preventDefault();
this._isSliding = true;
- this._isActive = true;
this._renderer.addFocus();
this._updateValueFromPosition({x: event.center.x, y: event.center.y});
}
@@ -426,8 +434,14 @@ export class MdSlider implements ControlValueAccessor {
this._emitValueIfChanged();
}
+ _onFocus() {
+ // We save the dimensions of the slider here so we can use them to update the spacing of the
+ // ticks and determine where on the slider click and slide events happen.
+ this._sliderDimensions = this._renderer.getSliderDimensions();
+ this._updateTickIntervalPercent();
+ }
+
_onBlur() {
- this._isActive = false;
this.onTouched();
}