mirror of
https://github.com/material-components/material-web.git
synced 2026-03-09 00:09:23 +08:00
458 lines
12 KiB
SCSS
458 lines
12 KiB
SCSS
//
|
|
// Copyright 2022 Google LLC
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
// go/keep-sorted start
|
|
@use 'sass:map';
|
|
@use 'sass:math';
|
|
// go/keep-sorted end
|
|
// go/keep-sorted start
|
|
@use '../../focus/focus-ring';
|
|
@use '../../ripple/ripple';
|
|
@use '../../sass/theme';
|
|
@use '../../tokens';
|
|
// go/keep-sorted end
|
|
|
|
// Motion token values.
|
|
$_md-sys-motion: tokens.md-sys-motion-values();
|
|
// The stroke width of the icon marks.
|
|
$_mark-stroke: 2px;
|
|
// The coordinates in an 18px viewBox of the bottom left corner of the
|
|
// indeterminate icon. Y is negative to fix an issue in Safari (see below).
|
|
$_indeterminate-bottom-left: 4px, -10px;
|
|
// The coordinates in an 18px viewBox of the bottom left corner of the
|
|
// checkmark icon. Y is negative to fix an issue in Safari (see below).
|
|
$_checkmark-bottom-left: 7px, -14px;
|
|
|
|
@mixin theme($tokens) {
|
|
$tokens: _resolve-tokens($tokens);
|
|
$tokens: theme.validate-theme(
|
|
_resolve-tokens(tokens.md-comp-checkbox-values()),
|
|
$tokens
|
|
);
|
|
$tokens: theme.create-theme-vars($tokens, checkbox);
|
|
|
|
@include theme.emit-theme-vars($tokens);
|
|
}
|
|
|
|
@mixin styles() {
|
|
$tokens: _resolve-tokens(tokens.md-comp-checkbox-values());
|
|
$tokens: theme.create-theme-vars($tokens, checkbox);
|
|
|
|
:host {
|
|
@each $token, $value in $tokens {
|
|
--_#{$token}: #{$value};
|
|
}
|
|
|
|
border-radius: var(--_container-shape);
|
|
display: inline-flex;
|
|
height: 48px;
|
|
position: relative;
|
|
vertical-align: top; // Fix extra space when placed inside display: block
|
|
width: 48px;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
input {
|
|
appearance: none;
|
|
inset: 0;
|
|
margin: 0;
|
|
outline: none;
|
|
position: absolute;
|
|
opacity: 0;
|
|
block-size: 100%;
|
|
inline-size: 100%;
|
|
}
|
|
|
|
.container {
|
|
border-radius: inherit;
|
|
height: 100%;
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
|
|
// Center elements within the container.
|
|
.outline,
|
|
.background,
|
|
md-ripple,
|
|
.icon {
|
|
inset: 0;
|
|
margin: auto;
|
|
position: absolute;
|
|
}
|
|
|
|
.outline,
|
|
.background {
|
|
border-radius: inherit;
|
|
height: var(--_container-size);
|
|
width: var(--_container-size);
|
|
}
|
|
|
|
.outline {
|
|
border-color: var(--_unselected-outline-color);
|
|
border-style: solid;
|
|
border-width: var(--_unselected-outline-width);
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.background {
|
|
background-color: var(--_selected-container-color);
|
|
}
|
|
|
|
// Background and icon transitions.
|
|
.background,
|
|
.icon {
|
|
opacity: 0; // Background and icon fade in
|
|
transition-duration: 150ms, 50ms; // Exit duration for scale and opacity.
|
|
transition-property: transform, opacity;
|
|
// Exit easing function for scale, linear for opacity.
|
|
transition-timing-function: map.get(
|
|
$_md-sys-motion,
|
|
easing-emphasized-accelerate
|
|
),
|
|
linear;
|
|
transform: scale(0.6); // Background and icon scale from 60% to 100%.
|
|
}
|
|
|
|
.selected .background,
|
|
.selected .icon {
|
|
opacity: 1;
|
|
// Enter duration for scale and opacity.
|
|
transition-duration: 350ms, 50ms;
|
|
// Enter easing function for scale, linear for opacity.
|
|
transition-timing-function: map.get(
|
|
$_md-sys-motion,
|
|
easing-emphasized-decelerate
|
|
),
|
|
linear;
|
|
transform: scale(1);
|
|
}
|
|
|
|
// Focus ring
|
|
|
|
md-focus-ring {
|
|
@include focus-ring.theme(
|
|
(
|
|
offset-vertical: -2px,
|
|
offset-horizontal: -2px,
|
|
shape: map.get(tokens.md-sys-shape-values(), corner-full),
|
|
)
|
|
);
|
|
}
|
|
|
|
// Ripple
|
|
|
|
md-ripple {
|
|
height: var(--_state-layer-size);
|
|
width: var(--_state-layer-size);
|
|
|
|
@include ripple.theme(
|
|
(
|
|
focus-color: var(--_unselected-focus-state-layer-color),
|
|
focus-opacity: var(--_unselected-focus-state-layer-opacity),
|
|
hover-color: var(--_unselected-hover-state-layer-color),
|
|
hover-opacity: var(--_unselected-hover-state-layer-opacity),
|
|
pressed-color: var(--_unselected-pressed-state-layer-color),
|
|
pressed-opacity: var(--_unselected-pressed-state-layer-opacity),
|
|
shape: var(--_state-layer-shape),
|
|
)
|
|
);
|
|
}
|
|
|
|
.selected md-ripple {
|
|
@include ripple.theme(
|
|
(
|
|
focus-color: var(--_selected-focus-state-layer-color),
|
|
focus-opacity: var(--_selected-focus-state-layer-opacity),
|
|
hover-color: var(--_selected-hover-state-layer-color),
|
|
hover-opacity: var(--_selected-hover-state-layer-opacity),
|
|
pressed-color: var(--_selected-pressed-state-layer-color),
|
|
pressed-opacity: var(--_selected-pressed-state-layer-opacity),
|
|
)
|
|
);
|
|
}
|
|
|
|
.error md-ripple {
|
|
@include ripple.theme(
|
|
(
|
|
focus-color: var(--_error-focus-state-layer-color),
|
|
focus-opacity: var(--_error-focus-state-layer-opacity),
|
|
hover-color: var(--_error-hover-state-layer-color),
|
|
hover-opacity: var(--_error-hover-state-layer-opacity),
|
|
pressed-color: var(--_error-pressed-state-layer-color),
|
|
pressed-opacity: var(--_error-pressed-state-layer-opacity),
|
|
)
|
|
);
|
|
}
|
|
|
|
// Icon and icon marks
|
|
|
|
.icon {
|
|
// The icon is created with two <rect> marks for animation:
|
|
// 1. Short end
|
|
// - the smaller leading part of the checkmark
|
|
// - hidden behind long end for indeterminate mark
|
|
// 2. Long end
|
|
// - the larger trailing part of the checkmark
|
|
// - the entirety of the indeterminate mark
|
|
fill: var(--_selected-icon-color);
|
|
height: var(--_icon-size);
|
|
width: var(--_icon-size);
|
|
}
|
|
|
|
// The short end of the checkmark. Initially hidden underneath the
|
|
// indeterminate mark.
|
|
.mark.short {
|
|
height: $_mark-stroke;
|
|
transition-property: transform, height;
|
|
width: $_mark-stroke;
|
|
}
|
|
|
|
// The long end of the checkmark. Initially the indeterminate mark.
|
|
.mark.long {
|
|
height: $_mark-stroke;
|
|
transition-property: transform, width;
|
|
width: 10px;
|
|
}
|
|
|
|
// Exit duration and easing.
|
|
.mark {
|
|
animation-duration: 150ms;
|
|
animation-timing-function: map.get(
|
|
$_md-sys-motion,
|
|
easing-emphasized-accelerate
|
|
);
|
|
transition-duration: 150ms;
|
|
transition-timing-function: map.get(
|
|
$_md-sys-motion,
|
|
easing-emphasized-accelerate
|
|
);
|
|
}
|
|
|
|
// Enter duration and easing.
|
|
.selected .mark {
|
|
animation-duration: 350ms;
|
|
animation-timing-function: map.get(
|
|
$_md-sys-motion,
|
|
easing-emphasized-decelerate
|
|
);
|
|
transition-duration: 350ms;
|
|
transition-timing-function: map.get(
|
|
$_md-sys-motion,
|
|
easing-emphasized-decelerate
|
|
);
|
|
}
|
|
|
|
// Creates the checkmark icon.
|
|
.checked,
|
|
// Keep the checkmark shape when unselecting a checked checkbox.
|
|
.prev-checked.unselected {
|
|
.mark {
|
|
// Transform from the bottom left of the rectangles, whch turn into the
|
|
// bottom-most point of the checkmark.
|
|
// Fix Safari's transform-origin bug from "top left" to "bottom left"
|
|
$scale: scaleY(-1);
|
|
// Move the "bottom left" corner to the checkmark location.
|
|
$translate: translate($_checkmark-bottom-left);
|
|
// Rotate the checkmark.
|
|
$rotate: rotate(45deg);
|
|
transform: $scale $translate $rotate;
|
|
}
|
|
|
|
.mark.short {
|
|
// The right triangle that forms the short end has an X, Y length of
|
|
// 4dp, 4dp. The hypoteneuse is √(4*4 + 4*4), which is the length of the
|
|
// short end when checked.
|
|
height: 1px * math.sqrt(32);
|
|
}
|
|
|
|
.mark.long {
|
|
// The right triangle that forms the long end has an X, Y length of
|
|
// 8dp, 8dp. The hypoteneuse is √(8*8 + 8*8), which is the length of the
|
|
// long end when checked.
|
|
width: 1px * math.sqrt(128);
|
|
}
|
|
}
|
|
|
|
// Creates the indeterminate icon.
|
|
.indeterminate,
|
|
// Keep the indeterminate shape when unselecting an indeterminate checkbox.
|
|
.prev-indeterminate.unselected {
|
|
.mark {
|
|
transform: scaleY(-1) translate($_indeterminate-bottom-left) rotate(0deg);
|
|
}
|
|
}
|
|
|
|
// When selecting an unselected checkbox, don't transition between the
|
|
// checked and indeterminate states. The checkmark icon or indeterminate icon
|
|
// should fade in from its final position.
|
|
.prev-unselected .mark {
|
|
transition-property: none;
|
|
}
|
|
|
|
// When checking a checkbox, the long mark of the checkmark grows from the
|
|
// bottom-most point of the checkmark. An animation provides the starting
|
|
// width to animate from.
|
|
.prev-unselected.checked .mark.long {
|
|
animation-name: prev-unselected-to-checked;
|
|
}
|
|
@keyframes prev-unselected-to-checked {
|
|
from {
|
|
width: 0;
|
|
}
|
|
}
|
|
|
|
// States
|
|
|
|
.error .outline {
|
|
border-color: var(--_unselected-error-outline-color);
|
|
// TODO(b/262410085): add once token is added
|
|
// border-width: var(--_unselected-error-outline-width);
|
|
}
|
|
|
|
.error .background {
|
|
background: var(--_selected-error-container-color);
|
|
}
|
|
|
|
.error .icon {
|
|
fill: var(--_selected-error-icon-color);
|
|
}
|
|
|
|
:host(:hover) .outline {
|
|
border-color: var(--_unselected-hover-outline-color);
|
|
border-width: var(--_unselected-hover-outline-width);
|
|
}
|
|
|
|
:host(:hover) .background {
|
|
background: var(--_selected-hover-container-color);
|
|
}
|
|
|
|
:host(:hover) .icon {
|
|
fill: var(--_selected-hover-icon-color);
|
|
}
|
|
|
|
:host(:hover) .error .outline {
|
|
border-color: var(--_unselected-error-hover-outline-color);
|
|
}
|
|
|
|
:host(:hover) .error .background {
|
|
background: var(--_selected-error-hover-container-color);
|
|
}
|
|
|
|
:host(:hover) .error .icon {
|
|
fill: var(--_selected-error-hover-icon-color);
|
|
}
|
|
|
|
:host(:focus-within) .outline {
|
|
border-color: var(--_unselected-focus-outline-color);
|
|
border-width: var(--_unselected-focus-outline-width);
|
|
}
|
|
|
|
:host(:focus-within) .background {
|
|
background: var(--_selected-focus-container-color);
|
|
}
|
|
|
|
:host(:focus-within) .icon {
|
|
fill: var(--_selected-focus-icon-color);
|
|
}
|
|
|
|
:host(:focus-within) .error .outline {
|
|
border-color: var(--_unselected-error-focus-outline-color);
|
|
}
|
|
|
|
:host(:focus-within) .error .background {
|
|
background: var(--_selected-error-focus-container-color);
|
|
}
|
|
|
|
:host(:focus-within) .error .icon {
|
|
fill: var(--_selected-error-focus-icon-color);
|
|
}
|
|
|
|
:host(:active) .outline {
|
|
border-color: var(--_unselected-pressed-outline-color);
|
|
border-width: var(--_unselected-pressed-outline-width);
|
|
}
|
|
|
|
:host(:active) .background {
|
|
background: var(--_selected-pressed-container-color);
|
|
}
|
|
|
|
:host(:active) .icon {
|
|
fill: var(--_selected-pressed-icon-color);
|
|
}
|
|
|
|
:host(:active) .error .outline {
|
|
border-color: var(--_unselected-error-pressed-outline-color);
|
|
}
|
|
|
|
:host(:active) .error .background {
|
|
background: var(--_selected-error-pressed-container-color);
|
|
}
|
|
|
|
:host(:active) .error .icon {
|
|
fill: var(--_selected-error-pressed-icon-color);
|
|
}
|
|
|
|
// Don't animate to/from disabled states because the outline is hidden when
|
|
// selected. Without this, there'd be a FOUC if the checkbox state is
|
|
// programmatically changed while disabled.
|
|
:host([disabled]),
|
|
.prev-disabled {
|
|
.background,
|
|
.icon,
|
|
.mark {
|
|
animation-duration: 0s;
|
|
transition-duration: 0s;
|
|
}
|
|
}
|
|
|
|
:host([disabled]) .outline {
|
|
border-color: var(--_unselected-disabled-outline-color);
|
|
border-width: var(--_unselected-disabled-outline-width);
|
|
opacity: var(--_unselected-disabled-container-opacity);
|
|
}
|
|
|
|
:host([disabled]) .selected .outline {
|
|
// Hide the outline behind the transparent selected container color.
|
|
// This can be removed once disabled colors are flattened.
|
|
visibility: hidden;
|
|
}
|
|
|
|
:host([disabled]) .selected .background {
|
|
// Set disabled opacity only when selected since opacity is used to show
|
|
// or hide the container background.
|
|
background: var(--_selected-disabled-container-color);
|
|
opacity: var(--_selected-disabled-container-opacity);
|
|
}
|
|
|
|
:host([disabled]) .icon {
|
|
fill: var(--_selected-disabled-icon-color);
|
|
}
|
|
}
|
|
|
|
@function _resolve-tokens($tokens) {
|
|
// Remove deprecated tokens
|
|
$tokens: map.remove(
|
|
$tokens,
|
|
'disabled-unselected-icon-color',
|
|
'disabled-unselected-icon-opacity',
|
|
'disabled-selected-icon-color',
|
|
'disabled-selected-icon-opacity',
|
|
'unselected-icon-color',
|
|
'unselected-focus-icon-color',
|
|
'unselected-hover-icon-color',
|
|
'unselected-pressed-icon-color'
|
|
);
|
|
// Remove unsupported tokens
|
|
$tokens: map.remove(
|
|
$tokens,
|
|
'selected-disabled-container-outline-width',
|
|
'selected-focus-outline-width',
|
|
'selected-hover-outline-width',
|
|
'selected-outline-width',
|
|
'selected-pressed-outline-width'
|
|
);
|
|
@return $tokens;
|
|
}
|