UNPKG

15.9 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:selector';
24@use 'sass:string';
25@use 'sass:list';
26@use 'sass:map';
27@use 'sass:meta';
28
29/// Global variable used to conditionally emit CSS selector fallback
30/// declarations in addition to CSS custom property overrides for IE11 support.
31/// Use `enable-css-selector-fallback-declarations()` mixin to configure this
32/// flag.
33///
34/// @example
35///
36/// @include shadow-dom.enable-css-selector-fallback-declarations();
37/// @include foo-bar-theme.theme($theme);
38///
39/// CSS output =>
40///
41/// --foo-bar: red;
42///
43/// // Fallback declarations for IE11 support
44/// .mdc-foo-bar__baz {
45/// color: red;
46/// }
47$css-selector-fallback-declarations: false;
48
49/// Enables CSS selector fallback declarations for IE11 support by setting
50/// global variable `$css-selector-fallback-declarations` to true. Call this
51/// mixin before theme mixin call.
52/// @param {Boolean} $enable Set to `true` to emit CSS selector fallback
53/// declarations.
54/// @example
55/// @include shadow-dom.enable-css-selector-fallback-declarations()
56/// @include foo-bar-theme.theme($theme);
57@mixin enable-css-selector-fallback-declarations($enable) {
58 $css-selector-fallback-declarations: $enable !global;
59}
60
61$_host: ':host';
62$_host-parens: ':host(';
63$_end-parens: ')';
64
65/// @deprecated - Use selector-ext.append-strict() instead:
66///
67/// @example - scss
68/// :host([outlined]), :host, :host button {
69/// @include selector-ext.append-strict(&, ':hover') {
70/// --my-custom-prop: blue;
71/// }
72/// }
73///
74/// @example - css
75/// :host([outlined]:hover), :host(:hover), :host button:hover {
76/// --my-custom-prop: blue;
77/// }
78///
79/// @example - scss
80/// :host([outlined]), :host, :host button {
81/// @at-root {
82/// #{selector-ext.append-strict(&, ':hover')},
83/// & {
84/// --my-custom-prop: blue;
85/// }
86/// }
87/// }
88///
89/// @example - css
90/// :host([outlined]:hover), :host(:hover), :host button:hover,
91/// :host([outlined]), :host, :host button {
92/// --my-custom-prop: blue;
93/// }
94///
95/// Given one or more selectors, this mixin will fix any invalid `:host` parent
96/// nesting by adding parentheses or inserting the nested selector into the
97/// parent `:host()` selector's parentheses. The content block provided to
98/// this mixin
99/// will be projected under the new selectors.
100///
101/// @example
102/// :host([outlined]), :host, :host button {
103/// @include host-aware(selector.append(&, ':hover'), &)) {
104/// --my-custom-prop: blue;
105/// }
106/// }
107///
108/// will output (but with selectors on a single line):
109/// :host([outlined]:hover), // Appended :hover argument
110/// :host(:hover),
111/// :host button:hover,
112/// :host([outlined]), // Ampersand argument
113/// :host,
114/// :host button, {
115/// --my-custom-prop: blue;
116/// };
117///
118/// @param {List} $selector-args - One or more selectors to be fixed for invalid
119/// :host syntax.
120@mixin host-aware($selector-args...) {
121 @each $selector in $selector-args {
122 @if not _is-sass-selector($selector) {
123 @error 'mdc-theme: host-aware() expected a sass:selector value type but received #{$selector}';
124 }
125 }
126
127 @if not _share-common-parent($selector-args...) {
128 @error 'mdc-theme: host-aware() requires all selectors to use the parent selector (&)';
129 }
130
131 $selectors: _flatten-selectors($selector-args...);
132 $processed-selectors: ();
133
134 @each $selector in $selectors {
135 $first-selector: list.nth($selector, 1);
136
137 @if _host-selector-needs-to-be-fixed($first-selector) {
138 $selector: list.set-nth(
139 $selector,
140 1,
141 _fix-host-selector($first-selector)
142 );
143
144 $processed-selectors: list.append(
145 $processed-selectors,
146 $selector,
147 $separator: comma
148 );
149 } @else {
150 // Either not in :host, or there are more selectors following the :host
151 // and nothing needs to be modified. The content can be placed within the
152 // original selector
153 $processed-selectors: list.append(
154 $processed-selectors,
155 $selector,
156 $separator: comma
157 );
158 }
159 }
160
161 @if list.length($processed-selectors) > 0 {
162 @at-root {
163 #{$processed-selectors} {
164 @content;
165 }
166 }
167 }
168}
169
170/// Determines whether a selector needs to be processed.
171/// Selectors that need to be processed would include anything of the format
172/// `^:host(\(.*\))?.+` e.g. `:host([outlined]):hover` or `:host:hover` but not
173/// `:host` or `:host([outlined])`
174///
175/// @param {String} $selector - Selector string to be processed
176/// @return {Boolean} Whether or not the given selector string needs to be fixed
177/// for an invalid :host selector
178@function _host-selector-needs-to-be-fixed($selector) {
179 $host-index: string.index($selector, $_host);
180 $begins-with-host: $host-index == 1;
181
182 @if not $begins-with-host {
183 @return false;
184 }
185
186 $_host-parens-index: _get-last-end-parens-index($selector);
187 $has-parens: $_host-parens-index != null;
188
189 @if $has-parens {
190 // e.g. :host(.inside).after -> needs to be fixed
191 // :host(.inside) -> does not need to be fixed
192 $end-parens-index: string.index($selector, $_end-parens);
193 $content-after-parens: string.slice($selector, $end-parens-index + 1);
194
195 $has-content-after-parens: string.length($selector) > $end-parens-index;
196
197 @return $has-content-after-parens;
198 } @else {
199 // e.g. :host.after -> needs to be fixed
200 // :host -> does not need to be fixed
201 $has-content-after-host: $selector != $_host;
202
203 @return $has-content-after-host;
204 }
205}
206
207/// Flattens a list of selectors
208///
209/// @param {List} $selector-args - A list of selectors to flatten
210/// @return {List} Flattened selectors
211@function _flatten-selectors($selector-args...) {
212 $selectors: ();
213 @each $selector-list in $selector-args {
214 $selectors: list.join($selectors, $selector-list);
215 }
216
217 @return $selectors;
218}
219
220/// Fixes an invalid `:host` selector of the format `^:host(\(.*\))?.+` to
221/// `:host(.+)`
222/// @example
223/// @debug _fix-host-selector(':host:hover') // :host(:hover)
224/// @debug _fix-host-selector(':host([outlined]):hover) // :host([outlined]:hover)
225///
226/// @param {String} $selector - Selector string to be fixed that follows the
227/// following format: `^:host(\(.*\))?.+`
228/// @return {String} Fixed host selector.
229@function _fix-host-selector($selector) {
230 $_host-parens-index: string.index($selector, $_host-parens);
231 $has-parens: $_host-parens-index != null;
232 $new-host-inside: '';
233
234 @if $has-parens {
235 // e.g. :host(.inside).after -> :host(.inside.after)
236 $end-parens-index: _get-last-end-parens-index($selector);
237 $inside-host-parens: string.slice(
238 $selector,
239 string.length($_host-parens) + 1,
240 $end-parens-index - 1
241 );
242 $after-host-parens: string.slice($selector, $end-parens-index + 1);
243
244 $new-host-inside: $inside-host-parens + $after-host-parens;
245 } @else {
246 // e.g. :host.after -> :host(.after)
247 $new-host-inside: string.slice($selector, string.length($_host) + 1);
248 }
249
250 @return ':host(#{$new-host-inside})';
251}
252
253/// Returns the index of the final occurrence of the end-parenthesis in the
254/// given string or null if there is none.
255///
256/// @param {String} $string - The string to be searched
257/// @return {null|Number}
258@function _get-last-end-parens-index($string) {
259 $index: string.length($string);
260
261 @while $index > 0 {
262 $char: string.slice($string, $index, $index);
263 @if $char == $_end-parens {
264 @return $index;
265 }
266
267 $index: $index - 1;
268 }
269
270 @return null;
271}
272
273/// Returns true if the provided List of Sass selectors share a common parent
274/// selector. This function ensures that the parent selector (`&`) is used with
275/// `host-aware()`.
276///
277/// @example
278/// _share-common-parent(
279/// ('.foo:hover'), ('.foo' '.bar'), ('.baz' '.foo')
280/// ); // true
281///
282/// _share-common-parent(
283/// ('.foo:hover'), ('.foo' '.bar'), ('.baz' '.bar')
284/// ); // false
285///
286/// The purpose of this function is to make sure that a group of selectors do
287/// not violate Sass nesting rules. Due to the dynamic nature of `host-aware()`,
288/// it's possible to provide invalid selector combinations.
289///
290/// @example
291/// // Valid native nesting
292/// :host {
293/// &:hover,
294/// .foo,
295/// .bar & {
296/// color: blue;
297/// }
298/// }
299/// // Valid host-aware() nesting
300/// :host {
301/// @include host-aware(
302/// selector.append(&, ':hover'),
303/// selector.nest(&, '.foo'),
304/// selector.nest('.bar', &),
305/// ) {
306/// color: blue;
307/// }
308/// }
309/// // Output
310/// :host(:hover),
311/// :host .foo,
312/// .bar :host {
313/// color: blue;
314/// }
315///
316/// // Invalid use of host-aware()
317/// :host {
318/// @include host-aware(
319/// selector.append(&, ':hover'),
320/// selector.parse('.foo') // Does not share a common parent via `&`
321/// ) {
322/// color: blue;
323/// }
324/// }
325/// // Invalid output: no way to write this natively without using @at-root
326/// :host(:hover),
327/// .foo {
328/// color: blue;
329/// }
330///
331/// @param {Arglist} $selector-lists - An argument list of Sass selectors.
332/// @return true if the selectors share a common parent selector, or false
333/// if not.
334@function _share-common-parent($selector-lists...) {
335 // To validate, this function will extract the simple selectors from each
336 // complex selector and compare them to each other. Every complex selector
337 // should share at least one common simple parent selector.
338 //
339 // We do this by keeping track of each simple selector and if they're present
340 // within a complex selector. At the end of checking all the selectors, at
341 // least one of simple selectors should have been seen for each one of the
342 // complex selectors.
343 //
344 // Each selector list index needs to track its own selector count Map. This is
345 // because each comma-separated list has its own root parent selector that
346 // we're looking for:
347 // .foo,
348 // .bar {
349 // &:hover,
350 // .baz & { ... }
351 // }
352 // ('.foo:hover', '.bar:hover'), ('.baz' '.foo', '.baz' '.bar')
353 //
354 // In the first index of each selector list, we're looking for the parent
355 // ".foo". In the second index we're looking for the parent ".bar".
356 $selector-counts-by-index: ();
357 $expected-counts-by-index: ();
358 @each $selector-list in $selector-lists {
359 @each $complex-selector in $selector-list {
360 $selector-list-index: list.index($selector-list, $complex-selector);
361 $selector-count-map: map.get(
362 $selector-counts-by-index,
363 $selector-list-index
364 );
365 @if not $selector-count-map {
366 $selector-count-map: ();
367 }
368
369 $expected-count: map.get($expected-counts-by-index, $selector-list-index);
370 @if not $expected-count {
371 $expected-count: 0;
372 }
373
374 $simple-selectors-set: ();
375 @each $selector in $complex-selector {
376 @each $simple-selector in selector.simple-selectors($selector) {
377 // Don't use list.join() because there may be duplicate selectors
378 // within the complex selector. We want to treat $simple-selectors-set
379 // like a Set where there are no duplicate values so that we don't
380 // mess up our count by counting one simple selector too many times
381 // for a single complex selector.
382 @if not list.index($simple-selectors-set, $simple-selector) {
383 $simple-selectors-set: list.append(
384 $simple-selectors-set,
385 $simple-selector
386 );
387 }
388 }
389 }
390
391 // Now that we have a "Set" of simple selectors for this complex
392 // selector, we can go through each one and update the selector count Map.
393 @each $simple-selector in $simple-selectors-set {
394 $count: map.get($selector-count-map, $simple-selector);
395 @if $count {
396 $count: $count + 1;
397 } @else {
398 $count: 1;
399 }
400
401 $selector-count-map: map.merge(
402 $selector-count-map,
403 (
404 $simple-selector: $count,
405 )
406 );
407 }
408
409 $selector-counts-by-index: map.merge(
410 $selector-counts-by-index,
411 (
412 $selector-list-index: $selector-count-map,
413 )
414 );
415 $expected-counts-by-index: map.merge(
416 $expected-counts-by-index,
417 (
418 $selector-list-index: $expected-count + 1,
419 )
420 );
421 }
422 }
423
424 @each $index, $selector-count-map in $selector-counts-by-index {
425 // If one of the selectors was seen the expected number of times, then we
426 // can reasonably assume that each selector shares a common parent.
427 // Verify for each index if there are multiple parents.
428 $found-parent: false;
429 @each $selector, $count in $selector-count-map {
430 $expected-count: map.get($expected-counts-by-index, $index);
431 @if $count == $expected-count {
432 $found-parent: true;
433 }
434 }
435
436 @if not $found-parent {
437 @return false;
438 }
439 }
440
441 // A common parent was found for each selector, or there were no selectors
442 // provided and we did not enter any for loops.
443 @return true;
444}
445
446/// Returns true if the value is a Sass selector type.
447///
448/// Selector types are a 2D List: a comma-separated list (the selector list)
449/// that contains space-separated lists (the complex selectors) that contain
450/// unquoted strings (the compound selectors).
451/// @link https://sass-lang.com/documentation/modules/selector
452///
453/// @example
454/// .foo, .bar button:hover { }
455/// $type: ((unquote('.foo')), (unquote('.bar') unquote('button:hover')),);
456///
457/// @param {*} $selector-list - A value to check.
458/// @return {Boolean} true if the value is a Sass selector, or false if not.
459@function _is-sass-selector($selector-list) {
460 // For the purposes of these utility functions, we don't care if the lists
461 // have the correct separated or if the strings are unquoted. All that
462 // matters is that the type is a 2D array and the values are strings to
463 // ensure "close enough" that the selector was generated by Sass.
464 //
465 // This function is primarily a safe-guard against an accidental string
466 // slipping in and forgetting to use a selector.append() which would cause a
467 // hard-to-debug problem.
468 @if meta.type-of($selector-list) != 'list' {
469 @return false;
470 }
471
472 // First level is the selector list: what's separated by commas
473 // e.g. ".foo, .bar"
474 @each $complex-selector in $selector-list {
475 // Second level is the complex selector: what's separated by spaces
476 // e.g. ".foo .bar"
477 @if meta.type-of($complex-selector) != 'list' {
478 @return false;
479 }
480
481 // Third level is the compound selector: the actual string
482 // e.g. ".foo"
483 @each $selector in $complex-selector {
484 @if meta.type-of($selector) != 'string' {
485 @return false;
486 }
487 }
488 }
489
490 @return true;
491}