| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769 |
- //
- // 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;
- }
- }
- }
|