Skip to content

Commit baf981a

Browse files
committed
fix(material/button-toggle): animate checkbox
Currently the checkbox inside button toggle is a bit jarring. These changes add an animation to it.
1 parent f1c4173 commit baf981a

File tree

4 files changed

+107
-32
lines changed

4 files changed

+107
-32
lines changed

src/material/button-toggle/button-toggle.html

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,20 @@
1111
[attr.aria-labelledby]="ariaLabelledby"
1212
[attr.aria-disabled]="disabled && disabledInteractive ? 'true' : null"
1313
(click)="_onButtonClick()">
14-
<span class="mat-button-toggle-label-content">
15-
<!-- Render checkmark at the beginning for single-selection. -->
16-
@if (buttonToggleGroup && checked && !buttonToggleGroup.multiple && !buttonToggleGroup.hideSingleSelectionIndicator) {
17-
<mat-pseudo-checkbox
18-
class="mat-mdc-option-pseudo-checkbox"
19-
[disabled]="disabled"
20-
state="checked"
21-
aria-hidden="true"
22-
appearance="minimal"></mat-pseudo-checkbox>
23-
}
24-
<!-- Render checkmark at the beginning for multiple-selection. -->
25-
@if (buttonToggleGroup && checked && buttonToggleGroup.multiple && !buttonToggleGroup.hideMultipleSelectionIndicator) {
14+
@if (buttonToggleGroup && (
15+
!buttonToggleGroup.multiple && !buttonToggleGroup.hideSingleSelectionIndicator ||
16+
buttonToggleGroup.multiple && !buttonToggleGroup.hideMultipleSelectionIndicator)
17+
) {
18+
<div class="mat-button-toggle-checkbox-wrapper">
2619
<mat-pseudo-checkbox
27-
class="mat-mdc-option-pseudo-checkbox"
28-
[disabled]="disabled"
29-
state="checked"
30-
aria-hidden="true"
31-
appearance="minimal"></mat-pseudo-checkbox>
32-
}
20+
[disabled]="disabled"
21+
state="checked"
22+
aria-hidden="true"
23+
appearance="minimal"/>
24+
</div>
25+
}
26+
27+
<span class="mat-button-toggle-label-content">
3328
<ng-content></ng-content>
3429
</span>
3530
</button>

src/material/button-toggle/button-toggle.scss

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
@use '../core/tokens/m2/mat/legacy-button-toggle' as tokens-mat-legacy-button-toggle;
88
@use '../core/tokens/m2/mat/standard-button-toggle' as tokens-mat-standard-button-toggle;
99

10-
$standard-padding: 0 12px !default;
11-
$legacy-padding: 0 16px !default;
12-
$checkmark-padding: 12px !default;
10+
$standard-padding: 12px !default;
11+
$legacy-padding: 16px !default;
12+
$_checkmark-size: 18px !default;
13+
$_checkmark-margin: 12px;
14+
$_checkmark-transition: 150ms 45ms cubic-bezier(0.4, 0, 0.2, 1);
1315

1416
// TODO(crisbeto): these variables aren't used anymore and should be removed.
1517
$legacy-height: 36px !default;
@@ -104,13 +106,51 @@ $_standard-tokens: (
104106
.mat-icon svg {
105107
vertical-align: top;
106108
}
109+
}
107110

