Elliott Marquez 06909e5b20 refactor(checkbox): move cursor styles to host
related: #5066 #5069
PiperOrigin-RevId: 574302352
2023-10-17 16:48:36 -07:00

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;
}
}
}