Skip to content

Commit

Permalink
fix(material/button-toggle): animate checkbox (#30025)
Browse files Browse the repository at this point in the history
Currently the checkbox inside button toggle is a bit jarring. These changes add an animation to it.

(cherry picked from commit 00959ec)
  • Loading branch information
crisbeto committed Nov 20, 2024
1 parent edac406 commit 2d7e078
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 32 deletions.
31 changes: 13 additions & 18 deletions src/material/button-toggle/button-toggle.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,20 @@
[attr.aria-labelledby]="ariaLabelledby"
[attr.aria-disabled]="disabled && disabledInteractive ? 'true' : null"
(click)="_onButtonClick()">
<span class="mat-button-toggle-label-content">
<!-- Render checkmark at the beginning for single-selection. -->
@if (buttonToggleGroup && checked && !buttonToggleGroup.multiple && !buttonToggleGroup.hideSingleSelectionIndicator) {
<mat-pseudo-checkbox
class="mat-mdc-option-pseudo-checkbox"
[disabled]="disabled"
state="checked"
aria-hidden="true"
appearance="minimal"></mat-pseudo-checkbox>
}
<!-- Render checkmark at the beginning for multiple-selection. -->
@if (buttonToggleGroup && checked && buttonToggleGroup.multiple && !buttonToggleGroup.hideMultipleSelectionIndicator) {
@if (buttonToggleGroup && (
!buttonToggleGroup.multiple && !buttonToggleGroup.hideSingleSelectionIndicator ||
buttonToggleGroup.multiple && !buttonToggleGroup.hideMultipleSelectionIndicator)
) {
<div class="mat-button-toggle-checkbox-wrapper">
<mat-pseudo-checkbox
class="mat-mdc-option-pseudo-checkbox"
[disabled]="disabled"
state="checked"
aria-hidden="true"
appearance="minimal"></mat-pseudo-checkbox>
}
[disabled]="disabled"
state="checked"
aria-hidden="true"
appearance="minimal"/>
</div>
}

<span class="mat-button-toggle-label-content">
<ng-content></ng-content>
</span>
</button>
Expand Down
89 changes: 78 additions & 11 deletions src/material/button-toggle/button-toggle.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
@use '../core/tokens/m2/mat/legacy-button-toggle' as tokens-mat-legacy-button-toggle;
@use '../core/tokens/m2/mat/standard-button-toggle' as tokens-mat-standard-button-toggle;

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

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

.mat-pseudo-checkbox {
margin-right: $checkmark-padding;
[dir='rtl'] & {
margin-right: 0;
margin-left: $checkmark-padding;
}
.mat-button-toggle-checkbox-wrapper {
display: inline-block;
justify-content: flex-start;
align-items: center;
width: 0;
height: $_checkmark-size;
line-height: $_checkmark-size;
overflow: hidden;
box-sizing: border-box;
position: absolute;
top: 50%;
left: $legacy-padding;

// Uses a 3d transform, because otherwise Safari has some some of rendering
// artifact that adds a small gap between the two parts of the checkmark.
transform: translate3d(0, -50%, 0);

[dir='rtl'] & {
left: auto;
right: $legacy-padding;
}

.mat-button-toggle-appearance-standard & {
left: $standard-padding;
}

[dir='rtl'] .mat-button-toggle-appearance-standard & {
left: auto;
right: $standard-padding;
}

.mat-button-toggle-checked & {
width: $_checkmark-size;
}

.mat-button-toggle-animations-enabled & {
transition: width $_checkmark-transition;
}

// Disable the transition in vertical mode since it looks weird.
// There should be a limited amount of usages anyway.
.mat-button-toggle-vertical & {
transition: none;
}
}

Expand Down Expand Up @@ -219,7 +259,7 @@ $_standard-tokens: (
.mat-button-toggle-label-content {
@include vendor-prefixes.user-select(none);
display: inline-block;
padding: $legacy-padding;
padding: 0 $legacy-padding;

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

.mat-button-toggle-appearance-standard & {
padding: $standard-padding;
padding: 0 $standard-padding;

@include token-utils.use-tokens($_standard-tokens...) {
@include token-utils.create-token-slot(line-height, height);
Expand Down Expand Up @@ -292,6 +332,7 @@ $_standard-tokens: (
}

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

.mat-button-toggle-animations-enabled & {
transition: padding $_checkmark-transition;
}

// Disable the transition in vertical mode since it looks weird.
// There should be a limited amount of usages anyway.
.mat-button-toggle-vertical & {
transition: none;
}

.mat-button-toggle-disabled & {
cursor: default;
}
Expand All @@ -310,6 +361,22 @@ $_standard-tokens: (
&::-moz-focus-inner {
border: 0;
}

// Note that we use padding and `position: absolute` to show/hide the checkmark, instead of
// just transitioning it between `width: 18px` and `width: 0`, because it was being shown/hidden
// with `@if` before the transition was added and leaving it in the DOM while hidden can break
// some pre-existing layouts.
&:has(.mat-button-toggle-checkbox-wrapper) {
.mat-button-toggle-checked & {
padding-left: $checkmark-spacing;
}

[dir='rtl'] .mat-button-toggle-checked & {
padding-left: 0;
padding-right: $checkmark-spacing;
}
}

}

// Change the border-radius of the focus indicator to match the
Expand Down
12 changes: 10 additions & 2 deletions src/material/button-toggle/button-toggle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,11 @@ describe('MatButtonToggle without forms', () => {
buttonToggleLabelElements[0].click();
fixture.detectChanges();

expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(1);
expect(
fixture.nativeElement.querySelectorAll(
'.mat-button-toggle-checked .mat-button-toggle-checkbox-wrapper',
).length,
).toBe(1);
});
});

Expand Down Expand Up @@ -763,7 +767,11 @@ describe('MatButtonToggle without forms', () => {
buttonToggleLabelElements[1].click();
fixture.detectChanges();

expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(2);
expect(
fixture.nativeElement.querySelectorAll(
'.mat-button-toggle-checked .mat-button-toggle-checkbox-wrapper',
).length,
).toBe(2);
});
});

Expand Down
11 changes: 10 additions & 1 deletion src/material/button-toggle/button-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
booleanAttribute,
inject,
HostAttributeToken,
ANIMATION_MODULE_TYPE,
} from '@angular/core';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
Expand Down Expand Up @@ -560,7 +561,7 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
private _focusMonitor = inject(FocusMonitor);
private _idGenerator = inject(_IdGenerator);

private _animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});
private _checked = false;

/**
Expand Down Expand Up @@ -699,6 +700,14 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
}

ngAfterViewInit() {
// This serves two purposes:
// 1. We don't want the animation to fire on the first render for pre-checked toggles so we
// delay adding the class until the view is rendered.
// 2. We don't want animation if the `NoopAnimationsModule` is provided.
if (this._animationMode !== 'NoopAnimations') {
this._elementRef.nativeElement.classList.add('mat-button-toggle-animations-enabled');
}

this._focusMonitor.monitor(this._elementRef, true);
}

Expand Down

0 comments on commit 2d7e078

Please sign in to comment.