108-
.mat-pseudo-checkbox {
109-
margin-right: $checkmark-padding;
110-
[dir='rtl'] & {
111-
margin-right: 0;
112-
margin-left: $checkmark-padding;
113-
}
111+
.mat-button-toggle-checkbox-wrapper {
112+
display: inline-block;
113+
justify-content: flex-start;
114+
align-items: center;
115+
width: 0;
116+
height: $_checkmark-size;
117+
line-height: $_checkmark-size;
118+
overflow: hidden;
119+
box-sizing: border-box;
120+
position: absolute;
121+
top: 50%;
122+
left: $legacy-padding;
123+
124+
// Uses a 3d transform, because otherwise Safari has some some of rendering
125+
// artifact that adds a small gap between the two parts of the checkmark.
126+
transform: translate3d(0, -50%, 0);
127+
128+
[dir='rtl'] & {
129+
left: auto;
130+
right: $legacy-padding;
131+
}
132+
133+
.mat-button-toggle-appearance-standard & {
134+
left: $standard-padding;
135+
}
136+
137+
[dir='rtl'] .mat-button-toggle-appearance-standard & {
138+
left: auto;
139+
right: $standard-padding;
140+
}
141+
142+
.mat-button-toggle-checked & {
143+
width: $_checkmark-size;
144+
}
145+
146+
.mat-button-toggle-animations-enabled & {
147+
transition: width $_checkmark-transition;
148+
}
149+
150+
// Disable the transition in vertical mode since it looks weird.
151+
// There should be a limited amount of usages anyway.
152+
.mat-button-toggle-vertical & {
153+
transition: none;
114154
}
115155
}
116156

@@ -219,7 +259,7 @@ $_standard-tokens: (
219259
.mat-button-toggle-label-content {
220260
@include vendor-prefixes.user-select(none);
221261
display: inline-block;
222-
padding: $legacy-padding;
262+
padding: 0 $legacy-padding;
223263

224264
@include token-utils.use-tokens($_legacy-tokens...) {
225265
@include token-utils.create-token-slot(line-height, height);
@@ -229,7 +269,7 @@ $_standard-tokens: (
229269
position: relative;
230270

231271
.mat-button-toggle-appearance-standard & {
232-
padding: $standard-padding;
272+
padding: 0 $standard-padding;
233273

234274
@include token-utils.use-tokens($_standard-tokens...) {
235275
@include token-utils.create-token-slot(line-height, height);
@@ -292,6 +332,7 @@ $_standard-tokens: (
292332
}
293333

294334
.mat-button-toggle-button {
335+
$checkmark-spacing: $_checkmark-size + $_checkmark-margin;
295336
border: 0;
296337
background: none;
297338
color: inherit;
@@ -302,6 +343,16 @@ $_standard-tokens: (
302343
width: 100%; // Stretch the button in case the consumer set a custom width.
303344
cursor: pointer;
304345

346+
.mat-button-toggle-animations-enabled & {
347+
transition: padding $_checkmark-transition;
348+
}
349+
350+
// Disable the transition in vertical mode since it looks weird.
351+
// There should be a limited amount of usages anyway.
352+
.mat-button-toggle-vertical & {
353+
transition: none;
354+
}
355+
305356
.mat-button-toggle-disabled & {
306357
cursor: default;
307358
}
@@ -310,6 +361,22 @@ $_standard-tokens: (
310361
&::-moz-focus-inner {
311362
border: 0;
312363
}
364+
365+
// Note that we use padding and `position: absolute` to show/hide the checkmark, instead of
366+
// just transitioning it between `width: 18px` and `width: 0`, because it was being shown/hidden
367+
// with `@if` before the transition was added and leaving it in the DOM while hidden can break
368+
// some pre-existing layouts.
369+
&:has(.mat-button-toggle-checkbox-wrapper) {
370+
.mat-button-toggle-checked & {
371+
padding-left: $checkmark-spacing;
372+
}
373+
374+
[dir='rtl'] .mat-button-toggle-checked & {
375+
padding-left: 0;
376+
padding-right: $checkmark-spacing;
377+
}
378+
}
379+
313380
}
314381

315382
// Change the border-radius of the focus indicator to match the

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,9 @@ describe('MatButtonToggle without forms', () => {
598598
buttonToggleLabelElements[0].click();
599599
fixture.detectChanges();
600600

601-
expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(1);
601+
expect(document.querySelectorAll('.mat-button-toggle-checkbox-wrapper-checked').length).toBe(
602+
1,
603+
);
602604
});
603605
});
604606

@@ -763,7 +765,9 @@ describe('MatButtonToggle without forms', () => {
763765
buttonToggleLabelElements[1].click();
764766
fixture.detectChanges();
765767

766-
expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(2);
768+
expect(document.querySelectorAll('.mat-button-toggle-checkbox-wrapper-checked').length).toBe(
769+
2,
770+
);
767771
});
768772
});
769773

src/material/button-toggle/button-toggle.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
booleanAttribute,
3232
inject,
3333
HostAttributeToken,
34+
ANIMATION_MODULE_TYPE,
3435
} from '@angular/core';
3536
import {Direction, Directionality} from '@angular/cdk/bidi';
3637
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
@@ -560,7 +561,7 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
560561
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
561562
private _focusMonitor = inject(FocusMonitor);
562563
private _idGenerator = inject(_IdGenerator);
563-
564+
private _animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});
564565
private _checked = false;
565566

566567
/**
@@ -699,6 +700,14 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
699700
}
700701

701702
ngAfterViewInit() {
703+
// This serves two purposes:
704+
// 1. We don't want the animation to fire on the first render for pre-checked toggles so we
705+
// delay adding the class until the view is rendered.
706+
// 2. We don't want animation if the `NoopAnimationsModule` is provided.
707+
if (this._animationMode !== 'NoopAnimations') {
708+
this._elementRef.nativeElement.classList.add('mat-button-toggle-animations-enabled');
709+
}
710+
702711
this._focusMonitor.monitor(this._elementRef, true);
703712
}
704713

0 commit comments

Comments
 (0)