Skip to content

Commit 53417d4

Browse files
devversionjelbourn
authored andcommitted
feat: add ripples to button-toggle (#9891)
* Adds ripples to the `MatButtonToggle` component * Fixes that the focus overlay for button toggles shows on mouse/touch press. * Properly stops monitoring through the `FocusMonitor` on component destroy. * Removes unnecessary mixin for focus-overlay color. Color can be always set, because the overlay will be toggled through `opacity` (performance improvement) Closes #9442
1 parent 66a01fb commit 53417d4

File tree

10 files changed

+84
-25
lines changed

10 files changed

+84
-25
lines changed

src/demo-app/button-toggle/button-toggle-demo.html

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@ <h1>Exclusive Selection</h1>
1010

1111
<section class="demo-section">
1212
<mat-button-toggle-group name="alignment" [vertical]="isVertical">
13-
<mat-button-toggle value="left"><mat-icon>format_align_left</mat-icon></mat-button-toggle>
14-
<mat-button-toggle value="center"><mat-icon>format_align_center</mat-icon></mat-button-toggle>
15-
<mat-button-toggle value="right"><mat-icon>format_align_right</mat-icon></mat-button-toggle>
16-
<mat-button-toggle value="justify" [disabled]="isDisabled"><mat-icon>format_align_justify</mat-icon></mat-button-toggle>
13+
<mat-button-toggle value="left"[disabled]="isDisabled">
14+
<mat-icon>format_align_left</mat-icon>
15+
</mat-button-toggle>
16+
<mat-button-toggle value="center" [disabled]="isDisabled">
17+
<mat-icon>format_align_center</mat-icon>
18+
</mat-button-toggle>
19+
<mat-button-toggle value="right" [disabled]="isDisabled">
20+
<mat-icon>format_align_right</mat-icon>
21+
</mat-button-toggle>
22+
<mat-button-toggle value="justify" [disabled]="isDisabled">
23+
<mat-icon>format_align_justify</mat-icon>
24+
</mat-button-toggle>
1725
</mat-button-toggle-group>
1826
</section>
1927

src/lib/button-toggle/_button-toggle-theme.scss

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,15 @@
22
@import '../core/theming/theming';
33
@import '../core/typography/typography-utils';
44

5-
// Applies a focus style to an mat-button-toggle element for each of the supported palettes.
6-
@mixin _mat-button-toggle-focus-color($theme) {
7-
$background: map-get($theme, background);
8-
9-
.mat-button-toggle-focus-overlay {
10-
background-color: mat-color($background, focused-button);
11-
}
12-
}
13-
145
@mixin mat-button-toggle-theme($theme) {
156
$foreground: map-get($theme, foreground);
167
$background: map-get($theme, background);
178

189
.mat-button-toggle {
1910
color: mat-color($foreground, hint-text);
2011

21-
&.cdk-focused {
22-
@include _mat-button-toggle-focus-color($theme);
12+
.mat-button-toggle-focus-overlay {
13+
background-color: mat-color($background, focused-button);
2314
}
2415
}
2516

src/lib/button-toggle/button-toggle-module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88

99
import {NgModule} from '@angular/core';
1010
import {MatButtonToggleGroup, MatButtonToggleGroupMultiple, MatButtonToggle} from './button-toggle';
11-
import {MatCommonModule} from '@angular/material/core';
11+
import {MatCommonModule, MatRippleModule} from '@angular/material/core';
1212
import {UNIQUE_SELECTION_DISPATCHER_PROVIDER} from '@angular/cdk/collections';
1313
import {A11yModule} from '@angular/cdk/a11y';
1414

1515

1616
@NgModule({
17-
imports: [MatCommonModule, A11yModule],
17+
imports: [MatCommonModule, MatRippleModule, A11yModule],
1818
exports: [
1919
MatButtonToggleGroup,
2020
MatButtonToggleGroupMultiple,

src/lib/button-toggle/button-toggle.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<label [attr.for]="inputId" class="mat-button-toggle-label">
1+
<label [attr.for]="inputId" class="mat-button-toggle-label" #label>
22
<input #input class="mat-button-toggle-input cdk-visually-hidden"
33
[type]="_type"
44
[id]="inputId"
@@ -15,3 +15,8 @@
1515
</div>
1616
</label>
1717
<div class="mat-button-toggle-focus-overlay"></div>
18+
19+
<div class="mat-button-toggle-ripple" matRipple
20+
[matRippleTrigger]="label"
21+
[matRippleDisabled]="this.disableRipple || this.disabled">
22+
</div>

src/lib/button-toggle/button-toggle.scss

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ $mat-button-toggle-border-radius: 2px !default;
3030
}
3131
}
3232

33-
3433
.mat-button-toggle-disabled .mat-button-toggle-label-content {
3534
cursor: default;
3635
}
@@ -39,8 +38,11 @@ $mat-button-toggle-border-radius: 2px !default;
3938
white-space: nowrap;
4039
position: relative;
4140

42-
&.cdk-keyboard-focused,
43-
&.cdk-program-focused {
41+
// Similar to components like the checkbox, slide-toggle and radio, we cannot show the focus
42+
// overlay for `.cdk-program-focused` because mouse clicks on the <label> element would be always
43+
// treated as programmatic focus.
44+
// TODO(paul): support `program` as well. See https://github.com/angular/material2/issues/9889
45+
&.cdk-keyboard-focused {
4446
.mat-button-toggle-focus-overlay {
4547
opacity: 1;
4648
}
@@ -68,3 +70,12 @@ $mat-button-toggle-border-radius: 2px !default;
6870
opacity: 0;
6971
@include mat-fill;
7072
}
73+
74+
.mat-button-toggle-ripple {
75+
@include mat-fill;
76+
77+
// Disable pointer events for the ripple container, because the container will overlay the user
78+
// content and we don't want to prevent mouse clicks that should toggle the state.
79+
// Pointer events can be safely disabled because the ripple trigger element is the label element.
80+
pointer-events: none;
81+
}

src/lib/button-toggle/button-toggle.spec.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ComponentFixture,
66
TestBed,
77
} from '@angular/core/testing';
8+
import {dispatchMouseEvent} from '@angular/cdk/testing';
89
import {NgModel, FormsModule, ReactiveFormsModule, FormControl} from '@angular/forms';
910
import {Component, DebugElement} from '@angular/core';
1011
import {By} from '@angular/platform-browser';
@@ -177,8 +178,32 @@ describe('MatButtonToggle with forms', () => {
177178

178179
expect(testComponent.modelValue).toBe('green');
179180
}));
180-
});
181181

182+
it('should show a ripple on label click', () => {
183+
const groupElement = groupDebugElement.nativeElement;
184+
185+
expect(groupElement.querySelectorAll('.mat-ripple-element').length).toBe(0);
186+
187+
dispatchMouseEvent(buttonToggleLabels[0], 'mousedown');
188+
dispatchMouseEvent(buttonToggleLabels[0], 'mouseup');
189+
190+
expect(groupElement.querySelectorAll('.mat-ripple-element').length).toBe(1);
191+
});
192+
193+
it('should allow ripples to be disabled', () => {
194+
const groupElement = groupDebugElement.nativeElement;
195+
196+
testComponent.disableRipple = true;
197+
fixture.detectChanges();
198+
199+
expect(groupElement.querySelectorAll('.mat-ripple-element').length).toBe(0);
200+
201+
dispatchMouseEvent(buttonToggleLabels[0], 'mousedown');
202+
dispatchMouseEvent(buttonToggleLabels[0], 'mouseup');
203+
204+
expect(groupElement.querySelectorAll('.mat-ripple-element').length).toBe(0);
205+
});
206+
});
182207
});
183208

