From 3d0d868007c254b4d33c06ab2e07460d35b5bab8 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 10 Jul 2024 14:22:41 +0200 Subject: [PATCH] refactor(material/slide-toggle): simplify structural styles Reworks the structural styles for the slide toggle to be smaller and easier to maintain. --- src/material/core/tokens/m2/mdc/_switch.scss | 16 +- src/material/core/tokens/m3/mdc/_switch.scss | 15 +- .../tokens/tests/test-validate-tokens.scss | 7 - src/material/slide-toggle/BUILD.bazel | 1 + .../slide-toggle/_slide-toggle-theme.scss | 47 +- src/material/slide-toggle/slide-toggle.scss | 626 +++++++++++++----- 6 files changed, 506 insertions(+), 206 deletions(-) diff --git a/src/material/core/tokens/m2/mdc/_switch.scss b/src/material/core/tokens/m2/mdc/_switch.scss index c840b69e8c16..2be60c93d7ca 100644 --- a/src/material/core/tokens/m2/mdc/_switch.scss +++ b/src/material/core/tokens/m2/mdc/_switch.scss @@ -53,12 +53,16 @@ $prefix: (mdc, switch); unselected-hover-state-layer-opacity: 0.04, // Opacity of ripple when unselected and pressed. unselected-pressed-state-layer-opacity: 0.1, + + // Unused tokens + handle-elevation: null, + handle-shadow-color: null, + disabled-handle-elevation: null, ); } // Tokens that can be configured through Angular Material's color theming API. @function get-color-tokens($theme) { - $elevation: inspection.get-theme-color($theme, foreground, elevation); $is-dark: inspection.get-theme-type($theme) == dark; $on-surface: if($is-dark, #f5f5f5, #424242); $hairline: if($is-dark, #616161, #e0e0e0); @@ -87,12 +91,6 @@ $prefix: (mdc, switch); disabled-unselected-track-color: $on-surface, // Color of slide-toggle handle's surface. handle-surface-color: surface, - // Elevation level for handle. - handle-elevation: 1, - // Color of handle's shadow - handle-shadow-color: if($elevation != null, $elevation, elevation.$color), - // Elevation level for handle when disabled. - disabled-handle-elevation: 0, // Color of icon (ex. checkmark) when selected selected-icon-color: $icon-color, // Color of handle when unselected and focused. @@ -119,6 +117,10 @@ $prefix: (mdc, switch); unselected-pressed-track-color: $hairline, // Color of track when selected. unselected-track-color: $hairline, + // Elevation level for handle. + handle-elevation-shadow: elevation.get-box-shadow(1), + // Elevation level for handle when disabled. + disabled-handle-elevation-shadow: elevation.get-box-shadow(0), ) ); } diff --git a/src/material/core/tokens/m3/mdc/_switch.scss b/src/material/core/tokens/m3/mdc/_switch.scss index ca77b61dc4f4..d42c3681b6e7 100644 --- a/src/material/core/tokens/m3/mdc/_switch.scss +++ b/src/material/core/tokens/m3/mdc/_switch.scss @@ -1,4 +1,5 @@ @use 'sass:map'; +@use '../../../style/elevation'; @use '../../token-utils'; // The prefix used to generate the fully qualified name for tokens in this file. @@ -10,7 +11,17 @@ $prefix: (mdc, switch); /// @param {Map} $token-slots Possible token slots /// @return {Map} A set of tokens for the MDC switch @function get-tokens($systems, $exclude-hardcoded, $token-slots) { - $mdc-tokens: token-utils.get-mdc-tokens('switch', $systems, $exclude-hardcoded); + $tokens: token-utils.get-mdc-tokens('switch', $systems, $exclude-hardcoded); + $elevation-tokens: (handle-elevation, disabled-handle-elevation); + + @each $token in $elevation-tokens { + $elevation: map.get($tokens, $token); + + @if ($elevation != null) { + $tokens: map.set($tokens, $token + '-shadow', elevation.get-box-shadow($elevation)); + } + } + $variant-tokens: ( primary: (), // Default, no overrides needed secondary: ( @@ -66,5 +77,5 @@ $prefix: (mdc, switch); ), ); - @return token-utils.namespace-tokens($prefix, ($mdc-tokens, $variant-tokens), $token-slots); + @return token-utils.namespace-tokens($prefix, ($tokens, $variant-tokens), $token-slots); } diff --git a/src/material/core/tokens/tests/test-validate-tokens.scss b/src/material/core/tokens/tests/test-validate-tokens.scss index 42fd0bfa32d3..863436c0ba0d 100644 --- a/src/material/core/tokens/tests/test-validate-tokens.scss +++ b/src/material/core/tokens/tests/test-validate-tokens.scss @@ -13,7 +13,6 @@ @use '@material/list/list-theme' as mdc-list-theme; @use '@material/tooltip/plain-tooltip-theme' as mdc-plaintip-tooltip-theme; @use '@material/radio/radio-theme' as mdc-radio-theme; -@use '@material/switch/switch-theme' as mdc-switch-theme; @use '@material/tab-indicator/tab-indicator-theme' as mdc-tab-indicator-theme; @use '@material/snackbar/snackbar-theme' as mdc-snackbar-theme; @use '@material/slider/slider-theme' as mdc-slider-theme; @@ -34,7 +33,6 @@ @use '../m2/mdc/outlined-card' as tokens-mdc-outlined-card; @use '../m2/mdc/plain-tooltip' as tokens-mdc-plain-tooltip; @use '../m2/mdc/radio' as tokens-mdc-radio; -@use '../m2/mdc/switch' as tokens-mdc-switch; @use '../m2/mdc/tab-indicator' as tokens-mdc-tab-indicator; @use '../m2/mdc/snack-bar' as tokens-mdc-snack-bar; @use '../m2/mdc/slider' as tokens-mdc-slider; @@ -99,11 +97,6 @@ $slots: tokens-mdc-radio.get-token-slots(), $reference: mdc-radio-theme.$light-theme ); -@include validate-slots( - $component: 'm2.mdc.switch', - $slots: tokens-mdc-switch.get-token-slots(), - $reference: mdc-switch-theme.$light-theme -); @include validate-slots( $component: 'm2.mdc.tab-indicator', $slots: tokens-mdc-tab-indicator.get-token-slots(), diff --git a/src/material/slide-toggle/BUILD.bazel b/src/material/slide-toggle/BUILD.bazel index 9b63b2ab7f8c..6ba6e715c3b7 100644 --- a/src/material/slide-toggle/BUILD.bazel +++ b/src/material/slide-toggle/BUILD.bazel @@ -42,6 +42,7 @@ sass_binary( src = "slide-toggle.scss", deps = [ "//:mdc_sass_lib", + "//src/cdk:sass_lib", "//src/material/core:core_scss_lib", ], ) diff --git a/src/material/slide-toggle/_slide-toggle-theme.scss b/src/material/slide-toggle/_slide-toggle-theme.scss index e0a4a11eeef6..ffc57bb46fcb 100644 --- a/src/material/slide-toggle/_slide-toggle-theme.scss +++ b/src/material/slide-toggle/_slide-toggle-theme.scss @@ -1,4 +1,3 @@ -@use '@material/switch/switch-theme' as mdc-switch-theme; @use '../core/style/sass-utils'; @use '../core/theming/theming'; @use '../core/theming/inspection'; @@ -16,12 +15,12 @@ @include _theme-from-tokens(inspection.get-theme-tokens($theme, base)); } @else { @include sass-utils.current-selector-or-root() { - $mat-tokens: tokens-mat-switch.get-unthemable-tokens(); - $mdc-tokens: tokens-mdc-switch.get-unthemable-tokens(); - @include mdc-switch-theme.theme($mdc-tokens); + @include token-utils.create-token-values(tokens-mdc-switch.$prefix, + tokens-mdc-switch.get-unthemable-tokens()); .mat-mdc-slide-toggle { - @include token-utils.create-token-values(tokens-mat-switch.$prefix, $mat-tokens); + @include token-utils.create-token-values(tokens-mat-switch.$prefix, + tokens-mat-switch.get-unthemable-tokens()); } } } @@ -36,13 +35,12 @@ @if inspection.get-theme-version($theme) == 1 { @include _theme-from-tokens(inspection.get-theme-tokens($theme, color), $options...); } @else { - $is-dark: inspection.get-theme-type($theme) == dark; - $mat-tokens: tokens-mat-switch.get-color-tokens($theme); $mdc-tokens: tokens-mdc-switch.get-color-tokens($theme); // Add values for MDC slide toggles tokens @include sass-utils.current-selector-or-root() { - @include mdc-switch-theme.theme($mdc-tokens); + @include token-utils.create-token-values(tokens-mdc-switch.$prefix, + tokens-mdc-switch.get-color-tokens($theme)); // TODO(wagnermaciel): Use our token system to define this css variable. --mdc-switch-disabled-label-text-color: #{inspection.get-theme-color( @@ -52,19 +50,18 @@ )}; .mat-mdc-slide-toggle { - @include token-utils.create-token-values(tokens-mat-switch.$prefix, $mat-tokens); + @include token-utils.create-token-values(tokens-mat-switch.$prefix, + tokens-mat-switch.get-color-tokens($theme)); // Change the color palette related tokens to accent or warn if applicable &.mat-accent { - @include mdc-switch-theme.theme( - tokens-mdc-switch.private-get-color-palette-color-tokens($theme, accent) - ); + @include token-utils.create-token-values(tokens-mdc-switch.$prefix, + tokens-mdc-switch.private-get-color-palette-color-tokens($theme, accent)); } &.mat-warn { - @include mdc-switch-theme.theme( - tokens-mdc-switch.private-get-color-palette-color-tokens($theme, warn) - ); + @include token-utils.create-token-values(tokens-mdc-switch.$prefix, + tokens-mdc-switch.private-get-color-palette-color-tokens($theme, warn)); } } } @@ -77,15 +74,13 @@ @if inspection.get-theme-version($theme) == 1 { @include _theme-from-tokens(inspection.get-theme-tokens($theme, typography)); } @else { - $mat-tokens: tokens-mat-switch.get-typography-tokens($theme); - $mdc-tokens: tokens-mdc-switch.get-typography-tokens($theme); - - // Add values for MDC slide toggle tokens @include sass-utils.current-selector-or-root() { - @include mdc-switch-theme.theme($mdc-tokens); + @include token-utils.create-token-values(tokens-mdc-switch.$prefix, + tokens-mdc-switch.get-typography-tokens($theme)); .mat-mdc-slide-toggle { - @include token-utils.create-token-values(tokens-mat-switch.$prefix, $mat-tokens); + @include token-utils.create-token-values(tokens-mat-switch.$prefix, + tokens-mat-switch.get-typography-tokens($theme)); } } } @@ -98,12 +93,12 @@ @include _theme-from-tokens(inspection.get-theme-tokens($theme, density)); } @else { @include sass-utils.current-selector-or-root() { - $mat-tokens: tokens-mat-switch.get-density-tokens($theme); - $mdc-tokens: tokens-mdc-switch.get-density-tokens($theme); - @include mdc-switch-theme.theme(tokens-mdc-switch.get-density-tokens($theme)); + @include token-utils.create-token-values(tokens-mdc-switch.$prefix, + tokens-mdc-switch.get-density-tokens($theme)); .mat-mdc-slide-toggle { - @include token-utils.create-token-values(tokens-mat-switch.$prefix, $mat-tokens); + @include token-utils.create-token-values(tokens-mat-switch.$prefix, + tokens-mat-switch.get-density-tokens($theme)); } } } @@ -158,6 +153,6 @@ // only the mdc-switch does. $mat-switch-tokens: token-utils.get-tokens-for($tokens, tokens-mat-switch.$prefix); - @include mdc-switch-theme.theme($mdc-switch-tokens); + @include token-utils.create-token-values(tokens-mdc-switch.$prefix, $mdc-switch-tokens); @include token-utils.create-token-values(tokens-mat-switch.$prefix, $mat-switch-tokens); } diff --git a/src/material/slide-toggle/slide-toggle.scss b/src/material/slide-toggle/slide-toggle.scss index 54cb1b956271..9e2cc83ee952 100644 --- a/src/material/slide-toggle/slide-toggle.scss +++ b/src/material/slide-toggle/slide-toggle.scss @@ -1,254 +1,552 @@ -@use '@material/switch/switch' as mdc-switch; -@use '@material/switch/switch-theme' as mdc-switch-theme; -@use '@material/theme/custom-properties' as mdc-custom-properties; +@use '@angular/cdk'; @use '../core/style/layout-common'; @use '../core/tokens/m2/mat/switch' as tokens-mat-switch; @use '../core/tokens/m2/mdc/switch' as tokens-mdc-switch; @use '../core/tokens/token-utils'; -@include mdc-custom-properties.configure($emit-fallback-values: false, $emit-fallback-vars: false) { - $mdc-switch-token-slots: tokens-mdc-switch.get-token-slots(); +$_mdc-slots: (tokens-mdc-switch.$prefix, tokens-mdc-switch.get-token-slots()); +$_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots()); + +.mdc-switch { + align-items: center; + background: none; + border: none; + cursor: pointer; + display: inline-flex; + flex-shrink: 0; + margin: 0; + outline: none; + overflow: visible; + padding: 0; + position: relative; + + &:disabled { + cursor: default; + pointer-events: none; + } - // Add the MDC switch static styles. - @include mdc-switch.static-styles-without-ripple(); + @include token-utils.use-tokens($_mdc-slots...) { + @include token-utils.create-token-slot(width, track-width); + } +} - .mat-mdc-slide-toggle .mat-internal-form-field { - @include token-utils.use-tokens( - tokens-mat-switch.$prefix, - tokens-mat-switch.get-token-slots() - ) { - @include token-utils.create-token-slot(color, label-text-color); - @include token-utils.create-token-slot(font-family, label-text-font); - @include token-utils.create-token-slot(line-height, label-text-line-height); - @include token-utils.create-token-slot(font-size, label-text-size); - @include token-utils.create-token-slot(letter-spacing, label-text-tracking); - @include token-utils.create-token-slot(font-weight, label-text-weight); +.mdc-switch__track { + overflow: hidden; + position: relative; + width: 100%; + + @include token-utils.use-tokens($_mdc-slots...) { + @include token-utils.create-token-slot(height, track-height); + @include token-utils.create-token-slot(border-radius, track-shape); + + .mdc-switch:disabled & { + @include token-utils.create-token-slot(opacity, disabled-track-opacity); } } - // TODO(wagnermaciel): Use our custom token system to emit this css rule. - .mat-mdc-slide-toggle .mdc-switch--disabled + label { - color: var(--mdc-switch-disabled-label-text-color); - } + &::before, + &::after { + border: 1px solid transparent; + border-radius: inherit; + box-sizing: border-box; + content: ''; + height: 100%; + left: 0; + position: absolute; + width: 100%; - .mdc-switch { - // Add the official slots for the MDC switch - @include mdc-switch-theme.theme-styles($mdc-switch-token-slots); + @include cdk.high-contrast(active, off) { + border-color: currentColor; + } - // The tokens are outputting handle-elevation and disabled-handle-elevation - // as the values for box-shadow. Rather the value that should be displayed - // is calculated using the tokens for elevation and the handle-shadow-color - // within theme-styles and is stored in new variables that end in -shadow. + @include token-utils.use-tokens($_mat-slots...) { + @include token-utils.create-token-slot(border-width, track-outline-width); + @include token-utils.create-token-slot(border-color, track-outline-color); - // TODO(amysorto): remove these overrides once MDC emits correct value for - // box-shadow. + .mdc-switch--selected & { + @include token-utils.create-token-slot(border-width, selected-track-outline-width); + @include token-utils.create-token-slot(border-color, selected-track-outline-color); + } - // Override box-shadow with correct values. - &:enabled .mdc-switch__shadow { - box-shadow: var(--mdc-switch-handle-elevation-shadow); + .mdc-switch--disabled & { + @include token-utils.create-token-slot(border-width, + disabled-unselected-track-outline-width); + @include token-utils.create-token-slot(border-color, + disabled-unselected-track-outline-color); + } } + } - &:disabled .mdc-switch__shadow { - box-shadow: var(--mdc-switch-disabled-handle-elevation-shadow); + &::before { + transition: transform 75ms 0ms cubic-bezier(0, 0, 0.2, 1); + transform: translateX(0); + + .mdc-switch--selected & { + transition: transform 75ms 0ms cubic-bezier(0.4, 0, 0.6, 1); + transform: translateX(100%); + + [dir='rtl'] .mdc-switch--selected & { + transform: translateX(-100%); + } } - } -} -.mat-mdc-slide-toggle { - display: inline-block; - -webkit-tap-highlight-color: transparent; + @include token-utils.use-tokens($_mat-slots...) { + .mdc-switch--selected & { + @include token-utils.create-token-slot(opacity, hidden-track-opacity); + @include token-utils.create-token-slot(transition, hidden-track-transition); + } - // Remove the native outline since we use the ripple for focus indication. - outline: 0; + .mdc-switch--unselected & { + @include token-utils.create-token-slot(opacity, visible-track-opacity); + @include token-utils.create-token-slot(transition, visible-track-transition); + } + } - // The ripple needs extra specificity so the base ripple styling doesn't override its `position`. - .mat-mdc-slide-toggle-ripple, - #{mdc-switch.$ripple-target}::after { - @include layout-common.fill(); - border-radius: 50%; - // Disable pointer events for the ripple container so that it doesn't eat the mouse events meant - // for the input. Pointer events can be safely disabled because the ripple trigger element is - // the host element. - pointer-events: none; - // Fixes the ripples not clipping to the border radius on Safari. Uses `:not(:empty)` - // in order to avoid creating extra layers when there aren't any ripples. - &:not(:empty) { - transform: translateZ(0); + @include token-utils.use-tokens($_mdc-slots...) { + @include token-utils.create-token-slot(background, unselected-track-color); + + .mdc-switch:enabled:hover:not(:focus):not(:active) & { + @include token-utils.create-token-slot(background, unselected-hover-track-color); + } + + .mdc-switch:enabled:focus:not(:active) & { + @include token-utils.create-token-slot(background, unselected-focus-track-color); + } + + .mdc-switch:enabled:active & { + @include token-utils.create-token-slot(background, unselected-pressed-track-color); + } + + .mdc-switch:disabled & { + @include token-utils.create-token-slot(background, disabled-unselected-track-color); + } } } - #{mdc-switch.$ripple-target}::after { - content: ''; - opacity: 0; - } + &::after { + transform: translateX(-100%); - .mdc-switch:hover #{mdc-switch.$ripple-target}::after { - opacity: 0.04; - transition: 75ms opacity cubic-bezier(0, 0, 0.2, 1); - } + [dir='rtl'] & { + transform: translateX(100%); + } - // Needs a little more specificity so the :hover styles don't override it. - &.mat-mdc-slide-toggle-focused { - .mdc-switch #{mdc-switch.$ripple-target}::after { - opacity: 0.12; + .mdc-switch--selected & { + transform: translateX(0); } - // For slide-toggles render the focus indicator when we know - // the hidden input is focused (slightly different for each control). - .mat-mdc-focus-indicator::before { - content: ''; + @include token-utils.use-tokens($_mat-slots...) { + .mdc-switch--selected & { + @include token-utils.create-token-slot(opacity, visible-track-opacity); + @include token-utils.create-token-slot(transition, visible-track-transition); + } + + .mdc-switch--unselected & { + @include token-utils.create-token-slot(opacity, hidden-track-opacity); + @include token-utils.create-token-slot(transition, hidden-track-transition); + } } - } - .mat-ripple-element { - opacity: 0.12; + @include token-utils.use-tokens($_mdc-slots...) { + @include token-utils.create-token-slot(background, selected-track-color); + + .mdc-switch:enabled:hover:not(:focus):not(:active) & { + @include token-utils.create-token-slot(background, selected-hover-track-color); + } + + .mdc-switch:enabled:focus:not(:active) & { + @include token-utils.create-token-slot(background, selected-focus-track-color); + } + + .mdc-switch:enabled:active & { + @include token-utils.create-token-slot(background, selected-pressed-track-color); + } + + .mdc-switch:disabled & { + @include token-utils.create-token-slot(background, disabled-selected-track-color); + } + } } +} - // Slide-toggle components have to set `border-radius: 50%` in order to support density scaling - // which will clip a square focus indicator so we have to turn it into a circle. - .mat-mdc-focus-indicator::before { - border-radius: 50%; +.mdc-switch__handle-track { + height: 100%; + pointer-events: none; + position: absolute; + top: 0; + transition: transform 75ms 0ms cubic-bezier(0.4, 0, 0.2, 1); + left: 0; + right: auto; + transform: translateX(0); + + [dir='rtl'] & { + left: auto; + right: 0; } - &._mat-animation-noopable { - .mdc-switch__handle-track, - .mdc-elevation-overlay, - .mdc-switch__icon, - .mdc-switch__handle::before, - .mdc-switch__handle::after, - .mdc-switch__track::before, - .mdc-switch__track::after { - transition: none; + .mdc-switch--selected & { + transform: translateX(100%); + + [dir='rtl'] & { + transform: translateX(-100%); } } - // If our slide-toggle is enabled the cursor on label should appear as a pointer. - .mdc-switch:enabled + .mdc-label { - cursor: pointer; + @include token-utils.use-tokens($_mdc-slots...) { + width: calc(100% - var(#{token-utils.get-token-variable(handle-width)})); } } -// Used for M3 animations. Does not affect the M2 slide-toggle -// because the width and height are static, and the margin is unused. .mdc-switch__handle { + display: flex; + pointer-events: auto; + position: absolute; + top: 50%; + transform: translateY(-50%); + left: 0; + right: auto; + + // Used for M3 animations. Does not affect the M2 slide-toggle + // because the width and height are static, and the margin is unused. transition: width 75ms cubic-bezier(0.4, 0, 0.2, 1), height 75ms cubic-bezier(0.4, 0, 0.2, 1), margin 75ms cubic-bezier(0.4, 0, 0.2, 1); -} - -// The hidden and visible track represent whichever track is visible or -// hidden in the ui. This could be the .mdc-switch__track :before or -// :after depending on whether the switch is selected or unselected. -// -// The m2 slide-toggle uses transforms to hide & show the tracks while -// the m3 slide-toggle uses opacity. -@include token-utils.use-tokens(tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots()) { - .mdc-switch--selected .mdc-switch__track { - &::before { - @include token-utils.create-token-slot(opacity, hidden-track-opacity); - @include token-utils.create-token-slot(transition, hidden-track-transition); - } - &::after { - @include token-utils.create-token-slot(opacity, visible-track-opacity); - @include token-utils.create-token-slot(transition, visible-track-transition); - } + [dir='rtl'] & { + left: auto; + right: 0; } - .mdc-switch--unselected .mdc-switch__track { - &::before { - @include token-utils.create-token-slot(opacity, visible-track-opacity); - @include token-utils.create-token-slot(transition, visible-track-transition); - } - &::after { - @include token-utils.create-token-slot(opacity, hidden-track-opacity); - @include token-utils.create-token-slot(transition, hidden-track-transition); - } + @include token-utils.use-tokens($_mdc-slots...) { + @include token-utils.create-token-slot(width, handle-width); + @include token-utils.create-token-slot(height, handle-height); + @include token-utils.create-token-slot(border-radius, handle-shape); } -} -@include token-utils.use-tokens(tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots()) { - .mat-mdc-slide-toggle { - .mdc-switch--unselected .mdc-switch__handle { + @include token-utils.use-tokens($_mat-slots...) { + .mat-mdc-slide-toggle .mdc-switch--unselected & { @include token-utils.create-token-slot(width, unselected-handle-size); @include token-utils.create-token-slot(height, unselected-handle-size); + @include token-utils.create-token-slot(margin, unselected-handle-horizontal-margin); + + &:has(.mdc-switch__icons) { + @include token-utils.create-token-slot(margin, + unselected-with-icon-handle-horizontal-margin); + } } - .mdc-switch--selected .mdc-switch__handle { + .mat-mdc-slide-toggle .mdc-switch--selected & { @include token-utils.create-token-slot(width, selected-handle-size); @include token-utils.create-token-slot(height, selected-handle-size); + @include token-utils.create-token-slot(margin, selected-handle-horizontal-margin); + + &:has(.mdc-switch__icons) { + @include token-utils.create-token-slot(margin, selected-with-icon-handle-horizontal-margin); + } } - .mdc-switch__handle:has(.mdc-switch__icons) { + .mat-mdc-slide-toggle &:has(.mdc-switch__icons) { @include token-utils.create-token-slot(width, with-icon-handle-size); @include token-utils.create-token-slot(height, with-icon-handle-size); } - &:active .mdc-switch:not(.mdc-switch--disabled) .mdc-switch__handle { + .mat-mdc-slide-toggle:active .mdc-switch:not(.mdc-switch--disabled) & { @include token-utils.create-token-slot(width, pressed-handle-size); @include token-utils.create-token-slot(height, pressed-handle-size); } + + .mat-mdc-slide-toggle:active .mdc-switch--selected:not(.mdc-switch--disabled) & { + @include token-utils.create-token-slot(margin, selected-pressed-handle-horizontal-margin); + } + + .mat-mdc-slide-toggle:active .mdc-switch--unselected:not(.mdc-switch--disabled) & { + @include token-utils.create-token-slot(margin, unselected-pressed-handle-horizontal-margin); + } + + .mdc-switch--disabled.mdc-switch--selected &::after { + @include token-utils.create-token-slot(opacity, disabled-selected-handle-opacity); + } + + .mdc-switch--disabled.mdc-switch--unselected &::after { + @include token-utils.create-token-slot(opacity, disabled-unselected-handle-opacity); + } } -} -@include token-utils.use-tokens(tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots()) { - .mat-mdc-slide-toggle { - .mdc-switch--selected .mdc-switch__handle { - @include token-utils.create-token-slot(margin, selected-handle-horizontal-margin); + &::before, + &::after { + border: 1px solid transparent; + border-radius: inherit; + box-sizing: border-box; + content: ''; + width: 100%; + height: 100%; + left: 0; + position: absolute; + top: 0; + transition: background-color 75ms 0ms cubic-bezier(0.4, 0, 0.2, 1), + border-color 75ms 0ms cubic-bezier(0.4, 0, 0.2, 1); + z-index: -1; + + @include cdk.high-contrast(active, off) { + border-color: currentColor; + } + } - &:has(.mdc-switch__icons) { - @include token-utils.create-token-slot(margin, selected-with-icon-handle-horizontal-margin); + @include token-utils.use-tokens($_mdc-slots...) { + &::after { + .mdc-switch--selected:enabled & { + @include token-utils.create-token-slot(background, selected-handle-color); } - } - .mdc-switch--unselected .mdc-switch__handle { - @include token-utils.create-token-slot(margin, unselected-handle-horizontal-margin); + .mdc-switch--selected:enabled:hover:not(:focus):not(:active) & { + @include token-utils.create-token-slot(background, selected-hover-handle-color); + } - &:has(.mdc-switch__icons) { - @include token-utils.create-token-slot( - margin, - unselected-with-icon-handle-horizontal-margin - ); + .mdc-switch--selected:enabled:focus:not(:active) & { + @include token-utils.create-token-slot(background, selected-focus-handle-color); + } + + .mdc-switch--selected:enabled:active & { + @include token-utils.create-token-slot(background, selected-pressed-handle-color); + } + + .mdc-switch--selected:disabled & { + @include token-utils.create-token-slot(background, disabled-selected-handle-color); + } + + .mdc-switch--unselected:enabled & { + @include token-utils.create-token-slot(background, unselected-handle-color); + } + + .mdc-switch--unselected:enabled:hover:not(:focus):not(:active) & { + @include token-utils.create-token-slot(background, unselected-hover-handle-color); + } + + .mdc-switch--unselected:enabled:focus:not(:active) & { + @include token-utils.create-token-slot(background, unselected-focus-handle-color); + } + + .mdc-switch--unselected:enabled:active & { + @include token-utils.create-token-slot(background, unselected-pressed-handle-color); + } + + .mdc-switch--unselected:disabled & { + @include token-utils.create-token-slot(background, disabled-unselected-handle-color); } } - &:active .mdc-switch--selected:not(.mdc-switch--disabled) .mdc-switch__handle { - @include token-utils.create-token-slot(margin, selected-pressed-handle-horizontal-margin); + &::before { + @include token-utils.create-token-slot(background, handle-surface-color); + } + } +} + +.mdc-switch__shadow { + border-radius: inherit; + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + + @include token-utils.use-tokens($_mdc-slots...) { + .mdc-switch:enabled & { + @include token-utils.create-token-slot(box-shadow, handle-elevation-shadow); } - &:active .mdc-switch--unselected:not(.mdc-switch--disabled) .mdc-switch__handle { - @include token-utils.create-token-slot(margin, unselected-pressed-handle-horizontal-margin); + .mdc-switch:disabled & { + @include token-utils.create-token-slot(box-shadow, disabled-handle-elevation-shadow); } } } -@include token-utils.use-tokens(tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots()) { - .mdc-switch__track::after, - .mdc-switch__track::before { - @include token-utils.create-token-slot(border-width, track-outline-width); - @include token-utils.create-token-slot(border-color, track-outline-color); +.mdc-switch__ripple { + left: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + z-index: -1; + + @include token-utils.use-tokens($_mdc-slots...) { + @include token-utils.create-token-slot(width, state-layer-size); + @include token-utils.create-token-slot(height, state-layer-size); + } + + &::after { + content: ''; + opacity: 0; + + .mdc-switch:disabled & { + display: none; + } + + .mdc-switch:hover & { + opacity: 0.04; + transition: 75ms opacity cubic-bezier(0, 0, 0.2, 1); + } + + // Needs a little more specificity so the :hover styles don't override it. + .mat-mdc-slide-toggle.mat-mdc-slide-toggle-focused .mdc-switch & { + opacity: 0.12; + } + + @include token-utils.use-tokens($_mdc-slots...) { + .mdc-switch--unselected:enabled:hover:not(:focus) & { + @include token-utils.create-token-slot(background, unselected-hover-state-layer-color); + } + + .mdc-switch--unselected:enabled:focus & { + @include token-utils.create-token-slot(background, unselected-focus-state-layer-color); + } + + .mdc-switch--unselected:enabled:active & { + @include token-utils.create-token-slot(background, unselected-pressed-state-layer-color); + @include token-utils.create-token-slot(opacity, unselected-pressed-state-layer-opacity); + transition: opacity 75ms linear; + } + + .mdc-switch--selected:enabled:hover:not(:focus) & { + @include token-utils.create-token-slot(background, selected-hover-state-layer-color); + } + + .mdc-switch--selected:enabled:focus & { + @include token-utils.create-token-slot(background, selected-focus-state-layer-color); + } + + .mdc-switch--selected:enabled:active & { + @include token-utils.create-token-slot(background, selected-pressed-state-layer-color); + @include token-utils.create-token-slot(opacity, selected-pressed-state-layer-opacity); + transition: opacity 75ms linear; + } + } } +} + +.mdc-switch__icons { + position: relative; + height: 100%; + width: 100%; + z-index: 1; + + @include token-utils.use-tokens($_mdc-slots...) { + .mdc-switch--unselected:disabled & { + @include token-utils.create-token-slot(opacity, disabled-unselected-icon-opacity); + } - .mdc-switch--selected .mdc-switch__track::after, - .mdc-switch--selected .mdc-switch__track::before { - @include token-utils.create-token-slot(border-width, selected-track-outline-width); - @include token-utils.create-token-slot(border-color, selected-track-outline-color); + .mdc-switch--selected:disabled & { + @include token-utils.create-token-slot(opacity, disabled-selected-icon-opacity); + } } +} + +.mdc-switch__icon { + bottom: 0; + left: 0; + margin: auto; + position: absolute; + right: 0; + top: 0; + opacity: 0; + transition: opacity 30ms 0ms cubic-bezier(0.4, 0, 1, 1); + + @include token-utils.use-tokens($_mdc-slots...) { + .mdc-switch--unselected & { + @include token-utils.create-token-slot(width, unselected-icon-size); + @include token-utils.create-token-slot(height, unselected-icon-size); + @include token-utils.create-token-slot(fill, unselected-icon-color); + } + + .mdc-switch--unselected:disabled & { + @include token-utils.create-token-slot(fill, disabled-unselected-icon-color); + } + + .mdc-switch--selected & { + @include token-utils.create-token-slot(width, selected-icon-size); + @include token-utils.create-token-slot(height, selected-icon-size); + @include token-utils.create-token-slot(fill, selected-icon-color); + } - .mdc-switch--disabled .mdc-switch__track::after, - .mdc-switch--disabled .mdc-switch__track::before { - @include token-utils.create-token-slot(border-width, disabled-unselected-track-outline-width); - @include token-utils.create-token-slot(border-color, disabled-unselected-track-outline-color); + .mdc-switch--selected:disabled & { + @include token-utils.create-token-slot(fill, disabled-selected-icon-color); + } } } -@include token-utils.use-tokens(tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots()) { - .mdc-switch--disabled.mdc-switch--selected .mdc-switch__handle::after { - @include token-utils.create-token-slot(opacity, disabled-selected-handle-opacity); +.mdc-switch--selected .mdc-switch__icon--on, +.mdc-switch--unselected .mdc-switch__icon--off { + opacity: 1; + transition: opacity 45ms 30ms cubic-bezier(0, 0, 0.2, 1); +} + +.mat-mdc-slide-toggle { + display: inline-block; + -webkit-tap-highlight-color: transparent; + + // Remove the native outline since we use the ripple for focus indication. + outline: 0; + + // The ripple needs extra specificity so the base ripple styling doesn't override its `position`. + .mat-mdc-slide-toggle-ripple, + .mdc-switch__ripple::after { + @include layout-common.fill(); + border-radius: 50%; + // Disable pointer events for the ripple container so that it doesn't eat the mouse events meant + // for the input. Pointer events can be safely disabled because the ripple trigger element is + // the host element. + pointer-events: none; + // Fixes the ripples not clipping to the border radius on Safari. Uses `:not(:empty)` + // in order to avoid creating extra layers when there aren't any ripples. + &:not(:empty) { + transform: translateZ(0); + } + } + + // Needs a little more specificity so the :hover styles don't override it. + // For slide-toggles render the focus indicator when we know + // the hidden input is focused (slightly different for each control). + &.mat-mdc-slide-toggle-focused .mat-mdc-focus-indicator::before { + content: ''; + } + + .mat-internal-form-field { + @include token-utils.use-tokens($_mat-slots...) { + @include token-utils.create-token-slot(color, label-text-color); + @include token-utils.create-token-slot(font-family, label-text-font); + @include token-utils.create-token-slot(line-height, label-text-line-height); + @include token-utils.create-token-slot(font-size, label-text-size); + @include token-utils.create-token-slot(letter-spacing, label-text-tracking); + @include token-utils.create-token-slot(font-weight, label-text-weight); + } + } + + .mat-ripple-element { + opacity: 0.12; + } + + // Slide-toggle components have to set `border-radius: 50%` in order to support density scaling + // which will clip a square focus indicator so we have to turn it into a circle. + .mat-mdc-focus-indicator::before { + border-radius: 50%; + } + + &._mat-animation-noopable { + .mdc-switch__handle-track, + .mdc-switch__icon, + .mdc-switch__handle::before, + .mdc-switch__handle::after, + .mdc-switch__track::before, + .mdc-switch__track::after { + transition: none; + } + } + + // If our slide-toggle is enabled the cursor on label should appear as a pointer. + .mdc-switch:enabled + .mdc-label { + cursor: pointer; } - .mdc-switch--disabled.mdc-switch--unselected .mdc-switch__handle::after { - @include token-utils.create-token-slot(opacity, disabled-unselected-handle-opacity); + // TODO(wagnermaciel): Use our custom token system to emit this css rule. + .mdc-switch--disabled + label { + color: var(--mdc-switch-disabled-label-text-color); } }