// // Copyright 2020 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. // @use 'sass:list'; @use 'sass:map'; @use 'sass:meta'; @use 'sass:selector'; @use 'sass:string'; @use './custom-properties'; @use './selector-ext'; /// List of all valid states. When adding new state functions, add the name of /// the state to this List. $_valid-states: ( enabled, disabled, dragged, error, focus, hover, opened, pressed, selected, unselected ); /// Retrieves the default state from the provided parameter. The parameter may /// be the state's default value or a state Map. A state Map has individual keys /// describing each state's value. /// /// @example /// get-default-state(blue); // blue /// get-default-state((default: blue)); // blue /// get-default-state((hover: red)); // null /// /// @param {*} $default-or-map - The state's default value or a state Map. /// @return The default state if present, or null. @function get-default-state($default-or-map) { $state: _get-state($default-or-map, default); @if $state == null and not _is-state-map($default-or-map) { @return $default-or-map; } @return $state; } /// Retrieves the enabled state from the provided parameter. The parameter may /// be the state's default value or a state Map. A state Map has individual keys /// describing each state's value. /// /// @example /// get-enabled-state(blue); // blue /// get-enabled-state((enabled: blue)); // blue /// get-enabled-state((hover: red)); // null /// /// @param {*} $default-or-map - The state's default value or a state Map. /// @return The enabled state if present, or null. @function get-enabled-state($default-or-map) { @return _get-state($default-or-map, enabled); } /// Retrieves the disabled state from the provided parameter. The parameter may /// be the state's default value or a state Map. A state Map has individual keys /// describing each state's value. /// /// @example /// get-disabled-state(blue); // null /// get-disabled-state((disabled: red)); // red /// get-disabled-state((default: blue)); // null /// /// @param {*} $default-or-map - The state's default value or a state Map. /// @return The disabled state if present, or null. @function get-disabled-state($default-or-map) { @return _get-state($default-or-map, disabled); } /// Retrieves the dragged state from the provided parameter. The parameter may /// be the state's default value or a state Map. A state Map has individual keys /// describing each state's value. /// /// @example /// get-dragged-state(blue); // null /// get-dragged-state((dragged: red)); // red /// get-dragged-state((default: blue)); // null /// /// @param {*} $default-or-map - The state's default value or a state Map. /// @return The dragged state if present, or null. @function get-dragged-state($default-or-map) { @return _get-state($default-or-map, dragged); } /// Retrieves the error state from the provided parameter. The parameter may /// be the state's default value or a state Map. A state Map has individual keys /// describing each state's value. /// /// @example /// get-error-state(blue); // null /// get-error-state((error: red)); // red /// get-error-state((default: blue)); // null /// /// @param {*} $default-or-map - The state's default value or a state Map. /// @return The error state if present, or null. @function get-error-state($default-or-map) { @return _get-state($default-or-map, error); } /// Retrieves the focus state from the provided parameter. The parameter may /// be the state's default value or a state Map. A state Map has individual keys /// describing each state's value. /// /// @example /// get-focus-state(blue); // null /// get-focus-state((focus: red)); // red /// get-focus-state((default: blue)); // null /// /// @param {*} $default-or-map - The state's default value or a state Map. /// @return The focus state if present, or null. @function get-focus-state($default-or-map) { @return _get-state($default-or-map, focus); } /// Retrieves the hover state from the provided parameter. The parameter may /// be the state's default value or a state Map. A state Map has individual keys /// describing each state's value. /// /// @example /// get-hover-state(blue); // null /// get-hover-state((hover: red)); // red /// get-hover-state((default: blue)); // null /// /// @param {*} $default-or-map - The state's default value or a state Map. /// @return The hover state if present, or null. @function get-hover-state($default-or-map) { @return _get-state($default-or-map, hover); } /// Retrieves the opened state from the provided parameter. The parameter may /// be the state's default value or a state Map. A state Map has individual keys /// describing each state's value. /// /// @example /// get-opened-state(blue); // null /// get-opened-state((opened: red)); // red /// get-opened-state((default: blue)); // null /// /// @param {*} $default-or-map - The state's default value or a state Map. /// @return The opened state if present, or null. @function get-opened-state($default-or-map) { @return _get-state($default-or-map, opened); } /// Retrieves the pressed state from the provided parameter. The parameter may /// be the state's default value or a state Map. A state Map has individual keys /// describing each state's value. /// /// @example /// get-pressed-state(blue); // null /// get-pressed-state((pressed: red)); // red /// get-pressed-state((default: blue)); // null /// /// @param {*} $default-or-map - The state's default value or a state Map. /// @return The pressed state if present, or null. @function get-pressed-state($default-or-map) { @return _get-state($default-or-map, pressed); } /// Retrieves the selected state from the provided parameter. The parameter may /// be the state's default value or a state Map. A state Map has individual keys /// describing each state's value. /// /// @example /// get-selected-state(blue); // null /// get-selected-state((selected: red)); // red /// get-selected-state((default: blue)); // null /// /// @param {*} $default-or-map - The state's default value or a state Map. /// @return The selected state if present, or null. @function get-selected-state($default-or-map) { @return _get-state($default-or-map, selected); } /// Retrieves the unselected state from the provided parameter. The parameter /// may be the state's default value or a state Map. A state Map has individual /// key describing each state's value. /// /// @example /// get-unselected-state(blue); // null /// get-unselected-state((unselected: red)); // red /// get-unselected-state((default: blue)); // null /// /// @param {*} $default-or-map - The state's default value or a state Map. /// @return The unselected state if present, or null. @function get-unselected-state($default-or-map) { @return _get-state($default-or-map, unselected); } @function _get-state($default-or-map, $state) { @if _is-state-map($default-or-map) { @return map.get($default-or-map, $state); } @else { @return null; } } @function _is-state-map($default-or-map) { @return meta.type-of($default-or-map) == 'map' and not custom-properties.is-custom-prop($default-or-map); } /// Appends the default state selector to the current parent. /// /// @example - scss /// .mdc-foo { /// @include default($selectors) { /// color: teal; /// } /// } /// /// @example - css /// .mdc-foo:enabled { /// color: teal; /// } /// /// @param {Map} $selectors A Map whose keys are states and values are string /// selectors. @mixin default($selectors) { @include enabled($selectors) { @content; } } /// Appends the enabled state selector to the current parent. /// /// @example - scss /// .mdc-foo { /// @include enabled($selectors) { /// color: teal; /// } /// } /// /// @example - css /// .mdc-foo:enabled { /// color: teal; /// } /// /// @param {Map} $selectors A Map whose keys are states and values are string /// selectors. @mixin enabled($selectors) { @include _selector($selectors, enabled) { @content; } } /// Appends the disabled state selector to the current parent. /// /// @example - scss /// .mdc-foo { /// @include disabled($selectors) { /// color: teal; /// } /// } /// /// @example - css /// .mdc-foo:disabled { /// color: teal; /// } /// /// @param {Map} $selectors A Map whose keys are states and values are string /// selectors. @mixin disabled($selectors) { @include _selector($selectors, disabled) { @content; } } /// Appends the dragged state selector to the current parent. /// /// @example - scss /// .mdc-foo { /// @include dragged($selectors) { /// color: teal; /// } /// } /// /// @example - css /// .mdc-foo:enabled.mdc-foo--dragged { /// color: teal; /// } /// /// @param {Map} $selectors A Map whose keys are states and values are string /// selectors. @mixin dragged($selectors) { @include enabled($selectors) { @include _selector($selectors, dragged) { @content; } } } /// Appends the error state selector to the current parent. /// /// @example - scss /// .mdc-foo { /// @include error($selectors) { /// color: teal; /// } /// } /// /// @example - css /// .mdc-foo:invalid { /// color: teal; /// } /// /// @param {Map} $selectors A Map whose keys are states and values are string /// selectors. @mixin error($selectors) { @include _selector($selectors, error) { @content; } } /// Appends the focus state selector to the current parent. /// /// @example - scss /// .mdc-foo { /// @include focus($selectors) { /// color: teal; /// } /// } /// /// @example - css /// .mdc-foo:enabled:focus:not(:active) { /// color: teal; /// } /// /// @param {Map} $selectors A Map whose keys are states and values are string /// selectors. @mixin focus($selectors) { @include enabled($selectors) { @include _selector($selectors, focus) { @content; } } } /// Appends the hover state selector to the current parent. /// /// @example - scss /// .mdc-foo { /// @include hover($selectors) { /// color: teal; /// } /// } /// /// @example - css /// .mdc-foo:enabled:hover:not(:focus):not(:active) { /// color: teal; /// } /// /// @param {Map} $selectors A Map whose keys are states and values are string /// selectors. @mixin hover($selectors) { @include enabled($selectors) { @include _selector($selectors, hover) { @content; } } } /// Appends the opened state selector to the current parent. /// /// @example - scss /// .mdc-foo { /// @include opened($selectors) { /// color: teal; /// } /// } /// /// @example - css /// .mdc-foo.mdc-foo--opened { /// color: teal; /// } /// /// @param {Map} $selectors A Map whose keys are states and values are string /// selectors. @mixin opened($selectors) { @include _selector($selectors, opened) { @content; } } /// Appends the pressed state selector to the current parent. /// /// @example - scss /// .mdc-foo { /// @include pressed($selectors) { /// color: teal; /// } /// } /// /// @example - css /// .mdc-foo:enabled:active { /// color: teal; /// } /// /// @param {Map} $selectors A Map whose keys are states and values are string /// selectors. @mixin pressed($selectors) { @include enabled($selectors) { @include _selector($selectors, pressed) { @content; } } } /// Appends the selected state selector to the current parent. /// /// @example - scss /// .mdc-foo { /// @include selected($selectors) { /// color: teal; /// } /// } /// /// @example - css /// .mdc-foo.mdc-foo--selected { /// color: teal; /// } /// /// @param {Map} $selectors A Map whose keys are states and values are string /// selectors. @mixin selected($selectors) { @include _selector($selectors, selected) { @content; } } /// Appends the unselected state selector to the current parent. /// /// @example - scss /// .mdc-foo { /// @include unselected($selectors) { /// color: teal; /// } /// } /// /// @example - css /// .mdc-foo.mdc-foo--unselected { /// color: teal; /// } /// /// @param {Map} $selectors A Map whose keys are states and values are string /// selectors. @mixin unselected($selectors) { @include _selector($selectors, unselected) { @content; } } /// Creates and returns a Map of independent selectors from a Map of simple /// selectors. /// /// This function ensures that each selector is independent given all possible /// states provided. An "independent" selector does not rely on CSS override /// order or specificity. /// /// @example - scss /// $selectors: state.create-selectors( /// ( /// disabled: ':disabled', /// hover: ':hover', /// focus: ':focus', /// pressed: ':active', /// ) /// ); /// // ( /// // enabled: ':enabled', /// // disabled: ':disabled', /// // hover: ':hover:not(:focus):not(:active)', /// // focus: ':focus:not(:active)', /// // pressed: ':active', /// // ) /// /// @see {function} _create-independent-selector /// /// @param {Map} $selectors A Map whose keys are states and values are string /// selectors. /// @return {Map} A Map of state selectors. @function _create-selectors($selectors) { $new-selectors: (); @each $state, $selector in $selectors { @if not list.index($_valid-states, $state) { @error 'Unsupported state #{$state}, must be one of #{$_valid-states}.'; } // Check if there are any dependent states for this state that we need to // add to the selector with :not() $dependent-states: (); @each $group in $_dependent-state-groups { $index: list.index($group, $state); @if $index and $index < list.length($group) { // State is part of this group. Add any remaining selectors as // dependents, only if they haven't already been added (the state may be // part of multiple groups with shared state dependents, like // :hover:focus:active and :link:visited:hover:active) @for $i from $index + 1 through list.length($group) { $dependent: list.nth($group, $i); @if not list.index($dependent-states, $dependent) { $dependent-states: list.append($dependent-states, $dependent); } } } } $dependents: (); @each $dependent-state in $dependent-states { $dependent: map.get($selectors, $dependent-state); @if $dependent and not list.index($_independent-states, $dependent-state) { $dependents: list.append($dependents, $dependent); } } // Make the selector independent (if any dependents were found) $selector: _create-independent-selector($selector, $dependents...); $new-selectors: map.set($new-selectors, $state, $selector); } $new-selectors: _add-default-enabled-selector($new-selectors); @return $new-selectors; } /// Adds a default selector for the "enabled" state if one does not exist and if /// it is possible to infer one from the provided Map of selectors. /// /// @example - scss /// _add-default-enabled-selector((disabled: ':disabled')); /// // ( /// // disabled: ':disabled', /// // enabled: ':enabled', /// // ) /// /// _add-default-enabled-selector((disabled: '.mdc-foo--disabled')); /// // ( /// // disabled: '.mdc-foo--disabled', /// // enabled: ':not(.mdc-foo--disabled)', /// // ) /// /// @param {Map} $selectors - A Map of state selectors. /// @return {Map} The same Map of selectors, potentially with an additional /// "enabled" key with the enabled selector value. @function _add-default-enabled-selector($selectors) { $enabled: map.get($selectors, enabled); $disabled: map.get($selectors, disabled); @if $disabled == ':disabled' { @if $enabled and $enabled != ':enabled' { // TODO: Clean up instances of :not(:disabled) // Enabled selector was provided, but it was not :enabled. These // can be cleaned up, but don't change them right now. @warn 'Use :enabled instead of #{$enabled} when using :disabled.'; @return $selectors; } // For :disabled, use :enabled instead of the :not() variant @return map.set($selectors, enabled, ':enabled'); } @if $disabled and not $enabled { @return map.set($selectors, enabled, selector-ext.negate($disabled)); } @return $selectors; } /// A Map of override selectors. This can be used to temporarily change and /// configure state selectors. /// @type {Map} /// @see {mixin} override-selectors $_override-selectors: (); /// Override the current selectors provided to a state mixin for the provided /// content. /// /// @example - scss /// // Change theme so that focus styles only show during keyboard navigation /// @include state.override-selectors((focus: ':focus-within')) { /// @include foo.theme($theme); /// } /// /// @param {Map} $selectors A Map whose keys are states and values are string /// selectors. /// @content The styles to override state selectors for. @mixin override-selectors($selectors) { $reset: $_override-selectors; $_override-selectors: $selectors !global; @content; $_override-selectors: $reset !global; } $_independent-states: (); /// Indicates that for the given content of state mixins, the provided states /// are on their own independent elements and that they should ignore typical /// dependent groupings, such as `:hover`, `:focus`, and `:active`. /// /// This mixin is useful when multiple states within a typical dependency group /// need to be visible at the same time (such as `:focus` and `:active`). To /// achieve this, the states must be on their own independent elements (such as /// separate `::before` and `::after` pseudo elements). /// /// @example - scss /// .broken-ripple { /// @include state.hover { /// &::before { opacity: 0.1; } /// } /// @include state.focus { /// &::before { opacity: 0.2; } /// } /// @include state.pressed { /// &::after { opacity: 0.3; } /// } /// } /// /// .fixed-ripple { /// @include state.independent-elements(pressed) { /// @include state.hover { /// &::before { opacity: 0.1; } /// } /// @include state.focus { /// &::before { opacity: 0.2; } /// } /// @include state.pressed { /// &::before { opacity: 0.3; } /// } /// } /// } /// /// @example - css /// .broken-ripple:hover:not(:focus):not(:active)::before { /// opacity: 0.1; /// } /// .broken-ripple:focus:not(:active)::before { /// /* Focus styles will not be visible due to :not(:active)!! */ /// opacity: 0.2; /// } /// .broken-ripple:active::after { /// opacity: 0.3; /// } /// /// .fixed-ripple:hover:not(:focus)::before { /// opacity: 0.1; /// } /// .fixed-ripple:focus::before { /// /* Both focus and pressed styles are visible during press. Only hover /// and focus need to be independent of each other since they share an /// element. */ /// opacity: 0.2; /// } /// .fixed-ripple:active::after { /// opacity: 0.3; /// } /// /// @param {String...} $states - One or more states that should be considered /// independent and on its own element. /// @content Two or more state mixins that are part of a dependency group /// involving the provided independent states. @mixin independent-elements($states...) { $reset: $_independent-states; $_independent-states: $states !global; @content; $_independent-states: $reset !global; } /// A List of state groups that are dependent on each other for CSS override /// order. These are used to determine which state selectors are needed for /// `_create-independent-selector()`. // Note: Sass syntax does not allow declaring nested Lists; an empty second List // placeholder is added for the correct data structure. $_dependent-state-groups: ((hover, focus, pressed), ()); /// Creates a selector that will be independent based on the other selectors /// that are dependents of it. /// /// Selector dependencies are selector groups that must follow a certain order /// for CSS overrides. For example: `:hover`, `:focus`, `:active` or `:link`, /// `:visited`, `:hover`, `:active`. /// /// Selectors at the start of a group are dependencies of selectors at the end /// of a group. /// /// @example - scss /// #{_create-independent-selector(':hover', ':focus', ':active')} { /// color: teal; /// } /// /// #{_create-independent-selector(':focus', ':active')} { /// color: magenta; /// } /// /// @example - css /// :hover:not(:focus):not(:active) { /// color: teal; /// } /// /// :focus:not(:active) { /// color: magenta; /// } /// /// The returned selector is considered "independent" and does not rely on CSS /// override order or specificity within its group. In other words, "hover" /// styles can be customized after "focus" styles without hiding default focus /// styles. /// /// @example - css /// /* Default focus styles */ /// :focus:not(:active) { color: magenta; } /// /// /* New hover styles, does not prevent focus styles from being visible */ /// :hover:not(:focus):not(:active) { color: orange; } /// /// @param {String} $selector - The main selector to target. /// @param {String...} $dependents - Additional group dependents of the main /// selector. They will be added as `:not()` selectors. /// @return {List} A new independent selector in selector value format. @function _create-independent-selector($selector, $dependents...) { @each $dependent in $dependents { @if $dependent { $selector: selector-ext.append-strict( $selector, selector-ext.negate($dependent) ); } } @return $selector; } @mixin _selector($selectors, $state) { $selectors: _create-selectors(map.merge($selectors, $_override-selectors)); @if not map.has-key($selectors, $state) { @error 'Missing #{$state} from #{$selectors}'; } @at-root { #{selector-ext.append-strict(&, map.get($selectors, $state))} { @content; } } }