184209
describe('MatButtonToggle without forms', () => {
@@ -647,7 +672,8 @@ class ButtonTogglesInsideButtonToggleGroup {
647672
@Component({
648673
template: `
649674
<mat-button-toggle-group [(ngModel)]="modelValue" (change)="lastEvent = $event">
650-
<mat-button-toggle *ngFor="let option of options" [value]="option.value">
675+
<mat-button-toggle *ngFor="let option of options" [value]="option.value"
676+
[disableRipple]="disableRipple">
651677
{{option.label}}
652678
</mat-button-toggle>
653679
</mat-button-toggle-group>
@@ -661,6 +687,7 @@ class ButtonToggleGroupWithNgModel {
661687
{label: 'Blue', value: 'blue'},
662688
];
663689
lastEvent: MatButtonToggleChange;
690+
disableRipple = false;
664691
}
665692

666693
@Component({

src/lib/button-toggle/button-toggle.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ import {
2828
ViewEncapsulation,
2929
} from '@angular/core';
3030
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
31-
import {CanDisable, mixinDisabled} from '@angular/material/core';
31+
import {
32+
CanDisable,
33+
CanDisableRipple,
34+
mixinDisabled,
35+
mixinDisableRipple
36+
} from '@angular/material/core';
3237

3338
/** Acceptable types for a button toggle. */
3439
export type ToggleType = 'checkbox' | 'radio';
@@ -225,6 +230,11 @@ export class MatButtonToggleGroupMultiple extends _MatButtonToggleGroupMixinBase
225230
private _vertical: boolean = false;
226231
}
227232

233+
// Boilerplate for applying mixins to the MatButtonToggle class.
234+
/** @docs-private */
235+
export class MatButtonToggleBase {}
236+
export const _MatButtonToggleMixinBase = mixinDisableRipple(MatButtonToggleBase);
237+
228238
/** Single button inside of a toggle group. */
229239
@Component({
230240
moduleId: module.id,
@@ -235,6 +245,7 @@ export class MatButtonToggleGroupMultiple extends _MatButtonToggleGroupMixinBase
235245
preserveWhitespaces: false,
236246
exportAs: 'matButtonToggle',
237247
changeDetection: ChangeDetectionStrategy.OnPush,
248+
inputs: ['disableRipple'],
238249
host: {
239250
'[class.mat-button-toggle-standalone]': '!buttonToggleGroup && !buttonToggleGroupMultiple',
240251
'[class.mat-button-toggle-checked]': 'checked',
@@ -243,7 +254,9 @@ export class MatButtonToggleGroupMultiple extends _MatButtonToggleGroupMixinBase
243254
'[attr.id]': 'id',
244255
}
245256
})
246-
export class MatButtonToggle implements OnInit, OnDestroy {
257+
export class MatButtonToggle extends _MatButtonToggleMixinBase
258+
implements OnInit, OnDestroy, CanDisableRipple {
259+
247260
/**
248261
* Attached to the aria-label attribute of the host element. In most cases, arial-labelledby will
249262
* take precedence so this may be omitted.
@@ -331,6 +344,7 @@ export class MatButtonToggle implements OnInit, OnDestroy {
331344
private _buttonToggleDispatcher: UniqueSelectionDispatcher,
332345
private _elementRef: ElementRef,
333346
private _focusMonitor: FocusMonitor) {
347+
super();
334348

335349
this.buttonToggleGroup = toggleGroup;
336350
this.buttonToggleGroupMultiple = toggleGroupMultiple;

src/lib/checkbox/checkbox.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ export class MatCheckbox extends _MatCheckboxMixinBase implements ControlValueAc
321321

322322
/** Function is called whenever the focus changes for the input element. */
323323
private _onInputFocusChange(focusOrigin: FocusOrigin) {
324+
// TODO(paul): support `program`. See https://github.com/angular/material2/issues/9889
324325
if (!this._focusRipple && focusOrigin === 'keyboard') {
325326
this._focusRipple = this.ripple.launch(0, 0, {persistent: true});
326327
} else if (!focusOrigin) {

src/lib/radio/radio.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@ export class MatRadioButton extends _MatRadioButtonMixinBase
593593

594594
/** Function is called whenever the focus changes for the input element. */
595595
private _onInputFocusChange(focusOrigin: FocusOrigin) {
596+
// TODO(paul): support `program`. See https://github.com/angular/material2/issues/9889
596597
if (!this._focusRipple && focusOrigin === 'keyboard') {
597598
this._focusRipple = this._ripple.launch(0, 0, {persistent: true});
598599
} else if (!focusOrigin) {

src/lib/slide-toggle/slide-toggle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ export class MatSlideToggle extends _MatSlideToggleMixinBase implements OnDestro
231231

232232
/** Function is called whenever the focus changes for the input element. */
233233
private _onInputFocusChange(focusOrigin: FocusOrigin) {
234+
// TODO(paul): support `program`. See https://github.com/angular/material2/issues/9889
234235
if (!this._focusRipple && focusOrigin === 'keyboard') {
235236
// For keyboard focus show a persistent ripple as focus indicator.
236237
this._focusRipple = this._ripple.launch(0, 0, {persistent: true});

0 commit comments

Comments
 (0)