UNPKG

23.3 kBSCSSView Raw
1//
2// Copyright 2020 Google Inc.
3//
4// Permission is hereby granted, free of charge, to any person obtaining a copy
5// of this software and associated documentation files (the "Software"), to deal
6// in the Software without restriction, including without limitation the rights
7// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8// copies of the Software, and to permit persons to whom the Software is
9// furnished to do so, subject to the following conditions:
10//
11// The above copyright notice and this permission notice shall be included in
12// all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20// THE SOFTWARE.
21//
22
23@use 'sass:list';
24@use 'sass:map';
25@use 'sass:meta';
26@use 'sass:selector';
27@use 'sass:string';
28@use './custom-properties';
29@use './selector-ext';
30
31/// List of all valid states. When adding new state functions, add the name of
32/// the state to this List.
33$_valid-states: (
34 enabled,
35 disabled,
36 dragged,
37 error,
38 focus,
39 hover,
40 opened,
41 pressed,
42 selected,
43 unselected
44);
45
46/// Retrieves the default state from the provided parameter. The parameter may
47/// be the state's default value or a state Map. A state Map has individual keys
48/// describing each state's value.
49///
50/// @example
51/// get-default-state(blue); // blue
52/// get-default-state((default: blue)); // blue
53/// get-default-state((hover: red)); // null
54///
55/// @param {*} $default-or-map - The state's default value or a state Map.
56/// @return The default state if present, or null.
57@function get-default-state($default-or-map) {
58 $state: _get-state($default-or-map, default);
59 @if $state == null and not _is-state-map($default-or-map) {
60 @return $default-or-map;
61 }
62
63 @return $state;
64}
65
66/// Retrieves the enabled state from the provided parameter. The parameter may
67/// be the state's default value or a state Map. A state Map has individual keys
68/// describing each state's value.
69///
70/// @example
71/// get-enabled-state(blue); // blue
72/// get-enabled-state((enabled: blue)); // blue
73/// get-enabled-state((hover: red)); // null
74///
75/// @param {*} $default-or-map - The state's default value or a state Map.
76/// @return The enabled state if present, or null.
77@function get-enabled-state($default-or-map) {
78 @return _get-state($default-or-map, enabled);
79}
80
81/// Retrieves the disabled state from the provided parameter. The parameter may
82/// be the state's default value or a state Map. A state Map has individual keys
83/// describing each state's value.
84///
85/// @example
86/// get-disabled-state(blue); // null
87/// get-disabled-state((disabled: red)); // red
88/// get-disabled-state((default: blue)); // null
89///
90/// @param {*} $default-or-map - The state's default value or a state Map.
91/// @return The disabled state if present, or null.
92@function get-disabled-state($default-or-map) {
93 @return _get-state($default-or-map, disabled);
94}
95
96/// Retrieves the dragged state from the provided parameter. The parameter may
97/// be the state's default value or a state Map. A state Map has individual keys
98/// describing each state's value.
99///
100/// @example
101/// get-dragged-state(blue); // null
102/// get-dragged-state((dragged: red)); // red
103/// get-dragged-state((default: blue)); // null
104///
105/// @param {*} $default-or-map - The state's default value or a state Map.
106/// @return The dragged state if present, or null.
107@function get-dragged-state($default-or-map) {
108 @return _get-state($default-or-map, dragged);
109}
110
111/// Retrieves the error state from the provided parameter. The parameter may
112/// be the state's default value or a state Map. A state Map has individual keys
113/// describing each state's value.
114///
115/// @example
116/// get-error-state(blue); // null
117/// get-error-state((error: red)); // red
118/// get-error-state((default: blue)); // null
119///
120/// @param {*} $default-or-map - The state's default value or a state Map.
121/// @return The error state if present, or null.
122@function get-error-state($default-or-map) {
123 @return _get-state($default-or-map, error);
124}
125
126/// Retrieves the focus state from the provided parameter. The parameter may
127/// be the state's default value or a state Map. A state Map has individual keys
128/// describing each state's value.
129///
130/// @example
131/// get-focus-state(blue); // null
132/// get-focus-state((focus: red)); // red
133/// get-focus-state((default: blue)); // null
134///
135/// @param {*} $default-or-map - The state's default value or a state Map.
136/// @return The focus state if present, or null.
137@function get-focus-state($default-or-map) {
138 @return _get-state($default-or-map, focus);
139}
140
141/// Retrieves the hover state from the provided parameter. The parameter may
142/// be the state's default value or a state Map. A state Map has individual keys
143/// describing each state's value.
144///
145/// @example
146/// get-hover-state(blue); // null
147/// get-hover-state((hover: red)); // red
148/// get-hover-state((default: blue)); // null
149///
150/// @param {*} $default-or-map - The state's default value or a state Map.
151/// @return The hover state if present, or null.
152@function get-hover-state($default-or-map) {
153 @return _get-state($default-or-map, hover);
154}
155
156/// Retrieves the opened state from the provided parameter. The parameter may
157/// be the state's default value or a state Map. A state Map has individual keys
158/// describing each state's value.
159///
160/// @example
161/// get-opened-state(blue); // null
162/// get-opened-state((opened: red)); // red
163/// get-opened-state((default: blue)); // null
164///
165/// @param {*} $default-or-map - The state's default value or a state Map.
166/// @return The opened state if present, or null.
167@function get-opened-state($default-or-map) {
168 @return _get-state($default-or-map, opened);
169}
170
171/// Retrieves the pressed state from the provided parameter. The parameter may
172/// be the state's default value or a state Map. A state Map has individual keys
173/// describing each state's value.
174///
175/// @example
176/// get-pressed-state(blue); // null
177/// get-pressed-state((pressed: red)); // red
178/// get-pressed-state((default: blue)); // null
179///
180/// @param {*} $default-or-map - The state's default value or a state Map.
181/// @return The pressed state if present, or null.
182@function get-pressed-state($default-or-map) {
183 @return _get-state($default-or-map, pressed);
184}
185
186/// Retrieves the selected state from the provided parameter. The parameter may
187/// be the state's default value or a state Map. A state Map has individual keys
188/// describing each state's value.
189///
190/// @example
191/// get-selected-state(blue); // null
192/// get-selected-state((selected: red)); // red
193/// get-selected-state((default: blue)); // null
194///
195/// @param {*} $default-or-map - The state's default value or a state Map.
196/// @return The selected state if present, or null.
197@function get-selected-state($default-or-map) {
198 @return _get-state($default-or-map, selected);
199}
200
201/// Retrieves the unselected state from the provided parameter. The parameter
202/// may be the state's default value or a state Map. A state Map has individual
203/// key describing each state's value.
204///
205/// @example
206/// get-unselected-state(blue); // null
207/// get-unselected-state((unselected: red)); // red
208/// get-unselected-state((default: blue)); // null
209///
210/// @param {*} $default-or-map - The state's default value or a state Map.
211/// @return The unselected state if present, or null.
212@function get-unselected-state($default-or-map) {
213 @return _get-state($default-or-map, unselected);
214}
215
216@function _get-state($default-or-map, $state) {
217 @if _is-state-map($default-or-map) {
218 @return map.get($default-or-map, $state);
219 } @else {
220 @return null;
221 }
222}
223
224@function _is-state-map($default-or-map) {
225 @return meta.type-of($default-or-map) == 'map' and not
226 custom-properties.is-custom-prop($default-or-map);
227}
228
229/// Appends the default state selector to the current parent.
230///
231/// @example - scss
232/// .mdc-foo {
233/// @include default($selectors) {
234/// color: teal;
235/// }
236/// }
237///
238/// @example - css
239/// .mdc-foo:enabled {
240/// color: teal;
241/// }
242///
243/// @param {Map} $selectors A Map whose keys are states and values are string
244/// selectors.
245@mixin default($selectors) {
246 @include enabled($selectors) {
247 @content;
248 }
249}
250
251/// Appends the enabled state selector to the current parent.
252///
253/// @example - scss
254/// .mdc-foo {
255/// @include enabled($selectors) {
256/// color: teal;
257/// }
258/// }
259///
260/// @example - css
261/// .mdc-foo:enabled {
262/// color: teal;
263/// }
264///
265/// @param {Map} $selectors A Map whose keys are states and values are string
266/// selectors.
267@mixin enabled($selectors) {
268 @include _selector($selectors, enabled) {
269 @content;
270 }
271}
272
273/// Appends the disabled state selector to the current parent.
274///
275/// @example - scss
276/// .mdc-foo {
277/// @include disabled($selectors) {
278/// color: teal;
279/// }
280/// }
281///
282/// @example - css
283/// .mdc-foo:disabled {
284/// color: teal;
285/// }
286///
287/// @param {Map} $selectors A Map whose keys are states and values are string
288/// selectors.
289@mixin disabled($selectors) {
290 @include _selector($selectors, disabled) {
291 @content;
292 }
293}
294
295/// Appends the dragged state selector to the current parent.
296///
297/// @example - scss
298/// .mdc-foo {
299/// @include dragged($selectors) {
300/// color: teal;
301/// }
302/// }
303///
304/// @example - css
305/// .mdc-foo:enabled.mdc-foo--dragged {
306/// color: teal;
307/// }
308///
309/// @param {Map} $selectors A Map whose keys are states and values are string
310/// selectors.
311@mixin dragged($selectors) {
312 @include enabled($selectors) {
313 @include _selector($selectors, dragged) {
314 @content;
315 }
316 }
317}
318
319/// Appends the error state selector to the current parent.
320///
321/// @example - scss
322/// .mdc-foo {
323/// @include error($selectors) {
324/// color: teal;
325/// }
326/// }
327///
328/// @example - css
329/// .mdc-foo:invalid {
330/// color: teal;
331/// }
332///
333/// @param {Map} $selectors A Map whose keys are states and values are string
334/// selectors.
335@mixin error($selectors) {
336 @include _selector($selectors, error) {
337 @content;
338 }
339}
340
341/// Appends the focus state selector to the current parent.
342///
343/// @example - scss
344/// .mdc-foo {
345/// @include focus($selectors) {
346/// color: teal;
347/// }
348/// }
349///
350/// @example - css
351/// .mdc-foo:enabled:focus:not(:active) {
352/// color: teal;
353/// }
354///
355/// @param {Map} $selectors A Map whose keys are states and values are string
356/// selectors.
357@mixin focus($selectors) {
358 @include enabled($selectors) {
359 @include _selector($selectors, focus) {
360 @content;
361 }
362 }
363}
364
365/// Appends the hover state selector to the current parent.
366///
367/// @example - scss
368/// .mdc-foo {
369/// @include hover($selectors) {
370/// color: teal;
371/// }
372/// }
373///
374/// @example - css
375/// .mdc-foo:enabled:hover:not(:focus):not(:active) {
376/// color: teal;
377/// }
378///
379/// @param {Map} $selectors A Map whose keys are states and values are string
380/// selectors.
381@mixin hover($selectors) {
382 @include enabled($selectors) {
383 @include _selector($selectors, hover) {
384 @content;
385 }
386 }
387}
388
389/// Appends the opened state selector to the current parent.
390///
391/// @example - scss
392/// .mdc-foo {
393/// @include opened($selectors) {
394/// color: teal;
395/// }
396/// }
397///
398/// @example - css
399/// .mdc-foo.mdc-foo--opened {
400/// color: teal;
401/// }
402///
403/// @param {Map} $selectors A Map whose keys are states and values are string
404/// selectors.
405@mixin opened($selectors) {
406 @include _selector($selectors, opened) {
407 @content;
408 }
409}
410
411/// Appends the pressed state selector to the current parent.
412///
413/// @example - scss
414/// .mdc-foo {
415/// @include pressed($selectors) {
416/// color: teal;
417/// }
418/// }
419///
420/// @example - css
421/// .mdc-foo:enabled:active {
422/// color: teal;
423/// }
424///
425/// @param {Map} $selectors A Map whose keys are states and values are string
426/// selectors.
427@mixin pressed($selectors) {
428 @include enabled($selectors) {
429 @include _selector($selectors, pressed) {
430 @content;
431 }
432 }
433}
434
435/// Appends the selected state selector to the current parent.
436///
437/// @example - scss
438/// .mdc-foo {
439/// @include selected($selectors) {
440/// color: teal;
441/// }
442/// }
443///
444/// @example - css
445/// .mdc-foo.mdc-foo--selected {
446/// color: teal;
447/// }
448///
449/// @param {Map} $selectors A Map whose keys are states and values are string
450/// selectors.
451@mixin selected($selectors) {
452 @include _selector($selectors, selected) {
453 @content;
454 }
455}
456
457/// Appends the unselected state selector to the current parent.
458///
459/// @example - scss
460/// .mdc-foo {
461/// @include unselected($selectors) {
462/// color: teal;
463/// }
464/// }
465///
466/// @example - css
467/// .mdc-foo.mdc-foo--unselected {
468/// color: teal;
469/// }
470///
471/// @param {Map} $selectors A Map whose keys are states and values are string
472/// selectors.
473@mixin unselected($selectors) {
474 @include _selector($selectors, unselected) {
475 @content;
476 }
477}
478
479/// Creates and returns a Map of independent selectors from a Map of simple
480/// selectors.
481///
482/// This function ensures that each selector is independent given all possible
483/// states provided. An "independent" selector does not rely on CSS override
484/// order or specificity.
485///
486/// @example - scss
487/// $selectors: state.create-selectors(
488/// (
489/// disabled: ':disabled',
490/// hover: ':hover',
491/// focus: ':focus',
492/// pressed: ':active',
493/// )
494/// );
495/// // (
496/// // enabled: ':enabled',
497/// // disabled: ':disabled',
498/// // hover: ':hover:not(:focus):not(:active)',
499/// // focus: ':focus:not(:active)',
500/// // pressed: ':active',
501/// // )
502///
503/// @see {function} _create-independent-selector
504///
505/// @param {Map} $selectors A Map whose keys are states and values are string
506/// selectors.
507/// @return {Map} A Map of state selectors.
508@function _create-selectors($selectors) {
509 $new-selectors: ();
510 @each $state, $selector in $selectors {
511 @if not list.index($_valid-states, $state) {
512 @error 'Unsupported state #{$state}, must be one of #{$_valid-states}.';
513 }
514
515 // Check if there are any dependent states for this state that we need to
516 // add to the selector with :not()
517 $dependent-states: ();
518 @each $group in $_dependent-state-groups {
519 $index: list.index($group, $state);
520 @if $index and $index < list.length($group) {
521 // State is part of this group. Add any remaining selectors as
522 // dependents, only if they haven't already been added (the state may be
523 // part of multiple groups with shared state dependents, like
524 // :hover:focus:active and :link:visited:hover:active)
525 @for $i from $index + 1 through list.length($group) {
526 $dependent: list.nth($group, $i);
527 @if not list.index($dependent-states, $dependent) {
528 $dependent-states: list.append($dependent-states, $dependent);
529 }
530 }
531 }
532 }
533
534 $dependents: ();
535 @each $dependent-state in $dependent-states {
536 $dependent: map.get($selectors, $dependent-state);
537 @if $dependent and not list.index($_independent-states, $dependent-state)
538 {
539 $dependents: list.append($dependents, $dependent);
540 }
541 }
542
543 // Make the selector independent (if any dependents were found)
544 $selector: _create-independent-selector($selector, $dependents...);
545 $new-selectors: map.set($new-selectors, $state, $selector);
546 }
547
548 $new-selectors: _add-default-enabled-selector($new-selectors);
549
550 @return $new-selectors;
551}
552
553/// Adds a default selector for the "enabled" state if one does not exist and if
554/// it is possible to infer one from the provided Map of selectors.
555///
556/// @example - scss
557/// _add-default-enabled-selector((disabled: ':disabled'));
558/// // (
559/// // disabled: ':disabled',
560/// // enabled: ':enabled',
561/// // )
562///
563/// _add-default-enabled-selector((disabled: '.mdc-foo--disabled'));
564/// // (
565/// // disabled: '.mdc-foo--disabled',
566/// // enabled: ':not(.mdc-foo--disabled)',
567/// // )
568///
569/// @param {Map} $selectors - A Map of state selectors.
570/// @return {Map} The same Map of selectors, potentially with an additional
571/// "enabled" key with the enabled selector value.
572@function _add-default-enabled-selector($selectors) {
573 $enabled: map.get($selectors, enabled);
574 $disabled: map.get($selectors, disabled);
575 @if $disabled == ':disabled' {
576 @if $enabled and $enabled != ':enabled' {
577 // TODO: Clean up instances of :not(:disabled)
578 // Enabled selector was provided, but it was not :enabled. These
579 // can be cleaned up, but don't change them right now.
580 @warn 'Use :enabled instead of #{$enabled} when using :disabled.';
581 @return $selectors;
582 }
583
584 // For :disabled, use :enabled instead of the :not() variant
585 @return map.set($selectors, enabled, ':enabled');
586 }
587
588 @if $disabled and not $enabled {
589 @return map.set($selectors, enabled, selector-ext.negate($disabled));
590 }
591
592 @return $selectors;
593}
594
595/// A Map of override selectors. This can be used to temporarily change and
596/// configure state selectors.
597/// @type {Map}
598/// @see {mixin} override-selectors
599$_override-selectors: ();
600
601/// Override the current selectors provided to a state mixin for the provided
602/// content.
603///
604/// @example - scss
605/// // Change theme so that focus styles only show during keyboard navigation
606/// @include state.override-selectors((focus: ':focus-within')) {
607/// @include foo.theme($theme);
608/// }
609///
610/// @param {Map} $selectors A Map whose keys are states and values are string
611/// selectors.
612/// @content The styles to override state selectors for.
613@mixin override-selectors($selectors) {
614 $reset: $_override-selectors;
615 $_override-selectors: $selectors !global;
616 @content;
617 $_override-selectors: $reset !global;
618}
619
620$_independent-states: ();
621
622/// Indicates that for the given content of state mixins, the provided states
623/// are on their own independent elements and that they should ignore typical
624/// dependent groupings, such as `:hover`, `:focus`, and `:active`.
625///
626/// This mixin is useful when multiple states within a typical dependency group
627/// need to be visible at the same time (such as `:focus` and `:active`). To
628/// achieve this, the states must be on their own independent elements (such as
629/// separate `::before` and `::after` pseudo elements).
630///
631/// @example - scss
632/// .broken-ripple {
633/// @include state.hover {
634/// &::before { opacity: 0.1; }
635/// }
636/// @include state.focus {
637/// &::before { opacity: 0.2; }
638/// }
639/// @include state.pressed {
640/// &::after { opacity: 0.3; }
641/// }
642/// }
643///
644/// .fixed-ripple {
645/// @include state.independent-elements(pressed) {
646/// @include state.hover {
647/// &::before { opacity: 0.1; }
648/// }
649/// @include state.focus {
650/// &::before { opacity: 0.2; }
651/// }
652/// @include state.pressed {
653/// &::before { opacity: 0.3; }
654/// }
655/// }
656/// }
657///
658/// @example - css
659/// .broken-ripple:hover:not(:focus):not(:active)::before {
660/// opacity: 0.1;
661/// }
662/// .broken-ripple:focus:not(:active)::before {
663/// /* Focus styles will not be visible due to :not(:active)!! */
664/// opacity: 0.2;
665/// }
666/// .broken-ripple:active::after {
667/// opacity: 0.3;
668/// }
669///
670/// .fixed-ripple:hover:not(:focus)::before {
671/// opacity: 0.1;
672/// }
673/// .fixed-ripple:focus::before {
674/// /* Both focus and pressed styles are visible during press. Only hover
675/// and focus need to be independent of each other since they share an
676/// element. */
677/// opacity: 0.2;
678/// }
679/// .fixed-ripple:active::after {
680/// opacity: 0.3;
681/// }
682///
683/// @param {String...} $states - One or more states that should be considered
684/// independent and on its own element.
685/// @content Two or more state mixins that are part of a dependency group
686/// involving the provided independent states.
687@mixin independent-elements($states...) {
688 $reset: $_independent-states;
689 $_independent-states: $states !global;
690 @content;
691 $_independent-states: $reset !global;
692}
693
694/// A List of state groups that are dependent on each other for CSS override
695/// order. These are used to determine which state selectors are needed for
696/// `_create-independent-selector()`.
697// Note: Sass syntax does not allow declaring nested Lists; an empty second List
698// placeholder is added for the correct data structure.
699$_dependent-state-groups: ((hover, focus, pressed), ());
700
701/// Creates a selector that will be independent based on the other selectors
702/// that are dependents of it.
703///
704/// Selector dependencies are selector groups that must follow a certain order
705/// for CSS overrides. For example: `:hover`, `:focus`, `:active` or `:link`,
706/// `:visited`, `:hover`, `:active`.
707///
708/// Selectors at the start of a group are dependencies of selectors at the end
709/// of a group.
710///
711/// @example - scss
712/// #{_create-independent-selector(':hover', ':focus', ':active')} {
713/// color: teal;
714/// }
715///
716/// #{_create-independent-selector(':focus', ':active')} {
717/// color: magenta;
718/// }
719///
720/// @example - css
721/// :hover:not(:focus):not(:active) {
722/// color: teal;
723/// }
724///
725/// :focus:not(:active) {
726/// color: magenta;
727/// }
728///
729/// The returned selector is considered "independent" and does not rely on CSS
730/// override order or specificity within its group. In other words, "hover"
731/// styles can be customized after "focus" styles without hiding default focus
732/// styles.
733///
734/// @example - css
735/// /* Default focus styles */
736/// :focus:not(:active) { color: magenta; }
737///
738/// /* New hover styles, does not prevent focus styles from being visible */
739/// :hover:not(:focus):not(:active) { color: orange; }
740///
741/// @param {String} $selector - The main selector to target.
742/// @param {String...} $dependents - Additional group dependents of the main
743/// selector. They will be added as `:not()` selectors.
744/// @return {List} A new independent selector in selector value format.
745@function _create-independent-selector($selector, $dependents...) {
746 @each $dependent in $dependents {
747 @if $dependent {
748 $selector: selector-ext.append-strict(
749 $selector,
750 selector-ext.negate($dependent)
751 );
752 }
753 }
754
755 @return $selector;
756}
757
758@mixin _selector($selectors, $state) {
759 $selectors: _create-selectors(map.merge($selectors, $_override-selectors));
760 @if not map.has-key($selectors, $state) {
761 @error 'Missing #{$state} from #{$selectors}';
762 }
763
764 @at-root {
765 #{selector-ext.append-strict(&, map.get($selectors, $state))} {
766 @content;
767 }
768 }
769}