552 lines
16 KiB
SCSS
552 lines
16 KiB
SCSS
|
//
|
||
|
// Copyright 2016 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.
|
||
|
//
|
||
|
|
||
|
// stylelint-disable selector-class-pattern --
|
||
|
// Selector '.mdc-*' should only be used in this project.
|
||
|
|
||
|
@use 'sass:color';
|
||
|
@use 'sass:map';
|
||
|
@use '@material/animation/functions' as functions2;
|
||
|
@use '@material/animation/variables' as variables2;
|
||
|
@use '@material/base/mixins' as base-mixins;
|
||
|
@use '@material/feature-targeting/feature-targeting';
|
||
|
@use '@material/theme/css';
|
||
|
@use '@material/theme/custom-properties';
|
||
|
@use '@material/theme/theme';
|
||
|
@use '@material/theme/keys';
|
||
|
@use '@material/theme/shadow-dom';
|
||
|
@use '@material/theme/theme-color';
|
||
|
|
||
|
$custom-property-prefix: 'ripple';
|
||
|
|
||
|
$fade-in-duration: 75ms !default;
|
||
|
$fade-out-duration: 150ms !default;
|
||
|
$translate-duration: 225ms !default;
|
||
|
$states-wash-duration: 15ms !default;
|
||
|
|
||
|
// Notes on states:
|
||
|
// * focus takes precedence over hover (i.e. if an element is both focused and hovered, only focus value applies)
|
||
|
// * press state applies to a separate pseudo-element, so it has an additive effect on top of other states
|
||
|
// * selected/activated are applied additively to hover/focus via calculations at preprocessing time
|
||
|
|
||
|
$dark-ink-opacities: (
|
||
|
hover: 0.04,
|
||
|
focus: 0.12,
|
||
|
press: 0.12,
|
||
|
selected: 0.08,
|
||
|
activated: 0.12,
|
||
|
) !default;
|
||
|
|
||
|
$light-ink-opacities: (
|
||
|
hover: 0.08,
|
||
|
focus: 0.24,
|
||
|
press: 0.24,
|
||
|
selected: 0.16,
|
||
|
activated: 0.24,
|
||
|
) !default;
|
||
|
|
||
|
// Legacy
|
||
|
|
||
|
$pressed-dark-ink-opacity: 0.16 !default;
|
||
|
$pressed-light-ink-opacity: 0.32 !default;
|
||
|
|
||
|
// State selector variables used for state selector mixins below.
|
||
|
$_hover-selector: '&:hover';
|
||
|
$_focus-selector: '&.mdc-ripple-upgraded--background-focused, &:not(.mdc-ripple-upgraded):focus';
|
||
|
$_active-selector: '&:not(:disabled):active';
|
||
|
|
||
|
$light-theme: (
|
||
|
focus-state-layer-color: theme-color.$on-surface,
|
||
|
focus-state-layer-opacity: map.get($dark-ink-opacities, focus),
|
||
|
hover-state-layer-color: theme-color.$on-surface,
|
||
|
hover-state-layer-opacity: map.get($dark-ink-opacities, hover),
|
||
|
pressed-state-layer-color: theme-color.$on-surface,
|
||
|
pressed-state-layer-opacity: map.get($dark-ink-opacities, press),
|
||
|
);
|
||
|
|
||
|
@mixin theme($theme) {
|
||
|
@include keys.declare-custom-properties(
|
||
|
$theme,
|
||
|
$prefix: $custom-property-prefix
|
||
|
);
|
||
|
|
||
|
@if shadow-dom.$css-selector-fallback-declarations {
|
||
|
.mdc-ripple-surface {
|
||
|
@include theme-styles($theme);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$_ripple-theme: (
|
||
|
hover-state-layer-color: null,
|
||
|
focus-state-layer-color: null,
|
||
|
pressed-state-layer-color: null,
|
||
|
hover-state-layer-opacity: null,
|
||
|
focus-state-layer-opacity: null,
|
||
|
pressed-state-layer-opacity: null,
|
||
|
);
|
||
|
|
||
|
@mixin theme-styles($theme, $ripple-target: '&') {
|
||
|
$theme: keys.create-theme-properties(
|
||
|
$theme,
|
||
|
$prefix: $custom-property-prefix
|
||
|
);
|
||
|
|
||
|
// TODO(b/191298796): Support states layer color for every interactive states.
|
||
|
// Use only hover state layer color, ignoring focus and pressed color.
|
||
|
@include internal-theme-styles($theme, $ripple-target);
|
||
|
}
|
||
|
|
||
|
@mixin internal-theme-styles($theme, $ripple-target: '&') {
|
||
|
@include theme.validate-theme-keys($_ripple-theme, $theme);
|
||
|
|
||
|
@include states-base-color(
|
||
|
map.get($theme, hover-state-layer-color),
|
||
|
$ripple-target: $ripple-target
|
||
|
);
|
||
|
@include states-hover-opacity(
|
||
|
map.get($theme, hover-state-layer-opacity),
|
||
|
$ripple-target: $ripple-target
|
||
|
);
|
||
|
@include states-focus-opacity(
|
||
|
map.get($theme, focus-state-layer-opacity),
|
||
|
$ripple-target: $ripple-target
|
||
|
);
|
||
|
@include states-press-opacity(
|
||
|
map.get($theme, pressed-state-layer-opacity),
|
||
|
$ripple-target: $ripple-target
|
||
|
);
|
||
|
}
|
||
|
|
||
|
@mixin states-base-color(
|
||
|
$color,
|
||
|
$query: feature-targeting.all(),
|
||
|
$ripple-target: '&'
|
||
|
) {
|
||
|
$feat-color: feature-targeting.create-target($query, color);
|
||
|
|
||
|
@if $color {
|
||
|
@if not custom-properties.is-custom-prop($color) {
|
||
|
$color: custom-properties.create(
|
||
|
ripple-color,
|
||
|
theme-color.get-custom-property($color)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
#{$ripple-target}::before,
|
||
|
#{$ripple-target}::after {
|
||
|
@include feature-targeting.targets($feat-color) {
|
||
|
@include theme.property(background-color, $color);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
///
|
||
|
/// Customizes ripple opacities in `hover`, `focus`, or `press` states
|
||
|
/// @param {map} $opacity-map - map specifying custom opacity of zero or more states
|
||
|
/// @param {bool} $has-nested-focusable-element - whether the component contains a focusable element in the root
|
||
|
/// @param {string} $ripple-target - the optional selector for the ripple element
|
||
|
///
|
||
|
@mixin states-opacities(
|
||
|
$opacity-map: (),
|
||
|
$has-nested-focusable-element: false,
|
||
|
$ripple-target: '&',
|
||
|
$query: feature-targeting.all()
|
||
|
) {
|
||
|
// Ensure sufficient specificity to override base state opacities
|
||
|
@if map.get($opacity-map, hover) {
|
||
|
@include states-hover-opacity(
|
||
|
map.get($opacity-map, hover),
|
||
|
$ripple-target: $ripple-target,
|
||
|
$query: $query
|
||
|
);
|
||
|
}
|
||
|
|
||
|
@if map.get($opacity-map, focus) {
|
||
|
@include states-focus-opacity(
|
||
|
map.get($opacity-map, focus),
|
||
|
$ripple-target: $ripple-target,
|
||
|
$has-nested-focusable-element: $has-nested-focusable-element,
|
||
|
$query: $query
|
||
|
);
|
||
|
}
|
||
|
|
||
|
@if map.get($opacity-map, press) {
|
||
|
@include states-press-opacity(
|
||
|
map.get($opacity-map, press),
|
||
|
$ripple-target: $ripple-target,
|
||
|
$query: $query
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@mixin states-hover-opacity(
|
||
|
$opacity,
|
||
|
$query: feature-targeting.all(),
|
||
|
$ripple-target: '&'
|
||
|
) {
|
||
|
$feat-color: feature-targeting.create-target($query, color);
|
||
|
|
||
|
@if $opacity and not custom-properties.is-custom-prop($opacity) {
|
||
|
$opacity: custom-properties.create(ripple-hover-opacity, $opacity);
|
||
|
}
|
||
|
|
||
|
// Background wash styles, for both CSS-only and upgraded stateful surfaces
|
||
|
&:hover,
|
||
|
&.mdc-ripple-surface--hover {
|
||
|
@include states-background-selector($ripple-target) {
|
||
|
// Opacity falls under color because the chosen opacity is color-dependent in typical usage
|
||
|
@include feature-targeting.targets($feat-color) {
|
||
|
@include theme.property(opacity, $opacity);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@mixin states-focus-opacity(
|
||
|
$opacity,
|
||
|
$has-nested-focusable-element: false,
|
||
|
$query: feature-targeting.all(),
|
||
|
$ripple-target: '&'
|
||
|
) {
|
||
|
// Focus overrides hover by reusing the ::before pseudo-element.
|
||
|
// :focus-within generally works on non-MS browsers and matches when a *child* of the element has focus.
|
||
|
// It is useful for cases where a component has a focusable element within the root node, e.g. text field,
|
||
|
// but undesirable in general in case of nested stateful components.
|
||
|
// We use a modifier class for JS-enabled surfaces to support all use cases in all browsers.
|
||
|
@if $has-nested-focusable-element {
|
||
|
// JS-enabled selectors.
|
||
|
&.mdc-ripple-upgraded--background-focused,
|
||
|
&.mdc-ripple-upgraded:focus-within,
|
||
|
// CSS-only selectors.
|
||
|
&:not(.mdc-ripple-upgraded):focus,
|
||
|
&:not(.mdc-ripple-upgraded):focus-within {
|
||
|
@include states-background-selector($ripple-target) {
|
||
|
@include states-focus-opacity-properties_(
|
||
|
$opacity: $opacity,
|
||
|
$query: $query
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
} @else {
|
||
|
// JS-enabled selectors.
|
||
|
&.mdc-ripple-upgraded--background-focused,
|
||
|
// CSS-only selectors.
|
||
|
&:not(.mdc-ripple-upgraded):focus {
|
||
|
@include states-background-selector($ripple-target) {
|
||
|
@include states-focus-opacity-properties_(
|
||
|
$opacity: $opacity,
|
||
|
$query: $query
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@mixin states-focus-opacity-properties_($opacity, $query) {
|
||
|
$feat-animation: feature-targeting.create-target($query, animation);
|
||
|
// Opacity falls under color because the chosen opacity is color-dependent in typical usage
|
||
|
$feat-color: feature-targeting.create-target($query, color);
|
||
|
|
||
|
@if $opacity {
|
||
|
@if not custom-properties.is-custom-prop($opacity) {
|
||
|
$opacity: custom-properties.create(ripple-focus-opacity, $opacity);
|
||
|
}
|
||
|
|
||
|
// Note that this duration is only effective on focus, not blur
|
||
|
@include feature-targeting.targets($feat-animation) {
|
||
|
transition-duration: 75ms;
|
||
|
}
|
||
|
|
||
|
@include feature-targeting.targets($feat-color) {
|
||
|
@include theme.property(opacity, $opacity);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@mixin states-press-opacity(
|
||
|
$opacity,
|
||
|
$query: feature-targeting.all(),
|
||
|
$ripple-target: '&'
|
||
|
) {
|
||
|
$feat-animation: feature-targeting.create-target($query, animation);
|
||
|
$feat-color: feature-targeting.create-target($query, color);
|
||
|
|
||
|
// Styles for non-upgraded (CSS-only) stateful surfaces
|
||
|
|
||
|
@if $opacity {
|
||
|
@if not custom-properties.is-custom-prop($opacity) {
|
||
|
$opacity: custom-properties.create(ripple-press-opacity, $opacity);
|
||
|
}
|
||
|
|
||
|
&:not(.mdc-ripple-upgraded) {
|
||
|
// Apply press additively by using the ::after pseudo-element
|
||
|
#{$ripple-target}::after {
|
||
|
@include feature-targeting.targets($feat-animation) {
|
||
|
transition: opacity $fade-out-duration linear;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
&:active {
|
||
|
#{$ripple-target}::after {
|
||
|
@include feature-targeting.targets($feat-animation) {
|
||
|
transition-duration: $fade-in-duration;
|
||
|
}
|
||
|
|
||
|
// Opacity falls under color because the chosen opacity is color-dependent in typical usage
|
||
|
@include feature-targeting.targets($feat-color) {
|
||
|
@include theme.property(opacity, $opacity);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
&.mdc-ripple-upgraded {
|
||
|
@include feature-targeting.targets($feat-color) {
|
||
|
// Upgraded ripple should always emit custom property, regardless of
|
||
|
// configuration, since ripple itself feature detects custom property
|
||
|
// support at runtime.
|
||
|
@include custom-properties.configure($emit-custom-properties: true) {
|
||
|
@include theme.property(
|
||
|
custom-properties.create(ripple-fg-opacity, $opacity)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Simple mixin for base states which automatically selects opacity values based on whether the ink color is
|
||
|
// light or dark.
|
||
|
@mixin states(
|
||
|
$color: theme-color.prop-value(on-surface),
|
||
|
$has-nested-focusable-element: false,
|
||
|
$query: feature-targeting.all(),
|
||
|
$ripple-target: '&',
|
||
|
$opacity-map: null
|
||
|
) {
|
||
|
@include states-interactions_(
|
||
|
$color: $color,
|
||
|
$has-nested-focusable-element: $has-nested-focusable-element,
|
||
|
$query: $query,
|
||
|
$ripple-target: $ripple-target,
|
||
|
$opacity-map: $opacity-map
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Simple mixin for activated states which automatically selects opacity values based on whether the ink color is
|
||
|
// light or dark.
|
||
|
@mixin states-activated(
|
||
|
$color,
|
||
|
$has-nested-focusable-element: false,
|
||
|
$query: feature-targeting.all(),
|
||
|
$ripple-target: '&'
|
||
|
) {
|
||
|
$feat-color: feature-targeting.create-target($query, color);
|
||
|
$activated-opacity: states-opacity($color, activated);
|
||
|
|
||
|
&--activated {
|
||
|
// Stylelint seems to think that '&' qualifies as a type selector here?
|
||
|
@include states-background-selector($ripple-target) {
|
||
|
// Opacity falls under color because the chosen opacity is color-dependent.
|
||
|
@include feature-targeting.targets($feat-color) {
|
||
|
@include theme.property(
|
||
|
opacity,
|
||
|
custom-properties.create(
|
||
|
--mdc-ripple-activated-opacity,
|
||
|
$activated-opacity
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@include states-interactions_(
|
||
|
$color: $color,
|
||
|
$has-nested-focusable-element: $has-nested-focusable-element,
|
||
|
$opacity-modifier: $activated-opacity,
|
||
|
$query: $query,
|
||
|
$ripple-target: $ripple-target
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Simple mixin for selected states which automatically selects opacity values based on whether the ink color is
|
||
|
// light or dark.
|
||
|
@mixin states-selected(
|
||
|
$color,
|
||
|
$has-nested-focusable-element: false,
|
||
|
$query: feature-targeting.all(),
|
||
|
$ripple-target: '&'
|
||
|
) {
|
||
|
$feat-color: feature-targeting.create-target($query, color);
|
||
|
$selected-opacity: states-opacity($color, selected);
|
||
|
|
||
|
&--selected {
|
||
|
@include states-background-selector($ripple-target) {
|
||
|
// Opacity falls under color because the chosen opacity is color-dependent.
|
||
|
@include feature-targeting.targets($feat-color) {
|
||
|
@include theme.property(
|
||
|
opacity,
|
||
|
custom-properties.create(
|
||
|
--mdc-ripple-selected-opacity,
|
||
|
$selected-opacity
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@include states-interactions_(
|
||
|
$color: $color,
|
||
|
$has-nested-focusable-element: $has-nested-focusable-element,
|
||
|
$opacity-modifier: $selected-opacity,
|
||
|
$query: $query,
|
||
|
$ripple-target: $ripple-target
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@mixin states-interactions_(
|
||
|
$color,
|
||
|
$has-nested-focusable-element,
|
||
|
$opacity-modifier: 0,
|
||
|
$query: feature-targeting.all(),
|
||
|
$ripple-target: '&',
|
||
|
$opacity-map: null
|
||
|
) {
|
||
|
@include target-selector($ripple-target) {
|
||
|
@include states-base-color($color, $query);
|
||
|
}
|
||
|
|
||
|
@if $opacity-map == null {
|
||
|
$opacity-map: (
|
||
|
hover: states-opacity($color, hover) + $opacity-modifier,
|
||
|
focus: states-opacity($color, focus) + $opacity-modifier,
|
||
|
press: states-opacity($color, press) + $opacity-modifier,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
@include states-opacities(
|
||
|
$opacity-map,
|
||
|
$has-nested-focusable-element: $has-nested-focusable-element,
|
||
|
$ripple-target: $ripple-target,
|
||
|
$query: $query
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Wraps content in the `ripple-target` selector if it exists.
|
||
|
@mixin target-selector($ripple-target: '&') {
|
||
|
@if $ripple-target == '&' {
|
||
|
@content;
|
||
|
} @else {
|
||
|
#{$ripple-target} {
|
||
|
@content;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Selector for hover, active and focus states.
|
||
|
@mixin states-selector() {
|
||
|
#{$_hover-selector},
|
||
|
#{$_focus-selector},
|
||
|
#{$_active-selector} {
|
||
|
@content;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@mixin hover() {
|
||
|
#{$_hover-selector} {
|
||
|
@content;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Selector for focus state. Using ':not(.mdc-ripple-upgraded)' to continue
|
||
|
// applying focus styles on JS-disabled components, and control focus
|
||
|
// on JS-enabled components with '.mdc-ripple-upgraded--background-focused'.
|
||
|
@mixin focus() {
|
||
|
#{$_focus-selector} {
|
||
|
@content;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Selector for active state. Using `:active:active` to override focus styles.
|
||
|
@mixin pressed() {
|
||
|
#{$_active-selector} {
|
||
|
@content;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// @deprecated Use `pressed()` mixin - renamed for consistency.
|
||
|
@mixin active() {
|
||
|
@include pressed() {
|
||
|
@content;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Keep the ripple (State overlay) behind the content.
|
||
|
@mixin behind-content(
|
||
|
$ripple-target,
|
||
|
$content-root-selector: '&',
|
||
|
$query: feature-targeting.all()
|
||
|
) {
|
||
|
// Needed for IE11. Without this, IE11 renders the state layer completely
|
||
|
// underneath the container, making it invisible.
|
||
|
$feat-structure: feature-targeting.create-target($query, structure);
|
||
|
|
||
|
#{$content-root-selector} {
|
||
|
@include feature-targeting.targets($feat-structure) {
|
||
|
z-index: 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#{$ripple-target}::before,
|
||
|
#{$ripple-target}::after {
|
||
|
@include feature-targeting.targets($feat-structure) {
|
||
|
@include theme.property(
|
||
|
z-index,
|
||
|
custom-properties.create(--mdc-ripple-z-index, -1)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@function states-opacity($color, $state) {
|
||
|
$color-value: theme-color.prop-value($color);
|
||
|
$opacity-map: if(
|
||
|
theme-color.tone($color-value) == 'light',
|
||
|
$light-ink-opacities,
|
||
|
$dark-ink-opacities
|
||
|
);
|
||
|
|
||
|
@if not map.has-key($opacity-map, $state) {
|
||
|
@error "Invalid state: '#{$state}'. Choose one of: #{map.keys($opacity-map)}";
|
||
|
}
|
||
|
|
||
|
@return map.get($opacity-map, $state);
|
||
|
}
|
||
|
|
||
|
@mixin states-background-selector($ripple-target) {
|
||
|
#{$ripple-target}::before {
|
||
|
@content;
|
||
|
}
|
||
|
}
|