feat(slider): differentiate sliders focused via keyboard vs other means (#3487)

This commit is contained in:
mmalerba
2017-03-13 14:13:53 -07:00
committed by GitHub
parent 290f71097b
commit bcb16c615f
6 changed files with 111 additions and 97 deletions
+13 -3
View File
@@ -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;
}
+4 -3
View File
@@ -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}]
+1
View File
@@ -7,6 +7,7 @@
<div class="mat-slider-ticks" [ngStyle]="_ticksStyles"></div>
</div>
<div class="mat-slider-thumb-container" [ngStyle]="_thumbContainerStyles">
<div class="mat-slider-focus-ring"></div>
<div class="mat-slider-thumb"></div>
<div class="mat-slider-thumb-label">
<span class="mat-slider-thumb-label-text">{{displayValue}}</span>
+53 -15
View File
@@ -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);
}
+7 -57
View File
@@ -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', () => {
@@ -572,29 +545,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', () => {
+33 -19
View File
@@ -1,26 +1,29 @@
import {
Component,
ElementRef,
Input,
Output,
ViewEncapsulation,
forwardRef,
EventEmitter,
Optional
forwardRef,
Input,
OnDestroy,
Optional,
Output,
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; }
@@ -369,8 +372,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() {
@@ -389,7 +399,6 @@ export class MdSlider implements ControlValueAccessor {
return;
}
this._isActive = true;
this._isSliding = false;
this._renderer.addFocus();
this._updateValueFromPosition({x: event.clientX, y: event.clientY});
@@ -422,7 +431,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});
}
@@ -432,8 +440,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();
}