492 lines
16 KiB
SCSS
492 lines
16 KiB
SCSS
|
//
|
||
|
// 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:selector';
|
||
|
@use 'sass:string';
|
||
|
@use 'sass:list';
|
||
|
@use 'sass:map';
|
||
|
@use 'sass:meta';
|
||
|
|
||
|
/// Global variable used to conditionally emit CSS selector fallback
|
||
|
/// declarations in addition to CSS custom property overrides for IE11 support.
|
||
|
/// Use `enable-css-selector-fallback-declarations()` mixin to configure this
|
||
|
/// flag.
|
||
|
///
|
||
|
/// @example
|
||
|
///
|
||
|
/// @include shadow-dom.enable-css-selector-fallback-declarations();
|
||
|
/// @include foo-bar-theme.theme($theme);
|
||
|
///
|
||
|
/// CSS output =>
|
||
|
///
|
||
|
/// --foo-bar: red;
|
||
|
///
|
||
|
/// // Fallback declarations for IE11 support
|
||
|
/// .mdc-foo-bar__baz {
|
||
|
/// color: red;
|
||
|
/// }
|
||
|
$css-selector-fallback-declarations: false;
|
||
|
|
||
|
/// Enables CSS selector fallback declarations for IE11 support by setting
|
||
|
/// global variable `$css-selector-fallback-declarations` to true. Call this
|
||
|
/// mixin before theme mixin call.
|
||
|
/// @param {Boolean} $enable Set to `true` to emit CSS selector fallback
|
||
|
/// declarations.
|
||
|
/// @example
|
||
|
/// @include shadow-dom.enable-css-selector-fallback-declarations()
|
||
|
/// @include foo-bar-theme.theme($theme);
|
||
|
@mixin enable-css-selector-fallback-declarations($enable) {
|
||
|
$css-selector-fallback-declarations: $enable !global;
|
||
|
}
|
||
|
|
||
|
$_host: ':host';
|
||
|
$_host-parens: ':host(';
|
||
|
$_end-parens: ')';
|
||
|
|
||
|
/// @deprecated - Use selector-ext.append-strict() instead:
|
||
|
///
|
||
|
/// @example - scss
|
||
|
/// :host([outlined]), :host, :host button {
|
||
|
/// @include selector-ext.append-strict(&, ':hover') {
|
||
|
/// --my-custom-prop: blue;
|
||
|
/// }
|
||
|
/// }
|
||
|
///
|
||
|
/// @example - css
|
||
|
/// :host([outlined]:hover), :host(:hover), :host button:hover {
|
||
|
/// --my-custom-prop: blue;
|
||
|
/// }
|
||
|
///
|
||
|
/// @example - scss
|
||
|
/// :host([outlined]), :host, :host button {
|
||
|
/// @at-root {
|
||
|
/// #{selector-ext.append-strict(&, ':hover')},
|
||
|
/// & {
|
||
|
/// --my-custom-prop: blue;
|
||
|
/// }
|
||
|
/// }
|
||
|
/// }
|
||
|
///
|
||
|
/// @example - css
|
||
|
/// :host([outlined]:hover), :host(:hover), :host button:hover,
|
||
|
/// :host([outlined]), :host, :host button {
|
||
|
/// --my-custom-prop: blue;
|
||
|
/// }
|
||
|
///
|
||
|
/// Given one or more selectors, this mixin will fix any invalid `:host` parent
|
||
|
/// nesting by adding parentheses or inserting the nested selector into the
|
||
|
/// parent `:host()` selector's parentheses. The content block provided to
|
||
|
/// this mixin
|
||
|
/// will be projected under the new selectors.
|
||
|
///
|
||
|
/// @example
|
||
|
/// :host([outlined]), :host, :host button {
|
||
|
/// @include host-aware(selector.append(&, ':hover'), &)) {
|
||
|
/// --my-custom-prop: blue;
|
||
|
/// }
|
||
|
/// }
|
||
|
///
|
||
|
/// will output (but with selectors on a single line):
|
||
|
/// :host([outlined]:hover), // Appended :hover argument
|
||
|
/// :host(:hover),
|
||
|
/// :host button:hover,
|
||
|
/// :host([outlined]), // Ampersand argument
|
||
|
/// :host,
|
||
|
/// :host button, {
|
||
|
/// --my-custom-prop: blue;
|
||
|
/// };
|
||
|
///
|
||
|
/// @param {List} $selector-args - One or more selectors to be fixed for invalid
|
||
|
/// :host syntax.
|
||
|
@mixin host-aware($selector-args...) {
|
||
|
@each $selector in $selector-args {
|
||
|
@if not _is-sass-selector($selector) {
|
||
|
@error 'mdc-theme: host-aware() expected a sass:selector value type but received #{$selector}';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@if not _share-common-parent($selector-args...) {
|
||
|
@error 'mdc-theme: host-aware() requires all selectors to use the parent selector (&)';
|
||
|
}
|
||
|
|
||
|
$selectors: _flatten-selectors($selector-args...);
|
||
|
$processed-selectors: ();
|
||
|
|
||
|
@each $selector in $selectors {
|
||
|
$first-selector: list.nth($selector, 1);
|
||
|
|
||
|
@if _host-selector-needs-to-be-fixed($first-selector) {
|
||
|
$selector: list.set-nth(
|
||
|
$selector,
|
||
|
1,
|
||
|
_fix-host-selector($first-selector)
|
||
|
);
|
||
|
|
||
|
$processed-selectors: list.append(
|
||
|
$processed-selectors,
|
||
|
$selector,
|
||
|
$separator: comma
|
||
|
);
|
||
|
} @else {
|
||
|
// Either not in :host, or there are more selectors following the :host
|
||
|
// and nothing needs to be modified. The content can be placed within the
|
||
|
// original selector
|
||
|
$processed-selectors: list.append(
|
||
|
$processed-selectors,
|
||
|
$selector,
|
||
|
$separator: comma
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@if list.length($processed-selectors) > 0 {
|
||
|
@at-root {
|
||
|
#{$processed-selectors} {
|
||
|
@content;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Determines whether a selector needs to be processed.
|
||
|
/// Selectors that need to be processed would include anything of the format
|
||
|
/// `^:host(\(.*\))?.+` e.g. `:host([outlined]):hover` or `:host:hover` but not
|
||
|
/// `:host` or `:host([outlined])`
|
||
|
///
|
||
|
/// @param {String} $selector - Selector string to be processed
|
||
|
/// @return {Boolean} Whether or not the given selector string needs to be fixed
|
||
|
/// for an invalid :host selector
|
||
|
@function _host-selector-needs-to-be-fixed($selector) {
|
||
|
$host-index: string.index($selector, $_host);
|
||
|
$begins-with-host: $host-index == 1;
|
||
|
|
||
|
@if not $begins-with-host {
|
||
|
@return false;
|
||
|
}
|
||
|
|
||
|
$_host-parens-index: _get-last-end-parens-index($selector);
|
||
|
$has-parens: $_host-parens-index != null;
|
||
|
|
||
|
@if $has-parens {
|
||
|
// e.g. :host(.inside).after -> needs to be fixed
|
||
|
// :host(.inside) -> does not need to be fixed
|
||
|
$end-parens-index: string.index($selector, $_end-parens);
|
||
|
$content-after-parens: string.slice($selector, $end-parens-index + 1);
|
||
|
|
||
|
$has-content-after-parens: string.length($selector) > $end-parens-index;
|
||
|
|
||
|
@return $has-content-after-parens;
|
||
|
} @else {
|
||
|
// e.g. :host.after -> needs to be fixed
|
||
|
// :host -> does not need to be fixed
|
||
|
$has-content-after-host: $selector != $_host;
|
||
|
|
||
|
@return $has-content-after-host;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Flattens a list of selectors
|
||
|
///
|
||
|
/// @param {List} $selector-args - A list of selectors to flatten
|
||
|
/// @return {List} Flattened selectors
|
||
|
@function _flatten-selectors($selector-args...) {
|
||
|
$selectors: ();
|
||
|
@each $selector-list in $selector-args {
|
||
|
$selectors: list.join($selectors, $selector-list);
|
||
|
}
|
||
|
|
||
|
@return $selectors;
|
||
|
}
|
||
|
|
||
|
/// Fixes an invalid `:host` selector of the format `^:host(\(.*\))?.+` to
|
||
|
/// `:host(.+)`
|
||
|
/// @example
|
||
|
/// @debug _fix-host-selector(':host:hover') // :host(:hover)
|
||
|
/// @debug _fix-host-selector(':host([outlined]):hover) // :host([outlined]:hover)
|
||
|
///
|
||
|
/// @param {String} $selector - Selector string to be fixed that follows the
|
||
|
/// following format: `^:host(\(.*\))?.+`
|
||
|
/// @return {String} Fixed host selector.
|
||
|
@function _fix-host-selector($selector) {
|
||
|
$_host-parens-index: string.index($selector, $_host-parens);
|
||
|
$has-parens: $_host-parens-index != null;
|
||
|
$new-host-inside: '';
|
||
|
|
||
|
@if $has-parens {
|
||
|
// e.g. :host(.inside).after -> :host(.inside.after)
|
||
|
$end-parens-index: _get-last-end-parens-index($selector);
|
||
|
$inside-host-parens: string.slice(
|
||
|
$selector,
|
||
|
string.length($_host-parens) + 1,
|
||
|
$end-parens-index - 1
|
||
|
);
|
||
|
$after-host-parens: string.slice($selector, $end-parens-index + 1);
|
||
|
|
||
|
$new-host-inside: $inside-host-parens + $after-host-parens;
|
||
|
} @else {
|
||
|
// e.g. :host.after -> :host(.after)
|
||
|
$new-host-inside: string.slice($selector, string.length($_host) + 1);
|
||
|
}
|
||
|
|
||
|
@return ':host(#{$new-host-inside})';
|
||
|
}
|
||
|
|
||
|
/// Returns the index of the final occurrence of the end-parenthesis in the
|
||
|
/// given string or null if there is none.
|
||
|
///
|
||
|
/// @param {String} $string - The string to be searched
|
||
|
/// @return {null|Number}
|
||
|
@function _get-last-end-parens-index($string) {
|
||
|
$index: string.length($string);
|
||
|
|
||
|
@while $index > 0 {
|
||
|
$char: string.slice($string, $index, $index);
|
||
|
@if $char == $_end-parens {
|
||
|
@return $index;
|
||
|
}
|
||
|
|
||
|
$index: $index - 1;
|
||
|
}
|
||
|
|
||
|
@return null;
|
||
|
}
|
||
|
|
||
|
/// Returns true if the provided List of Sass selectors share a common parent
|
||
|
/// selector. This function ensures that the parent selector (`&`) is used with
|
||
|
/// `host-aware()`.
|
||
|
///
|
||
|
/// @example
|
||
|
/// _share-common-parent(
|
||
|
/// ('.foo:hover'), ('.foo' '.bar'), ('.baz' '.foo')
|
||
|
/// ); // true
|
||
|
///
|
||
|
/// _share-common-parent(
|
||
|
/// ('.foo:hover'), ('.foo' '.bar'), ('.baz' '.bar')
|
||
|
/// ); // false
|
||
|
///
|
||
|
/// The purpose of this function is to make sure that a group of selectors do
|
||
|
/// not violate Sass nesting rules. Due to the dynamic nature of `host-aware()`,
|
||
|
/// it's possible to provide invalid selector combinations.
|
||
|
///
|
||
|
/// @example
|
||
|
/// // Valid native nesting
|
||
|
/// :host {
|
||
|
/// &:hover,
|
||
|
/// .foo,
|
||
|
/// .bar & {
|
||
|
/// color: blue;
|
||
|
/// }
|
||
|
/// }
|
||
|
/// // Valid host-aware() nesting
|
||
|
/// :host {
|
||
|
/// @include host-aware(
|
||
|
/// selector.append(&, ':hover'),
|
||
|
/// selector.nest(&, '.foo'),
|
||
|
/// selector.nest('.bar', &),
|
||
|
/// ) {
|
||
|
/// color: blue;
|
||
|
/// }
|
||
|
/// }
|
||
|
/// // Output
|
||
|
/// :host(:hover),
|
||
|
/// :host .foo,
|
||
|
/// .bar :host {
|
||
|
/// color: blue;
|
||
|
/// }
|
||
|
///
|
||
|
/// // Invalid use of host-aware()
|
||
|
/// :host {
|
||
|
/// @include host-aware(
|
||
|
/// selector.append(&, ':hover'),
|
||
|
/// selector.parse('.foo') // Does not share a common parent via `&`
|
||
|
/// ) {
|
||
|
/// color: blue;
|
||
|
/// }
|
||
|
/// }
|
||
|
/// // Invalid output: no way to write this natively without using @at-root
|
||
|
/// :host(:hover),
|
||
|
/// .foo {
|
||
|
/// color: blue;
|
||
|
/// }
|
||
|
///
|
||
|
/// @param {Arglist} $selector-lists - An argument list of Sass selectors.
|
||
|
/// @return true if the selectors share a common parent selector, or false
|
||
|
/// if not.
|
||
|
@function _share-common-parent($selector-lists...) {
|
||
|
// To validate, this function will extract the simple selectors from each
|
||
|
// complex selector and compare them to each other. Every complex selector
|
||
|
// should share at least one common simple parent selector.
|
||
|
//
|
||
|
// We do this by keeping track of each simple selector and if they're present
|
||
|
// within a complex selector. At the end of checking all the selectors, at
|
||
|
// least one of simple selectors should have been seen for each one of the
|
||
|
// complex selectors.
|
||
|
//
|
||
|
// Each selector list index needs to track its own selector count Map. This is
|
||
|
// because each comma-separated list has its own root parent selector that
|
||
|
// we're looking for:
|
||
|
// .foo,
|
||
|
// .bar {
|
||
|
// &:hover,
|
||
|
// .baz & { ... }
|
||
|
// }
|
||
|
// ('.foo:hover', '.bar:hover'), ('.baz' '.foo', '.baz' '.bar')
|
||
|
//
|
||
|
// In the first index of each selector list, we're looking for the parent
|
||
|
// ".foo". In the second index we're looking for the parent ".bar".
|
||
|
$selector-counts-by-index: ();
|
||
|
$expected-counts-by-index: ();
|
||
|
@each $selector-list in $selector-lists {
|
||
|
@each $complex-selector in $selector-list {
|
||
|
$selector-list-index: list.index($selector-list, $complex-selector);
|
||
|
$selector-count-map: map.get(
|
||
|
$selector-counts-by-index,
|
||
|
$selector-list-index
|
||
|
);
|
||
|
@if not $selector-count-map {
|
||
|
$selector-count-map: ();
|
||
|
}
|
||
|
|
||
|
$expected-count: map.get($expected-counts-by-index, $selector-list-index);
|
||
|
@if not $expected-count {
|
||
|
$expected-count: 0;
|
||
|
}
|
||
|
|
||
|
$simple-selectors-set: ();
|
||
|
@each $selector in $complex-selector {
|
||
|
@each $simple-selector in selector.simple-selectors($selector) {
|
||
|
// Don't use list.join() because there may be duplicate selectors
|
||
|
// within the complex selector. We want to treat $simple-selectors-set
|
||
|
// like a Set where there are no duplicate values so that we don't
|
||
|
// mess up our count by counting one simple selector too many times
|
||
|
// for a single complex selector.
|
||
|
@if not list.index($simple-selectors-set, $simple-selector) {
|
||
|
$simple-selectors-set: list.append(
|
||
|
$simple-selectors-set,
|
||
|
$simple-selector
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Now that we have a "Set" of simple selectors for this complex
|
||
|
// selector, we can go through each one and update the selector count Map.
|
||
|
@each $simple-selector in $simple-selectors-set {
|
||
|
$count: map.get($selector-count-map, $simple-selector);
|
||
|
@if $count {
|
||
|
$count: $count + 1;
|
||
|
} @else {
|
||
|
$count: 1;
|
||
|
}
|
||
|
|
||
|
$selector-count-map: map.merge(
|
||
|
$selector-count-map,
|
||
|
(
|
||
|
$simple-selector: $count,
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$selector-counts-by-index: map.merge(
|
||
|
$selector-counts-by-index,
|
||
|
(
|
||
|
$selector-list-index: $selector-count-map,
|
||
|
)
|
||
|
);
|
||
|
$expected-counts-by-index: map.merge(
|
||
|
$expected-counts-by-index,
|
||
|
(
|
||
|
$selector-list-index: $expected-count + 1,
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@each $index, $selector-count-map in $selector-counts-by-index {
|
||
|
// If one of the selectors was seen the expected number of times, then we
|
||
|
// can reasonably assume that each selector shares a common parent.
|
||
|
// Verify for each index if there are multiple parents.
|
||
|
$found-parent: false;
|
||
|
@each $selector, $count in $selector-count-map {
|
||
|
$expected-count: map.get($expected-counts-by-index, $index);
|
||
|
@if $count == $expected-count {
|
||
|
$found-parent: true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@if not $found-parent {
|
||
|
@return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// A common parent was found for each selector, or there were no selectors
|
||
|
// provided and we did not enter any for loops.
|
||
|
@return true;
|
||
|
}
|
||
|
|
||
|
/// Returns true if the value is a Sass selector type.
|
||
|
///
|
||
|
/// Selector types are a 2D List: a comma-separated list (the selector list)
|
||
|
/// that contains space-separated lists (the complex selectors) that contain
|
||
|
/// unquoted strings (the compound selectors).
|
||
|
/// @link https://sass-lang.com/documentation/modules/selector
|
||
|
///
|
||
|
/// @example
|
||
|
/// .foo, .bar button:hover { }
|
||
|
/// $type: ((unquote('.foo')), (unquote('.bar') unquote('button:hover')),);
|
||
|
///
|
||
|
/// @param {*} $selector-list - A value to check.
|
||
|
/// @return {Boolean} true if the value is a Sass selector, or false if not.
|
||
|
@function _is-sass-selector($selector-list) {
|
||
|
// For the purposes of these utility functions, we don't care if the lists
|
||
|
// have the correct separated or if the strings are unquoted. All that
|
||
|
// matters is that the type is a 2D array and the values are strings to
|
||
|
// ensure "close enough" that the selector was generated by Sass.
|
||
|
//
|
||
|
// This function is primarily a safe-guard against an accidental string
|
||
|
// slipping in and forgetting to use a selector.append() which would cause a
|
||
|
// hard-to-debug problem.
|
||
|
@if meta.type-of($selector-list) != 'list' {
|
||
|
@return false;
|
||
|
}
|
||
|
|
||
|
// First level is the selector list: what's separated by commas
|
||
|
// e.g. ".foo, .bar"
|
||
|
@each $complex-selector in $selector-list {
|
||
|
// Second level is the complex selector: what's separated by spaces
|
||
|
// e.g. ".foo .bar"
|
||
|
@if meta.type-of($complex-selector) != 'list' {
|
||
|
@return false;
|
||
|
}
|
||
|
|
||
|
// Third level is the compound selector: the actual string
|
||
|
// e.g. ".foo"
|
||
|
@each $selector in $complex-selector {
|
||
|
@if meta.type-of($selector) != 'string' {
|
||
|
@return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@return true;
|
||
|
}
|