// // Copyright 2016 Google Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // // stylelint-disable selector-class-pattern -- // Selector '.mdc-*' should only be used in this project. @use 'sass:map'; @use 'sass:math'; @use '@material/animation/functions' as functions2; @use '@material/density/functions' as density-functions; @use '@material/density/variables' as density-variables; @use '@material/feature-targeting/feature-targeting'; @use '@material/ripple/ripple'; @use '@material/ripple/ripple-theme'; @use '@material/theme/color-custom-properties'; @use '@material/theme/custom-properties'; @use '@material/theme/keys'; @use '@material/theme/theme'; @use '@material/theme/theme-color'; @use '@material/theme/shadow-dom'; @use '@material/touch-target/touch-target'; @use './checkbox-custom-properties'; $baseline-theme-color: secondary !default; $mark-color: theme-color.prop-value(on-secondary) !default; $border-color: rgba(theme-color.prop-value(on-surface), 0.54) !default; $disabled-color: rgba(theme-color.prop-value(on-surface), 0.38) !default; $ripple-size: 40px !default; $icon-size: 18px !default; $mark-stroke-size: math.div(2, 15) * $icon-size !default; $border-width: 2px !default; $transition-duration: 90ms !default; $item-spacing: 4px !default; $focus-indicator-opacity: map.get( ripple-theme.$dark-ink-opacities, focus ) !default; $minimum-size: 28px !default; $maximum-size: $ripple-size !default; $density-scale: density-variables.$default-scale !default; $density-config: ( size: ( minimum: $minimum-size, default: $ripple-size, maximum: $maximum-size, ), ) !default; $ripple-target: '.mdc-checkbox__ripple'; $custom-property-prefix: 'checkbox'; // TODO(b/188417756): State layer (ripple) size token is missing including // `state-layer-size`. // TODO(b/188529841): `selected-checkmark-color` and `disabled-selected-checkmark-color` does not exist in tokens. $light-theme: ( disabled-selected-checkmark-color: $mark-color, disabled-selected-icon-color: $disabled-color, disabled-selected-icon-opacity: null, disabled-unselected-icon-color: $disabled-color, disabled-unselected-icon-opacity: null, selected-checkmark-color: $mark-color, selected-focus-icon-color: $baseline-theme-color, selected-focus-state-layer-color: theme-color.$on-surface, selected-focus-state-layer-opacity: 0.12, selected-hover-icon-color: $baseline-theme-color, selected-hover-state-layer-color: $baseline-theme-color, selected-hover-state-layer-opacity: map.get(ripple-theme.$dark-ink-opacities, hover), selected-icon-color: $baseline-theme-color, selected-pressed-icon-color: $baseline-theme-color, selected-pressed-state-layer-color: theme-color.$on-surface, selected-pressed-state-layer-opacity: map.get(ripple-theme.$dark-ink-opacities, pressed), state-layer-size: $ripple-size, unselected-focus-icon-color: $baseline-theme-color, unselected-focus-state-layer-color: theme-color.$on-surface, unselected-focus-state-layer-opacity: map.get(ripple-theme.$dark-ink-opacities, focus), unselected-hover-icon-color: $baseline-theme-color, unselected-hover-state-layer-color: theme-color.$on-surface, unselected-hover-state-layer-opacity: map.get(ripple-theme.$dark-ink-opacities, hover), unselected-icon-color: $border-color, unselected-pressed-icon-color: $border-color, unselected-pressed-state-layer-color: theme-color.$on-surface, unselected-pressed-state-layer-opacity: map.get(ripple-theme.$dark-ink-opacities, pressed), ); $forced-colors-theme: ( disabled-selected-checkmark-color: ButtonFace, disabled-selected-icon-color: GrayText, disabled-selected-icon-opacity: 1, disabled-unselected-icon-color: GrayText, disabled-unselected-icon-opacity: 1, selected-checkmark-color: ButtonText, ); @mixin theme($theme) { // TODO(b/251881053): Replace with `validate-theme`. @include theme.validate-theme-styles($light-theme, $theme); @include keys.declare-custom-properties( $theme, $prefix: $custom-property-prefix ); } @mixin theme-styles($theme) { @include theme.validate-theme-styles($light-theme, $theme); $theme: keys.create-theme-properties( $theme, $prefix: $custom-property-prefix ); @include disabled-container-colors( $unmarked-stroke-color: map.get($theme, disabled-unselected-icon-color), $marked-fill-color: map.get($theme, disabled-selected-icon-color) ); @include ink-color(map.get($theme, selected-checkmark-color)); @include disabled-ink-color( map.get($theme, disabled-selected-checkmark-color) ); @include _icon-color( map.get($theme, unselected-icon-color), map.get($theme, selected-icon-color) ); &:hover { @include _icon-color( map.get($theme, unselected-hover-icon-color), map.get($theme, selected-hover-icon-color) ); } @include ripple-theme.focus() { @include _icon-color( map.get($theme, unselected-focus-icon-color), map.get($theme, selected-focus-icon-color) ); } @include ripple-theme.active() { @include _icon-color( map.get($theme, unselected-pressed-icon-color), map.get($theme, selected-pressed-icon-color) ); } @include ripple-color( $color: map.get($theme, unselected-hover-state-layer-color), $opacity-map: ( hover: map.get($theme, unselected-hover-state-layer-opacity), focus: map.get($theme, unselected-focus-state-layer-opacity), press: map.get($theme, unselected-pressed-state-layer-opacity), ) ); @include focus-indicator-color( $color: map.get($theme, selected-hover-state-layer-color), $opacity-map: ( hover: map.get($theme, selected-hover-state-layer-opacity), focus: map.get($theme, selected-focus-state-layer-opacity), press: map.get($theme, selected-pressed-state-layer-opacity), ) ); @include ripple-size(map.get($theme, state-layer-size)); // Set touch target to ripple size. @include touch-target( map.get($theme, state-layer-size), map.get($theme, state-layer-size) ); } $light-theme-deprecated: ( density-scale: 0, checkmark-color: $mark-color, container-checked-color: $baseline-theme-color, container-checked-hover-color: null, container-disabled-color: $disabled-color, outline-color: $border-color, outline-hover-color: null, ripple-color: theme-color.$on-surface, ripple-opacity: ripple-theme.$dark-ink-opacities, ripple-checked-color: $baseline-theme-color, ripple-checked-opacity: ripple-theme.$dark-ink-opacities, ); /// Sets theme to checkbox based on provided theme configuration. /// Only emits theme related styles. /// @param {Map} $theme - Theme configuration to use for theming checkbox. @mixin theme-deprecated($theme, $query: feature-targeting.all()) { @include theme.validate-theme($light-theme-deprecated, $theme); $ripple-color: map.get($theme, ripple-color); $ripple-opacity: map.get($theme, ripple-opacity); @if $ripple-opacity == null { $ripple-opacity: (); } @if $ripple-color { @include ripple-color( $color: $ripple-color, $opacity-map: $ripple-opacity, $query: $query ); } $ripple-checked-color: map.get($theme, ripple-checked-color); $ripple-checked-opacity: map.get($theme, ripple-checked-opacity); @if $ripple-checked-opacity == null { $ripple-checked-opacity: (); } @if $ripple-checked-color { @include focus-indicator-color( $color: $ripple-checked-color, $opacity-map: $ripple-checked-opacity, $query: $query ); } $density-scale: map.get($theme, density-scale); @if $density-scale != null { @include density($density-scale: $density-scale, $query: $query); } $outline-color: map.get($theme, outline-color); $container-checked-color: map.get($theme, container-checked-color); @if ( ($outline-color and not $container-checked-color) or (not $outline-color and $container-checked-color) ) { @error 'Both `outline-color` and `container-checked-color` keys should be provided.'; } @if ($outline-color and $container-checked-color) { @include container-colors( $unmarked-stroke-color: $outline-color, $marked-stroke-color: $container-checked-color, $marked-fill-color: $container-checked-color, $query: $query ); } $outline-hover-color: map.get($theme, outline-hover-color); $container-checked-hover-color: map.get( $theme, container-checked-hover-color ); @if ( ($outline-hover-color and not $container-checked-hover-color) or (not $outline-hover-color and $container-checked-hover-color) ) { @error 'Both `outline-hover-color` and `container-checked-hover-color` keys should be provided.'; } @if ($outline-hover-color and $container-checked-hover-color) { @include ripple-theme.states-selector() { @include container-colors( $unmarked-stroke-color: $outline-hover-color, $marked-stroke-color: $container-checked-hover-color, $marked-fill-color: $container-checked-hover-color, $query: $query ); } } $container-disabled-color: map.get($theme, container-disabled-color); @if $container-disabled-color { @include disabled-container-colors( $unmarked-stroke-color: $container-disabled-color, $marked-fill-color: $container-disabled-color, $query: $query ); } $checkmark-color: map.get($theme, checkmark-color); @if $checkmark-color { @include ink-color($checkmark-color, $query: $query); @include disabled-ink-color($checkmark-color, $query: $query); } } /// /// @param {Number | String} $density-scale - Density scale value for component. /// Supported density scale values `-3`, `-2`, `-1`, `0`. /// @return Returns ripple size of checkbox for given density scale. /// @function get-ripple-size($density-scale) { @return density-functions.prop-value( $density-config: $density-config, $density-scale: $density-scale, $property-name: size ); } /// /// Sets density scale for checkbox. /// /// @param {Number | String} $density-scale - Density scale value for component. Supported density scale values /// `-3`, `-2`, `-1`, `0`. /// @mixin density($density-scale, $query: feature-targeting.all()) { $size: get-ripple-size($density-scale); @include ripple-size($size, $query: $query); @include touch-target($size, $ripple-size: $size, $query: $query); } /// Sets ripple size of checkbox and optionally set touch target size which can /// be more than the size of ripple. /// @param {Number} $ripple-size - Visual ripple size of checkbox. @mixin ripple-size($ripple-size, $query: feature-targeting.all()) { $feat-structure: feature-targeting.create-target($query, structure); @if $ripple-size and not custom-properties.is-custom-prop($ripple-size) { $ripple-size: custom-properties.create( checkbox-custom-properties.$ripple-size, $ripple-size ); } $checkbox-padding: 'calc((_ripple-size - _icon-size) / 2)'; $replace: ( _ripple-size: $ripple-size, _icon-size: $icon-size, ); @include feature-targeting.targets($feat-structure) { @include theme.property(padding, $checkbox-padding, $replace: $replace); } .mdc-checkbox__background { @include feature-targeting.targets($feat-structure) { @include theme.property(top, $checkbox-padding, $replace: $replace); @include theme.property(left, $checkbox-padding, $replace: $replace); } } } /// Sets the touch target size and appropriate margin to accommodate the /// touch target. /// @param {Number} $touch-target-size Size of touch target (Native input) in `px`. /// @param {Number} $ripple-size Size of ripple in `px`. @mixin touch-target( $touch-target-size, $ripple-size, $query: feature-targeting.all() ) { $feat-structure: feature-targeting.create-target($query, structure); @if $touch-target-size { @if not custom-properties.is-custom-prop($touch-target-size) { $touch-target-size: custom-properties.create( checkbox-custom-properties.$touch-target-size, $touch-target-size ); } $margin: 'calc((_touch-target-size - _ripple-size) / 2)'; $replace: ( _touch-target-size: $touch-target-size, _ripple-size: $ripple-size, ); @include feature-targeting.targets($feat-structure) { @include theme.property(margin, $margin, $replace: $replace); } $offset: 'calc((_ripple-size - _touch-target-size) / 2)'; @include feature-targeting.targets($feat-structure) { .mdc-checkbox__native-control { @include theme.property(top, $offset, $replace: $replace); @include theme.property(right, $offset, $replace: $replace); @include theme.property(left, $offset, $replace: $replace); @include theme.property(width, $touch-target-size); @include theme.property(height, $touch-target-size); } } } } @mixin _icon-color($unselected-color, $selected-color) { @if $unselected-color and $selected-color { @include container-colors( $unmarked-stroke-color: $unselected-color, $marked-stroke-color: $selected-color, $marked-fill-color: $selected-color ); } @else if $unselected-color or $selected-color { @error 'Both unselected and selected icon colors should be provided.'; } } /// /// Sets stroke & fill colors for both marked and unmarked state of enabled checkbox. /// Set $generate-keyframes to false to prevent the mixin from generating @keyframes /// @param {Color} $unmarked-stroke-color - The desired stroke color for the unmarked state /// @param {Color} $unmarked-fill-color - The desired fill color for the unmarked state /// @param {Color} $marked-stroke-color - The desired stroke color for the marked state /// @param {Color} $marked-fill-color - The desired fill color for the marked state /// @param {Boolean} $generate-keyframes [true] - Whether animation keyframes should be generated /// @mixin container-colors( $unmarked-stroke-color: $border-color, $unmarked-fill-color: transparent, $marked-stroke-color: $baseline-theme-color, $marked-fill-color: $baseline-theme-color, $generate-keyframes: true, $query: feature-targeting.all() ) { $feat-animation: feature-targeting.create-target($query, animation); $feat-color: feature-targeting.create-target($query, color); // Unchecked colors @if ( $unmarked-stroke-color and not custom-properties.is-custom-prop($unmarked-stroke-color) ) { $unmarked-stroke-color: custom-properties.create( checkbox-custom-properties.$unchecked-color, theme-color.prop-value($unmarked-stroke-color) ); } @include if-unmarked-enabled_ { @include container-colors_( $unmarked-stroke-color, $unmarked-fill-color, $query: $query ); } // Checked colors @if ( $marked-stroke-color and not custom-properties.is-custom-prop($marked-stroke-color) ) { $marked-stroke-color: custom-properties.create( checkbox-custom-properties.$checked-color, custom-properties.create( color-custom-properties.$secondary, theme-color.prop-value($marked-stroke-color) ) ); } @if ( $marked-fill-color and not custom-properties.is-custom-prop($marked-fill-color) ) { $marked-fill-color: custom-properties.create( checkbox-custom-properties.$checked-color, custom-properties.create( color-custom-properties.$secondary, theme-color.prop-value($marked-fill-color) ) ); } @include if-marked-enabled_ { @include container-colors_( $marked-stroke-color, $marked-fill-color, $query: $query ); } @if $generate-keyframes and $unmarked-stroke-color and $marked-stroke-color and $unmarked-fill-color and $marked-fill-color { $uid: theme-color.color-hash($unmarked-stroke-color) + theme-color.color-hash($marked-stroke-color) + theme-color.color-hash($unmarked-fill-color) + theme-color.color-hash($marked-fill-color); $anim-selector: if(&, '&.mdc-checkbox--anim', '.mdc-checkbox--anim'); @include feature-targeting.targets($feat-animation, $feat-color) { @include container-keyframes_( $from-stroke-color: $unmarked-stroke-color, $to-stroke-color: $marked-stroke-color, $from-fill-color: $unmarked-fill-color, $to-fill-color: $marked-fill-color, $uid: #{$uid} ); } #{$anim-selector} { &-unchecked-checked, &-unchecked-indeterminate { .mdc-checkbox__native-control:enabled ~ .mdc-checkbox__background { @include feature-targeting.targets($feat-animation) { animation-name: mdc-checkbox-fade-in-background-#{$uid}; } } } &-checked-unchecked, &-indeterminate-unchecked { .mdc-checkbox__native-control:enabled ~ .mdc-checkbox__background { @include feature-targeting.targets($feat-animation) { animation-name: mdc-checkbox-fade-out-background-#{$uid}; } } } } } } /// /// Sets stroke & fill colors for both marked and unmarked state of disabled checkbox. /// @param {Color} $unmarked-stroke-color - The desired stroke color for the unmarked state /// @param {Color} $unmarked-fill-color - The desired fill color for the unmarked state /// @param {Color} $marked-stroke-color - The desired stroke color for the marked state /// @param {Color} $marked-fill-color - The desired fill color for the marked state /// @mixin disabled-container-colors( $unmarked-stroke-color: $disabled-color, $unmarked-fill-color: transparent, $marked-stroke-color: transparent, $marked-fill-color: $disabled-color, $query: feature-targeting.all() ) { @if ( $unmarked-stroke-color and not custom-properties.is-custom-prop($unmarked-stroke-color) ) { $unmarked-stroke-color: custom-properties.create( checkbox-custom-properties.$disabled-color, theme-color.prop-value($unmarked-stroke-color) ); } @if $unmarked-stroke-color == null { $unmarked-fill-color: null; } @include if-unmarked-disabled_ { @include container-colors_( $unmarked-stroke-color, $unmarked-fill-color, $query: $query ); } @if ( $marked-fill-color and not custom-properties.is-custom-prop($marked-fill-color) ) { $marked-fill-color: custom-properties.create( checkbox-custom-properties.$disabled-color, theme-color.prop-value($marked-fill-color) ); } @if $marked-fill-color and custom-properties.get-fallback($marked-fill-color) == GrayText { // Transparent appears white in HCM $marked-stroke-color: GrayText; } @if $marked-fill-color == null { $marked-stroke-color: null; } @include if-marked-disabled_ { @include container-colors_( $marked-stroke-color, $marked-fill-color, $query: $query ); } } /// /// Sets the ink color of the checked and indeterminate icons for an enabled checkbox /// @param {Color} $color - The desired ink color in enabled state /// @mixin ink-color($color, $query: feature-targeting.all()) { @if ($color and not custom-properties.is-custom-prop($color)) { $color: custom-properties.create( checkbox-custom-properties.$ink-color, $color ); } @include if-enabled_ { @include ink-color_($color, $query: $query); } } /// /// Sets the ink color of the checked and indeterminate icons for a disabled checkbox /// @param {Color} $color - The desired ink color in disabled state /// @mixin disabled-ink-color($color, $query: feature-targeting.all()) { @if ($color and not custom-properties.is-custom-prop($color)) { $color: custom-properties.create( checkbox-custom-properties.$ink-color, $color ); } @include if-disabled_ { @include ink-color_($color, $query: $query); } } /// Sets ripple color when checkbox is not in checked state. @mixin ripple-color( $color, $opacity-map: null, $query: feature-targeting.all() ) { @include ripple-theme.states( $color: $color, $opacity-map: $opacity-map, $query: $query, $ripple-target: $ripple-target ); } /// Sets focus indicator color when checkbox is in checked state. @mixin focus-indicator-color( $color, $opacity-map: null, $query: feature-targeting.all() ) { $feat-color: feature-targeting.create-target($query, color); &.mdc-checkbox--selected { @include ripple-theme.states( $color: $color, $opacity-map: $opacity-map, $query: $query, $ripple-target: $ripple-target ); } &.mdc-ripple-upgraded--background-focused.mdc-checkbox--selected { @include ripple-theme.states-base-color( $color: $color, $query: $query, $ripple-target: $ripple-target ); } } // // Private // /// /// Helps select the checkbox background only when its native control is in /// enabled state. /// @access private /// @mixin if-enabled_ { .mdc-checkbox__native-control:enabled ~ { @content; } } /// /// Helps select the checkbox background only when its native control is in /// disabled state. /// @access private /// @mixin if-disabled_ { .mdc-checkbox__native-control:disabled ~ { @content; } } /// /// Helps select the checkbox background only when its native control is in /// unmarked & enabled state. /// @access private /// @mixin if-unmarked-enabled_ { .mdc-checkbox__native-control:enabled:not(:checked):not(:indeterminate):not([data-indeterminate='true']) ~ { @content; } } /// /// Helps select the checkbox background only when its native control is in /// unmarked & disabled state. /// @access private /// @mixin if-unmarked-disabled_ { // Note: we must use `[disabled]` instead of `:disabled` below because Edge does not always recalculate the style // property when the `:disabled` pseudo-class is followed by a sibling combinator. See: // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/11295231/ .mdc-checkbox__native-control[disabled]:not(:checked):not(:indeterminate):not([data-indeterminate='true']) ~ { @content; } } /// /// Helps select the checkbox background only when its native control is in /// marked & enabled state. /// @access private /// @mixin if-marked-enabled_ { .mdc-checkbox__native-control:enabled:checked, .mdc-checkbox__native-control:enabled:indeterminate, .mdc-checkbox__native-control[data-indeterminate='true']:enabled { ~ { @content; } } } /// /// Helps select the checkbox background only when its native control is in /// marked & disabled state. /// @access private /// @mixin if-marked-disabled_ { // Note: we must use `[disabled]` instead of `:disabled` below because Edge does not always recalculate the style // property when the `:disabled` pseudo-class is followed by a sibling combinator. See: // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/11295231/ .mdc-checkbox__native-control[disabled]:checked, .mdc-checkbox__native-control[disabled]:indeterminate, .mdc-checkbox__native-control[data-indeterminate='true'][disabled] { ~ { @content; } } } /// /// Sets the stroke & fill colors for the checkbox. /// This mixin should be wrapped in a mixin that qualifies state such as /// `mdc-checkbox-if-unmarked-enabled_`. /// @access private /// @mixin container-colors_( $stroke-color, $fill-color, $query: feature-targeting.all() ) { $feat-color: feature-targeting.create-target($query, color); .mdc-checkbox__background { @include feature-targeting.targets($feat-color) { @include theme.property(border-color, $stroke-color); @include theme.property(background-color, $fill-color); } } } /// /// Sets the ink color of the checked and indeterminate icons for a checkbox. /// This mixin should be wrapped in a mixin that qualifies state such as /// `mdc-checkbox-if-unmarked_`. /// @access private /// @mixin ink-color_($color, $query: feature-targeting.all()) { $feat-color: feature-targeting.create-target($query, color); .mdc-checkbox__background { .mdc-checkbox__checkmark { @include feature-targeting.targets($feat-color) { @include theme.property(color, $color); } } .mdc-checkbox__mixedmark { @include feature-targeting.targets($feat-color) { @include theme.property(border-color, $color); } } } } @mixin container-keyframes_( $from-stroke-color, $to-stroke-color, $from-fill-color, $to-fill-color, $uid ) { @keyframes mdc-checkbox-fade-in-background-#{$uid} { 0% { @include theme.property(border-color, $from-stroke-color); @include theme.property(background-color, $from-fill-color); } 50% { @include theme.property(border-color, $to-stroke-color); @include theme.property(background-color, $to-fill-color); } } @keyframes mdc-checkbox-fade-out-background-#{$uid} { 0%, 80% { @include theme.property(border-color, $to-stroke-color); @include theme.property(background-color, $to-fill-color); } 100% { @include theme.property(border-color, $from-stroke-color); @include theme.property(background-color, $from-fill-color); } } }