UNPKG

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