From 860aa42e177cc722ae0797aa34cb8ef5355c47d9 Mon Sep 17 00:00:00 2001 From: Amy Sorto <8575252+amysorto@users.noreply.github.com> Date: Tue, 20 Jun 2023 09:24:32 -0700 Subject: [PATCH] fix(material/slide-toggle): change slide-toggle to use MDC's token API (#26966) --- .../core/tokens/m2/mat/_slide-toggle.scss | 52 +++++ src/material/core/tokens/m2/mdc/_switch.scss | 182 ++++++++++++++++++ .../tokens/tests/test-validate-tokens.scss | 7 + .../slide-toggle/_slide-toggle-theme.scss | 101 +++------- src/material/slide-toggle/slide-toggle.scss | 58 +++++- 5 files changed, 315 insertions(+), 85 deletions(-) create mode 100644 src/material/core/tokens/m2/mat/_slide-toggle.scss create mode 100644 src/material/core/tokens/m2/mdc/_switch.scss diff --git a/src/material/core/tokens/m2/mat/_slide-toggle.scss b/src/material/core/tokens/m2/mat/_slide-toggle.scss new file mode 100644 index 000000000000..a5a96da5e755 --- /dev/null +++ b/src/material/core/tokens/m2/mat/_slide-toggle.scss @@ -0,0 +1,52 @@ +@use '../../token-utils'; +@use '../../../style/sass-utils'; +@use '../../../typography/typography-utils'; + +// The prefix used to generate the fully qualified name for tokens in this file. +$prefix: (mat, slide-toggle); + +// Tokens that can't be configured through Angular Material's current theming API, +// but may be in a future version of the theming API. +@function get-unthemable-tokens() { + @return (); +} + +// Tokens that can be configured through Angular Material's color theming API. +@function get-color-tokens($config) { + @return (); +} + +// Tokens that can be configured through Angular Material's typography theming API. +@function get-typography-tokens($config) { + // TODO(amysorto): The earlier implementation of the slide-toggle used MDC's APIs to create the + // typography tokens. As a result, we unintentionally allowed `null` typography configs to be + // passed in. Since there a lot of apps that now depend on this pattern, we need this temporary + // fallback. + @if ($config == null) { + $config: mdc-helpers.private-fallback-typography-from-mdc(); + } + + @return ( + label-text-font: typography-utils.font-family($config), + label-text-size: typography-utils.font-size($config, body-2), + label-text-letter-spacing: typography-utils.letter-spacing($config, body-2), + label-text-line-height: typography-utils.line-height($config, body-2), + label-text-weight: typography-utils.font-weight($config, body-2), + ); + } + +// Tokens that can be configured through Angular Material's density theming API. +@function get-density-tokens($config) { + @return (); +} + +// Combines the tokens generated by the above functions into a single map with placeholder values. +// This is used to create token slots. +@function get-token-slots() { + @return sass-utils.deep-merge-all( + get-unthemable-tokens(), + get-color-tokens(token-utils.$placeholder-color-config), + get-typography-tokens(token-utils.$placeholder-typography-config), + get-density-tokens(token-utils.$placeholder-density-config) + ); +} diff --git a/src/material/core/tokens/m2/mdc/_switch.scss b/src/material/core/tokens/m2/mdc/_switch.scss new file mode 100644 index 000000000000..df5a23a46798 --- /dev/null +++ b/src/material/core/tokens/m2/mdc/_switch.scss @@ -0,0 +1,182 @@ +@use 'sass:map'; +@use '../../../style/elevation'; +@use '../../../style/sass-utils'; +@use '../../../theming/theming'; +@use '../../token-utils'; + +// The prefix used to generate the fully qualified name for tokens in this file. +$prefix: (mdc, switch); + +// Tokens that can't be configured through Angular Material's current theming API, +// but may be in a future version of the theming API. +// +// Tokens that are available in MDC, but not used in Angular Material should be mapped to `null`. +// `null` indicates that we are intentionally choosing not to emit a slot or value for the token in +// our CSS. + +@function get-unthemable-tokens() { + @return ( + // Opacity of handle when disabled. + disabled-handle-opacity: 0.38, + // Opacity of icon when disabled and selected. + disabled-selected-icon-opacity: 0.38, + // Opacity of track when disabled. + disabled-track-opacity: 0.12, + // Opacity of icon when disabled and unselected. + disabled-unselected-icon-opacity: 0.38, + // Height of handle. + handle-height: 20px, + // Border radius of handle. + handle-shape: 10px, + // Width of handle. + handle-width: 20px, + // Width and height of icon when selected. + selected-icon-size: 18px, + // Height of track. + track-height: 14px, + // Border radius of track. + track-shape: 7px, + // Width of track. + track-width: 36px, + // Width and height of icon when unselected. + unselected-icon-size: 18px, + // The diameter of the handle ripple. + state-layer-size: 40px, + // Opacity of ripple when selected and focused. + selected-focus-state-layer-opacity: 0.12, + // Opacity of ripple when selected and on hover. + selected-hover-state-layer-opacity: 0.04, + // Opacity of ripple when selected and pressed. + selected-pressed-state-layer-opacity: 0.1, + // Opacity of ripple when unselected and focused. + unselected-focus-state-layer-opacity: 0.12, + // Opacity of ripple when unselected and on hover. + unselected-hover-state-layer-opacity: 0.04, + // Opacity of ripple when unselected and pressed. + unselected-pressed-state-layer-opacity: 0.1, + ); +} + +// Tokens that can be configured through Angular Material's color theming API. +@function get-color-tokens($config) { + $foreground: map.get($config, foreground); + $elevation: theming.get-color-from-palette($foreground, elevation); + $is-dark: map.get($config, is-dark); + $on-surface: if($is-dark, #f5f5f5, #424242); + $hairline: if($is-dark, #616161, #e0e0e0); + $on-surface-variant: if($is-dark, #9e9e9e, #616161); + $on-surface-state-content: if($is-dark, #fafafa, #212121); + $disabled-handle-color: if($is-dark, #000, #424242); + $icon-color: if($is-dark, #212121, #fff); + + // The default for tokens that rely on the theme will use the primary palette + $theme-color-tokens: private-get-color-palette-color-tokens( + map.get($config, primary), + $is-dark + ); + + @return map.merge( + $theme-color-tokens, + ( + // Color of handle when selected while disabled. + disabled-selected-handle-color: $disabled-handle-color, + // Color of handle when selected while disabled. + disabled-selected-icon-color: $icon-color, + // Color of track when selected while disabled. + disabled-selected-track-color: $on-surface, + // Color of handle when unselected while disabled. + disabled-unselected-handle-color: $disabled-handle-color, + // Color of icon when unselected while disabled. + disabled-unselected-icon-color: $icon-color, + // Color of track when disabled. + 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. + unselected-focus-handle-color: $on-surface-state-content, + // Color of ripple when unselected and focused. + unselected-focus-state-layer-color: $on-surface, + // Color of track when unselected and focused. + unselected-focus-track-color: $hairline, + // Color of handle when unselected. + unselected-handle-color: $on-surface-variant, + // Color of handle when unselected and on hover. + unselected-hover-handle-color: $on-surface-state-content, + // Color of ripple when unselected and on hover. + unselected-hover-state-layer-color: $on-surface, + // Color of track when unselected and on hover. + unselected-hover-track-color: $hairline, + // Color of icon color when unselected. + unselected-icon-color: $icon-color, + // Color of handle when unselected and pressed. + unselected-pressed-handle-color: $on-surface-state-content, + // Color of ripple when unselected and pressed. + unselected-pressed-state-layer-color: $on-surface, + // Color of track when unselected and pressed. + unselected-pressed-track-color: $hairline, + // Color of track when selected. + unselected-track-color: $hairline, + ) + ); +} + +// Generates the mapping for the properties that change based on the slide toggle color. +@function private-get-color-palette-color-tokens($color-palette, $is-dark) { + $primary: theming.get-color-from-palette($color-palette, if($is-dark, 300, 600)); + $state-content: theming.get-color-from-palette($color-palette, if($is-dark, 200, 900)); + $inverse: theming.get-color-from-palette($color-palette, if($is-dark, 600, 300)); + + @return ( + // Color of ripple when selected and focused. + selected-focus-state-layer-color: $primary, + // Color of handle when selected + selected-handle-color: $primary, + // Color of ripple when selected and on hover. + selected-hover-state-layer-color: $primary, + // Color of ripple when selected and pressed. + selected-pressed-state-layer-color: $primary, + // Color of handle when selected and focused. + selected-focus-handle-color: $state-content, + // Color of handle when selected and on hover. + selected-hover-handle-color: $state-content, + // Color of handle when selected and pressed. + selected-pressed-handle-color: $state-content, + // Color of track when selected and focused. + selected-focus-track-color: $inverse, + // Color of track when selected and on hover. + selected-hover-track-color: $inverse, + /// Color of track when selected and pressed. + selected-pressed-track-color: $inverse, + // Color of track when selected. + selected-track-color: $inverse, + ); +} + +// Tokens that can be configured through Angular Material's typography theming API. +@function get-typography-tokens($config) { + @return (); +} + +// Tokens that can be configured through Angular Material's density theming API. +@function get-density-tokens($config) { + @return (); +} + +// Combines the tokens generated by the above functions into a single map with placeholder values. +// This is used to create token slots. +@function get-token-slots() { + @return sass-utils.deep-merge-all( + get-unthemable-tokens(), + get-color-tokens(token-utils.$placeholder-color-config), + get-typography-tokens(token-utils.$placeholder-typography-config), + get-density-tokens(token-utils.$placeholder-density-config) + ); +} diff --git a/src/material/core/tokens/tests/test-validate-tokens.scss b/src/material/core/tokens/tests/test-validate-tokens.scss index 56c5040fbfe5..5b656e09ff07 100644 --- a/src/material/core/tokens/tests/test-validate-tokens.scss +++ b/src/material/core/tokens/tests/test-validate-tokens.scss @@ -10,6 +10,7 @@ @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/tab/tab-theme' as mdc-tab-theme; @use '@material/snackbar/snackbar-theme' as mdc-snackbar-theme; @@ -25,6 +26,7 @@ @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/tab' as tokens-mdc-tab; @use '../m2/mdc/snack-bar' as tokens-mdc-snack-bar; @@ -87,6 +89,11 @@ $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/_slide-toggle-theme.scss b/src/material/slide-toggle/_slide-toggle-theme.scss index f119caa73444..8400964ec63f 100644 --- a/src/material/slide-toggle/_slide-toggle-theme.scss +++ b/src/material/slide-toggle/_slide-toggle-theme.scss @@ -1,73 +1,12 @@ @use 'sass:map'; -@use 'sass:color'; @use '@material/switch/switch-theme' as mdc-switch-theme; -@use '@material/theme/color-palette' as mdc-color-palette; @use '@material/form-field' as mdc-form-field; @use '../core/theming/theming'; @use '../core/mdc-helpers/mdc-helpers'; @use '../core/typography/typography'; - - -// Generates all color mapping for the properties that only change based on the theme. -@function _get-theme-base-map($is-dark) { - $on-surface: if($is-dark, mdc-color-palette.$grey-100, mdc-color-palette.$grey-800); - $hairline: if($is-dark, mdc-color-palette.$grey-700, mdc-color-palette.$grey-300); - $on-surface-variant: if($is-dark, mdc-color-palette.$grey-500, mdc-color-palette.$grey-700); - $on-surface-state-content: if($is-dark, mdc-color-palette.$grey-50, mdc-color-palette.$grey-900); - $disabled-handle-color: if($is-dark, #000, mdc-color-palette.$grey-800); - $icon-color: if($is-dark, mdc-color-palette.$grey-900, #fff); - - @return ( - disabled-selected-handle-color: $disabled-handle-color, - disabled-unselected-handle-color: $disabled-handle-color, - - disabled-selected-track-color: $on-surface, - disabled-unselected-track-color: $on-surface, - unselected-focus-state-layer-color: $on-surface, - unselected-pressed-state-layer-color: $on-surface, - unselected-hover-state-layer-color: $on-surface, - - unselected-focus-track-color: $hairline, - unselected-hover-track-color: $hairline, - unselected-pressed-track-color: $hairline, - unselected-track-color: $hairline, - - unselected-focus-handle-color: $on-surface-state-content, - unselected-hover-handle-color: $on-surface-state-content, - unselected-pressed-handle-color: $on-surface-state-content, - - handle-surface-color: surface, - unselected-handle-color: $on-surface-variant, - - selected-icon-color: $icon-color, - disabled-selected-icon-color: $icon-color, - disabled-unselected-icon-color: $icon-color, - unselected-icon-color: $icon-color, - ); -} - -// Generates the mapping for the properties that change based on the slide toggle color. -@function _get-theme-color-map($color-palette, $is-dark) { - $primary: theming.get-color-from-palette($color-palette, if($is-dark, 300, 600)); - $state-content: theming.get-color-from-palette($color-palette, if($is-dark, 200, 900)); - $inverse: theming.get-color-from-palette($color-palette, if($is-dark, 600, 300)); - - @return ( - selected-focus-state-layer-color: $primary, - selected-handle-color: $primary, - selected-hover-state-layer-color: $primary, - selected-pressed-state-layer-color: $primary, - - selected-focus-handle-color: $state-content, - selected-hover-handle-color: $state-content, - selected-pressed-handle-color: $state-content, - - selected-focus-track-color: $inverse, - selected-hover-track-color: $inverse, - selected-pressed-track-color: $inverse, - selected-track-color: $inverse, - ); -} +@use '../core/tokens/m2/mdc/switch' as m2-mdc-switch; +@use '../core/tokens/m2/mat/slide-toggle' as m2-mat-slide-toggle; +@use '../core/tokens/token-utils'; @mixin color($config-or-theme) { $config: theming.get-color-config($config-or-theme); @@ -77,28 +16,32 @@ $is-dark: map.get($config, is-dark); $foreground: map.get($config, foreground); + $mdc-switch-color-tokens: m2-mdc-switch.get-color-tokens($config); + @include mdc-helpers.using-mdc-theme($config) { - // MDC's switch doesn't support a `color` property. We add support - // for it by adding a CSS class for accent and warn style. + // Add values for MDC slide toggles tokens .mat-mdc-slide-toggle { @include mdc-form-field.core-styles($query: mdc-helpers.$mdc-theme-styles-query); - @include mdc-switch-theme.theme(_get-theme-base-map($is-dark)); + @include mdc-switch-theme.theme($mdc-switch-color-tokens); // MDC should set the disabled color on the label, but doesn't, so we do it here instead. .mdc-switch--disabled + label { color: theming.get-color-from-palette($foreground, disabled-text); } - &.mat-primary { - @include mdc-switch-theme.theme(_get-theme-color-map($primary, $is-dark)); - } - + // Change the color palette related tokens to accent or warn if applicable &.mat-accent { - @include mdc-switch-theme.theme(_get-theme-color-map($accent, $is-dark)); + @include mdc-switch-theme.theme(m2-mdc-switch.private-get-color-palette-color-tokens( + map.get($config, accent), + map.get($config, is-dark) + )); } &.mat-warn { - @include mdc-switch-theme.theme(_get-theme-color-map($warn, $is-dark)); + @include mdc-switch-theme.theme(m2-mdc-switch.private-get-color-palette-color-tokens( + map.get($config, warn), + map.get($config, is-dark) + )); } } } @@ -107,8 +50,17 @@ @mixin typography($config-or-theme) { $config: typography.private-typography-to-2018-config( theming.get-typography-config($config-or-theme)); - @include mdc-helpers.using-mdc-typography($config) { + $mdc-switch-typography-tokens: m2-mdc-switch.get-typography-tokens($config); + $mat-slide-toggle-typography-tokens: m2-mat-slide-toggle.get-typography-tokens($config); + + //Add values for MDC slide toggle tokens + .mat-mdc-slide-toggle { @include mdc-form-field.core-styles($query: mdc-helpers.$mdc-typography-styles-query); + @include mdc-switch-theme.theme($mdc-switch-typography-tokens); + @include token-utils.create-token-values( + m2-mat-slide-toggle.$prefix, + $mat-slide-toggle-typography-tokens + ); } } @@ -138,4 +90,3 @@ } } } - diff --git a/src/material/slide-toggle/slide-toggle.scss b/src/material/slide-toggle/slide-toggle.scss index 4296aaa6a79b..9a65dd5bcf4c 100644 --- a/src/material/slide-toggle/slide-toggle.scss +++ b/src/material/slide-toggle/slide-toggle.scss @@ -6,11 +6,57 @@ @use '@material/ripple' as mdc-ripple; @use '../core/mdc-helpers/mdc-helpers'; @use '../core/style/layout-common'; +@use '@material/theme/custom-properties' as mdc-custom-properties; +@use '../core/tokens/m2/mdc/switch' as m2-mdc-switch; +@use '../core/tokens/m2/mat/slide-toggle' as mat-slide-toggle-tokens; +@use '../core/tokens/token-utils'; +@include mdc-custom-properties.configure($emit-fallback-values: false, $emit-fallback-vars: false) { + $mdc-switch-token-slots: m2-mdc-switch.get-token-slots(); -@include mdc-helpers.disable-mdc-fallback-declarations { @include mdc-form-field.core-styles($query: mdc-helpers.$mdc-base-styles-query); - @include mdc-switch.static-styles-without-ripple; + // Add the MDC switch static styles. + @include mdc-switch.static-styles-without-ripple(); + + .mdc-switch { + // Add the official slots for the MDC switch + @include mdc-switch-theme.theme-styles($mdc-switch-token-slots); + + // 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. + + // TODO(amysorto): remove these overrides once MDC emits correct value for + // box-shadow. + + // Override box-shadow with correct values. + &:enabled .mdc-switch__shadow { + box-shadow: var(--mdc-switch-handle-elevation-shadow); + } + + &:disabled .mdc-switch__shadow { + box-shadow: var(--mdc-switch-disabled-handle-elevation-shadow); + } + + // Add default values for MDC switch tokens that aren't outputted by the theming API + @include mdc-switch-theme.theme(m2-mdc-switch.get-unthemable-tokens()); + } + + + // Add slots for custom Angular Material slide-toggle tokens. + @include token-utils.use-tokens( + mat-slide-toggle-tokens.$prefix, + mat-slide-toggle-tokens.get-token-slots() + ) { + .mat-mdc-slide-toggle .mdc-label { + @include token-utils.create-token-slot(font-family, label-text-font); + @include token-utils.create-token-slot(font-size, label-text-size); + @include token-utils.create-token-slot(letter-spacing, label-text-letter-spacing); + @include token-utils.create-token-slot(line-height, label-text-line-height); + @include token-utils.create-token-slot(font-weight, label-text-weight); + } + } } .mat-mdc-slide-toggle { @@ -20,14 +66,6 @@ // Remove the native outline since we use the ripple for focus indication. outline: 0; - .mdc-switch { - // MDC theme styles also include structural styles so we have to include the theme at least - // once here. The values will be overwritten by our own theme file afterwards. - @include mdc-helpers.disable-mdc-fallback-declarations { - @include mdc-switch-theme.theme-styles(mdc-switch-theme.$light-theme); - } - } - // 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();