mirror of
https://github.com/material-components/material-web.git
synced 2026-03-09 00:09:23 +08:00
423 lines
11 KiB
SCSS
423 lines
11 KiB
SCSS
//
|
|
// Copyright 2022 Google LLC
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
// go/keep-sorted start
|
|
@use 'sass:list';
|
|
@use 'sass:map';
|
|
@use 'sass:math';
|
|
// go/keep-sorted end
|
|
// go/keep-sorted start
|
|
@use '../../ripple/ripple';
|
|
@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) {
|
|
$supported-tokens: list.join(
|
|
tokens.$md-comp-checkbox-supported-tokens,
|
|
(
|
|
'container-shape-start-start',
|
|
'container-shape-start-end',
|
|
'container-shape-end-end',
|
|
'container-shape-end-start'
|
|
)
|
|
);
|
|
|
|
@each $token, $value in $tokens {
|
|
@if list.index($supported-tokens, $token) == null {
|
|
@error 'Token `#{$token}` is not a supported token.';
|
|
}
|
|
|
|
@if $value {
|
|
--md-checkbox-#{$token}: #{$value};
|
|
}
|
|
}
|
|
}
|
|
|
|
@mixin styles() {
|
|
$tokens: tokens.md-comp-checkbox-values();
|
|
|
|
:host {
|
|
@each $token, $value in $tokens {
|
|
--_#{$token}: var(--md-checkbox-#{$token}, #{$value});
|
|
}
|
|
|
|
// Support logical shape properties
|
|
--_container-shape-start-start: var(
|
|
--md-checkbox-container-shape-start-start,
|
|
var(--_container-shape)
|
|
);
|
|
--_container-shape-start-end: var(
|
|
--md-checkbox-container-shape-start-end,
|
|
var(--_container-shape)
|
|
);
|
|
--_container-shape-end-end: var(
|
|
--md-checkbox-container-shape-end-end,
|
|
var(--_container-shape)
|
|
);
|
|
--_container-shape-end-start: var(
|
|
--md-checkbox-container-shape-end-start,
|
|
var(--_container-shape)
|
|
);
|
|
|
|
border-start-start-radius: var(--_container-shape-start-start);
|
|
border-start-end-radius: var(--_container-shape-start-end);
|
|
border-end-end-radius: var(--_container-shape-end-end);
|
|
border-end-start-radius: var(--_container-shape-end-start);
|
|
display: inline-flex;
|
|
height: var(--_container-size);
|
|
position: relative;
|
|
vertical-align: top; // Fix extra space when placed inside display: block
|
|
width: var(--_container-size);
|
|
-webkit-tap-highlight-color: transparent;
|
|
cursor: pointer;
|
|
}
|
|
|
|
:host([disabled]) {
|
|
cursor: default;
|
|
}
|
|
|
|
:host([touch-target='wrapper']) {
|
|
margin: max(0px, ((48px - var(--_container-size)) / 2));
|
|
}
|
|
|
|
md-focus-ring {
|
|
height: 44px;
|
|
inset: unset;
|
|
width: 44px;
|
|
}
|
|
|
|
// <input> is also the touch target
|
|
input {
|
|
appearance: none;
|
|
height: 48px;
|
|
margin: 0;
|
|
opacity: 0;
|
|
outline: none;
|
|
position: absolute;
|
|
width: 48px;
|
|
z-index: 1;
|
|
cursor: inherit;
|
|
}
|
|
|
|
:host([touch-target='none']) input {
|
|
height: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
.container {
|
|
border-radius: inherit;
|
|
display: flex;
|
|
height: 100%;
|
|
place-content: center;
|
|
place-items: center;
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
|
|
.outline,
|
|
.background,
|
|
.icon {
|
|
inset: 0;
|
|
position: absolute;
|
|
}
|
|
|
|
.outline,
|
|
.background {
|
|
border-radius: inherit;
|
|
}
|
|
|
|
.outline {
|
|
border-color: var(--_outline-color);
|
|
border-style: solid;
|
|
border-width: var(--_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%.
|
|
}
|
|
|
|
:where(.selected) :is(.background, .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);
|
|
}
|
|
|
|
// Ripple
|
|
|
|
md-ripple {
|
|
border-radius: var(--_state-layer-shape);
|
|
height: var(--_state-layer-size);
|
|
inset: unset;
|
|
width: var(--_state-layer-size);
|
|
|
|
@include ripple.theme(
|
|
(
|
|
hover-color: var(--_hover-state-layer-color),
|
|
hover-opacity: var(--_hover-state-layer-opacity),
|
|
pressed-color: var(--_pressed-state-layer-color),
|
|
pressed-opacity: var(--_pressed-state-layer-opacity),
|
|
)
|
|
);
|
|
}
|
|
|
|
.selected md-ripple {
|
|
@include ripple.theme(
|
|
(
|
|
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),
|
|
)
|
|
);
|
|
}
|
|
|
|
// 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
|
|
|
|
:where(:hover) .outline {
|
|
border-color: var(--_hover-outline-color);
|
|
border-width: var(--_hover-outline-width);
|
|
}
|
|
|
|
:where(:hover) .background {
|
|
background: var(--_selected-hover-container-color);
|
|
}
|
|
|
|
:where(:hover) .icon {
|
|
fill: var(--_selected-hover-icon-color);
|
|
}
|
|
|
|
:where(:focus-within) .outline {
|
|
border-color: var(--_focus-outline-color);
|
|
border-width: var(--_focus-outline-width);
|
|
}
|
|
|
|
:where(:focus-within) .background {
|
|
background: var(--_selected-focus-container-color);
|
|
}
|
|
|
|
:where(:focus-within) .icon {
|
|
fill: var(--_selected-focus-icon-color);
|
|
}
|
|
|
|
:where(:active) .outline {
|
|
border-color: var(--_pressed-outline-color);
|
|
border-width: var(--_pressed-outline-width);
|
|
}
|
|
|
|
:where(:active) .background {
|
|
background: var(--_selected-pressed-container-color);
|
|
}
|
|
|
|
:where(:active) .icon {
|
|
fill: var(--_selected-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.
|
|
:where(.disabled, .prev-disabled) :is(.background, .icon, .mark) {
|
|
animation-duration: 0s;
|
|
transition-duration: 0s;
|
|
}
|
|
|
|
:where(.disabled) .outline {
|
|
border-color: var(--_disabled-outline-color);
|
|
border-width: var(--_disabled-outline-width);
|
|
opacity: var(--_disabled-container-opacity);
|
|
}
|
|
|
|
:where(.selected.disabled) .outline {
|
|
// Hide the outline behind the transparent selected container color.
|
|
// This can be removed once disabled colors are flattened.
|
|
visibility: hidden;
|
|
}
|
|
|
|
:where(.selected.disabled) .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);
|
|
}
|
|
|
|
:where(.disabled) .icon {
|
|
fill: var(--_selected-disabled-icon-color);
|
|
}
|
|
|
|
@media (forced-colors: active) {
|
|
.background {
|
|
background-color: CanvasText;
|
|
}
|
|
|
|
.selected.disabled .background {
|
|
background-color: GrayText;
|
|
opacity: 1;
|
|
}
|
|
|
|
.outline {
|
|
border-color: CanvasText;
|
|
}
|
|
|
|
.disabled .outline {
|
|
border-color: GrayText;
|
|
opacity: 1;
|
|
}
|
|
|
|
.icon {
|
|
fill: Canvas;
|
|
}
|
|
}
|
|
}
|