mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(slider): differentiate sliders focused via keyboard vs other means (#3487)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}]
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user