UNPKG

16.4 kBSCSSView Raw
1//
2// Copyright 2016 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// stylelint-disable selector-class-pattern --
24// Selector '.mdc-*' should only be used in this project.
25
26@use 'sass:color';
27@use 'sass:map';
28@use '@material/animation/functions' as functions2;
29@use '@material/animation/variables' as variables2;
30@use '@material/base/mixins' as base-mixins;
31@use '@material/feature-targeting/feature-targeting';
32@use '@material/theme/css';
33@use '@material/theme/custom-properties';
34@use '@material/theme/theme';
35@use '@material/theme/keys';
36@use '@material/theme/shadow-dom';
37@use '@material/theme/theme-color';
38
39$custom-property-prefix: 'ripple';
40
41$fade-in-duration: 75ms !default;
42$fade-out-duration: 150ms !default;
43$translate-duration: 225ms !default;
44$states-wash-duration: 15ms !default;
45
46// Notes on states:
47// * focus takes precedence over hover (i.e. if an element is both focused and hovered, only focus value applies)
48// * press state applies to a separate pseudo-element, so it has an additive effect on top of other states
49// * selected/activated are applied additively to hover/focus via calculations at preprocessing time
50
51$dark-ink-opacities: (
52 hover: 0.04,
53 focus: 0.12,
54 press: 0.12,
55 selected: 0.08,
56 activated: 0.12,
57) !default;
58
59$light-ink-opacities: (
60 hover: 0.08,
61 focus: 0.24,
62 press: 0.24,
63 selected: 0.16,
64 activated: 0.24,
65) !default;
66
67// Legacy
68
69$pressed-dark-ink-opacity: 0.16 !default;
70$pressed-light-ink-opacity: 0.32 !default;
71
72// State selector variables used for state selector mixins below.
73$_hover-selector: '&:hover';
74$_focus-selector: '&.mdc-ripple-upgraded--background-focused, &:not(.mdc-ripple-upgraded):focus';
75$_active-selector: '&:not(:disabled):active';
76
77$light-theme: (
78 focus-state-layer-color: theme-color.$on-surface,
79 focus-state-layer-opacity: map.get($dark-ink-opacities, focus),
80 hover-state-layer-color: theme-color.$on-surface,
81 hover-state-layer-opacity: map.get($dark-ink-opacities, hover),
82 pressed-state-layer-color: theme-color.$on-surface,
83 pressed-state-layer-opacity: map.get($dark-ink-opacities, press),
84);
85
86@mixin theme($theme) {
87 @include keys.declare-custom-properties(
88 $theme,
89 $prefix: $custom-property-prefix
90 );
91
92 @if shadow-dom.$css-selector-fallback-declarations {
93 .mdc-ripple-surface {
94 @include theme-styles($theme);
95 }
96 }
97}
98
99$_ripple-theme: (
100 hover-state-layer-color: null,
101 focus-state-layer-color: null,
102 pressed-state-layer-color: null,
103 hover-state-layer-opacity: null,
104 focus-state-layer-opacity: null,
105 pressed-state-layer-opacity: null,
106);
107
108@mixin theme-styles($theme, $ripple-target: '&') {
109 $theme: keys.create-theme-properties(
110 $theme,
111 $prefix: $custom-property-prefix
112 );
113
114 // TODO(b/191298796): Support states layer color for every interactive states.
115 // Use only hover state layer color, ignoring focus and pressed color.
116 @include internal-theme-styles($theme, $ripple-target);
117}
118
119@mixin internal-theme-styles($theme, $ripple-target: '&') {
120 @include theme.validate-theme-keys($_ripple-theme, $theme);
121
122 @include states-base-color(
123 map.get($theme, hover-state-layer-color),
124 $ripple-target: $ripple-target
125 );
126 @include states-hover-opacity(
127 map.get($theme, hover-state-layer-opacity),
128 $ripple-target: $ripple-target
129 );
130 @include states-focus-opacity(
131 map.get($theme, focus-state-layer-opacity),
132 $ripple-target: $ripple-target
133 );
134 @include states-press-opacity(
135 map.get($theme, pressed-state-layer-opacity),
136 $ripple-target: $ripple-target
137 );
138}
139
140@mixin states-base-color(
141 $color,
142 $query: feature-targeting.all(),
143 $ripple-target: '&'
144) {
145 $feat-color: feature-targeting.create-target($query, color);
146
147 @if $color {
148 @if not custom-properties.is-custom-prop($color) {
149 $color: custom-properties.create(
150 ripple-color,
151 theme-color.get-custom-property($color)
152 );
153 }
154
155 #{$ripple-target}::before,
156 #{$ripple-target}::after {
157 @include feature-targeting.targets($feat-color) {
158 @include theme.property(background-color, $color);
159 }
160 }
161 }
162}
163
164///
165/// Customizes ripple opacities in `hover`, `focus`, or `press` states
166/// @param {map} $opacity-map - map specifying custom opacity of zero or more states
167/// @param {bool} $has-nested-focusable-element - whether the component contains a focusable element in the root
168/// @param {string} $ripple-target - the optional selector for the ripple element
169///
170@mixin states-opacities(
171 $opacity-map: (),
172 $has-nested-focusable-element: false,
173 $ripple-target: '&',
174 $query: feature-targeting.all()
175) {
176 // Ensure sufficient specificity to override base state opacities
177 @if map.get($opacity-map, hover) {
178 @include states-hover-opacity(
179 map.get($opacity-map, hover),
180 $ripple-target: $ripple-target,
181 $query: $query
182 );
183 }
184
185 @if map.get($opacity-map, focus) {
186 @include states-focus-opacity(
187 map.get($opacity-map, focus),
188 $ripple-target: $ripple-target,
189 $has-nested-focusable-element: $has-nested-focusable-element,
190 $query: $query
191 );
192 }
193
194 @if map.get($opacity-map, press) {
195 @include states-press-opacity(
196 map.get($opacity-map, press),
197 $ripple-target: $ripple-target,
198 $query: $query
199 );
200 }
201}
202
203@mixin states-hover-opacity(
204 $opacity,
205 $query: feature-targeting.all(),
206 $ripple-target: '&'
207) {
208 $feat-color: feature-targeting.create-target($query, color);
209
210 @if $opacity and not custom-properties.is-custom-prop($opacity) {
211 $opacity: custom-properties.create(ripple-hover-opacity, $opacity);
212 }
213
214 // Background wash styles, for both CSS-only and upgraded stateful surfaces
215 &:hover,
216 &.mdc-ripple-surface--hover {
217 @include states-background-selector($ripple-target) {
218 // Opacity falls under color because the chosen opacity is color-dependent in typical usage
219 @include feature-targeting.targets($feat-color) {
220 @include theme.property(opacity, $opacity);
221 }
222 }
223 }
224}
225
226@mixin states-focus-opacity(
227 $opacity,
228 $has-nested-focusable-element: false,
229 $query: feature-targeting.all(),
230 $ripple-target: '&'
231) {
232 // Focus overrides hover by reusing the ::before pseudo-element.
233 // :focus-within generally works on non-MS browsers and matches when a *child* of the element has focus.
234 // It is useful for cases where a component has a focusable element within the root node, e.g. text field,
235 // but undesirable in general in case of nested stateful components.
236 // We use a modifier class for JS-enabled surfaces to support all use cases in all browsers.
237 @if $has-nested-focusable-element {
238 // JS-enabled selectors.
239 &.mdc-ripple-upgraded--background-focused,
240 &.mdc-ripple-upgraded:focus-within,
241 // CSS-only selectors.
242 &:not(.mdc-ripple-upgraded):focus,
243 &:not(.mdc-ripple-upgraded):focus-within {
244 @include states-background-selector($ripple-target) {
245 @include states-focus-opacity-properties_(
246 $opacity: $opacity,
247 $query: $query
248 );
249 }
250 }
251 } @else {
252 // JS-enabled selectors.
253 &.mdc-ripple-upgraded--background-focused,
254 // CSS-only selectors.
255 &:not(.mdc-ripple-upgraded):focus {
256 @include states-background-selector($ripple-target) {
257 @include states-focus-opacity-properties_(
258 $opacity: $opacity,
259 $query: $query
260 );
261 }
262 }
263 }
264}
265
266@mixin states-focus-opacity-properties_($opacity, $query) {
267 $feat-animation: feature-targeting.create-target($query, animation);
268 // Opacity falls under color because the chosen opacity is color-dependent in typical usage
269 $feat-color: feature-targeting.create-target($query, color);
270
271 @if $opacity {
272 @if not custom-properties.is-custom-prop($opacity) {
273 $opacity: custom-properties.create(ripple-focus-opacity, $opacity);
274 }
275
276 // Note that this duration is only effective on focus, not blur
277 @include feature-targeting.targets($feat-animation) {
278 transition-duration: 75ms;
279 }
280
281 @include feature-targeting.targets($feat-color) {
282 @include theme.property(opacity, $opacity);
283 }
284 }
285}
286
287@mixin states-press-opacity(
288 $opacity,
289 $query: feature-targeting.all(),
290 $ripple-target: '&'
291) {
292 $feat-animation: feature-targeting.create-target($query, animation);
293 $feat-color: feature-targeting.create-target($query, color);
294
295 // Styles for non-upgraded (CSS-only) stateful surfaces
296
297 @if $opacity {
298 @if not custom-properties.is-custom-prop($opacity) {
299 $opacity: custom-properties.create(ripple-press-opacity, $opacity);
300 }
301
302 &:not(.mdc-ripple-upgraded) {
303 // Apply press additively by using the ::after pseudo-element
304 #{$ripple-target}::after {
305 @include feature-targeting.targets($feat-animation) {
306 transition: opacity $fade-out-duration linear;
307 }
308 }
309
310 &:active {
311 #{$ripple-target}::after {
312 @include feature-targeting.targets($feat-animation) {
313 transition-duration: $fade-in-duration;
314 }
315
316 // Opacity falls under color because the chosen opacity is color-dependent in typical usage
317 @include feature-targeting.targets($feat-color) {
318 @include theme.property(opacity, $opacity);
319 }
320 }
321 }
322 }
323
324 &.mdc-ripple-upgraded {
325 @include feature-targeting.targets($feat-color) {
326 // Upgraded ripple should always emit custom property, regardless of
327 // configuration, since ripple itself feature detects custom property
328 // support at runtime.
329 @include custom-properties.configure($emit-custom-properties: true) {
330 @include theme.property(
331 custom-properties.create(ripple-fg-opacity, $opacity)
332 );
333 }
334 }
335 }
336 }
337}
338
339// Simple mixin for base states which automatically selects opacity values based on whether the ink color is
340// light or dark.
341@mixin states(
342 $color: theme-color.prop-value(on-surface),
343 $has-nested-focusable-element: false,
344 $query: feature-targeting.all(),
345 $ripple-target: '&',
346 $opacity-map: null
347) {
348 @include states-interactions_(
349 $color: $color,
350 $has-nested-focusable-element: $has-nested-focusable-element,
351 $query: $query,
352 $ripple-target: $ripple-target,
353 $opacity-map: $opacity-map
354 );
355}
356
357// Simple mixin for activated states which automatically selects opacity values based on whether the ink color is
358// light or dark.
359@mixin states-activated(
360 $color,
361 $has-nested-focusable-element: false,
362 $query: feature-targeting.all(),
363 $ripple-target: '&'
364) {
365 $feat-color: feature-targeting.create-target($query, color);
366 $activated-opacity: states-opacity($color, activated);
367
368 &--activated {
369 // Stylelint seems to think that '&' qualifies as a type selector here?
370 @include states-background-selector($ripple-target) {
371 // Opacity falls under color because the chosen opacity is color-dependent.
372 @include feature-targeting.targets($feat-color) {
373 @include theme.property(
374 opacity,
375 custom-properties.create(
376 --mdc-ripple-activated-opacity,
377 $activated-opacity
378 )
379 );
380 }
381 }
382
383 @include states-interactions_(
384 $color: $color,
385 $has-nested-focusable-element: $has-nested-focusable-element,
386 $opacity-modifier: $activated-opacity,
387 $query: $query,
388 $ripple-target: $ripple-target
389 );
390 }
391}
392
393// Simple mixin for selected states which automatically selects opacity values based on whether the ink color is
394// light or dark.
395@mixin states-selected(
396 $color,
397 $has-nested-focusable-element: false,
398 $query: feature-targeting.all(),
399 $ripple-target: '&'
400) {
401 $feat-color: feature-targeting.create-target($query, color);
402 $selected-opacity: states-opacity($color, selected);
403
404 &--selected {
405 @include states-background-selector($ripple-target) {
406 // Opacity falls under color because the chosen opacity is color-dependent.
407 @include feature-targeting.targets($feat-color) {
408 @include theme.property(
409 opacity,
410 custom-properties.create(
411 --mdc-ripple-selected-opacity,
412 $selected-opacity
413 )
414 );
415 }
416 }
417
418 @include states-interactions_(
419 $color: $color,
420 $has-nested-focusable-element: $has-nested-focusable-element,
421 $opacity-modifier: $selected-opacity,
422 $query: $query,
423 $ripple-target: $ripple-target
424 );
425 }
426}
427
428@mixin states-interactions_(
429 $color,
430 $has-nested-focusable-element,
431 $opacity-modifier: 0,
432 $query: feature-targeting.all(),
433 $ripple-target: '&',
434 $opacity-map: null
435) {
436 @include target-selector($ripple-target) {
437 @include states-base-color($color, $query);
438 }
439
440 @if $opacity-map == null {
441 $opacity-map: (
442 hover: states-opacity($color, hover) + $opacity-modifier,
443 focus: states-opacity($color, focus) + $opacity-modifier,
444 press: states-opacity($color, press) + $opacity-modifier,
445 );
446 }
447
448 @include states-opacities(
449 $opacity-map,
450 $has-nested-focusable-element: $has-nested-focusable-element,
451 $ripple-target: $ripple-target,
452 $query: $query
453 );
454}
455
456// Wraps content in the `ripple-target` selector if it exists.
457@mixin target-selector($ripple-target: '&') {
458 @if $ripple-target == '&' {
459 @content;
460 } @else {
461 #{$ripple-target} {
462 @content;
463 }
464 }
465}
466
467/// Selector for hover, active and focus states.
468@mixin states-selector() {
469 #{$_hover-selector},
470 #{$_focus-selector},
471 #{$_active-selector} {
472 @content;
473 }
474}
475
476@mixin hover() {
477 #{$_hover-selector} {
478 @content;
479 }
480}
481
482// Selector for focus state. Using ':not(.mdc-ripple-upgraded)' to continue
483// applying focus styles on JS-disabled components, and control focus
484// on JS-enabled components with '.mdc-ripple-upgraded--background-focused'.
485@mixin focus() {
486 #{$_focus-selector} {
487 @content;
488 }
489}
490
491// Selector for active state. Using `:active:active` to override focus styles.
492@mixin pressed() {
493 #{$_active-selector} {
494 @content;
495 }
496}
497
498// @deprecated Use `pressed()` mixin - renamed for consistency.
499@mixin active() {
500 @include pressed() {
501 @content;
502 }
503}
504
505/// Keep the ripple (State overlay) behind the content.
506@mixin behind-content(
507 $ripple-target,
508 $content-root-selector: '&',
509 $query: feature-targeting.all()
510) {
511 // Needed for IE11. Without this, IE11 renders the state layer completely
512 // underneath the container, making it invisible.
513 $feat-structure: feature-targeting.create-target($query, structure);
514
515 #{$content-root-selector} {
516 @include feature-targeting.targets($feat-structure) {
517 z-index: 0;
518 }
519 }
520
521 #{$ripple-target}::before,
522 #{$ripple-target}::after {
523 @include feature-targeting.targets($feat-structure) {
524 @include theme.property(
525 z-index,
526 custom-properties.create(--mdc-ripple-z-index, -1)
527 );
528 }
529 }
530}
531
532@function states-opacity($color, $state) {
533 $color-value: theme-color.prop-value($color);
534 $opacity-map: if(
535 theme-color.tone($color-value) == 'light',
536 $light-ink-opacities,
537 $dark-ink-opacities
538 );
539
540 @if not map.has-key($opacity-map, $state) {
541 @error "Invalid state: '#{$state}'. Choose one of: #{map.keys($opacity-map)}";
542 }
543
544 @return map.get($opacity-map, $state);
545}
546
547@mixin states-background-selector($ripple-target) {
548 #{$ripple-target}::before {
549 @content;
550 }
551}