From 773c95953de4ff4bde0355917b62d91ad0efd3da Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 7 Mar 2017 13:58:31 -0800 Subject: [PATCH] Differentiate sliders focused via keyboard vs other means --- src/lib/slider/_slider-theme.scss | 16 ++++++-- src/lib/slider/index.ts | 7 ++-- src/lib/slider/slider.html | 1 + src/lib/slider/slider.scss | 68 ++++++++++++++++++++++++------- src/lib/slider/slider.spec.ts | 64 ++++------------------------- src/lib/slider/slider.ts | 50 +++++++++++++++-------- 6 files changed, 110 insertions(+), 96 deletions(-) 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(); }