From 27f7ea89cee0d1a31e50b81e5db994b97bba7748 Mon Sep 17 00:00:00 2001 From: Elizabeth Mitchell Date: Tue, 13 Dec 2022 11:58:34 -0800 Subject: [PATCH] feat(checkbox): refactor and simplify rendering/style logic BREAKING CHANGE: Removed reducedTouchTarget. Instead, set the width and height on the checkbox. PiperOrigin-RevId: 495087302 --- checkbox/_checkbox.scss | 7 +- checkbox/checkbox.ts | 8 +- checkbox/checkbox_test.ts | 17 + checkbox/harness.ts | 2 +- checkbox/lib/_checkbox-theme.scss | 532 ------------------- checkbox/lib/_checkbox.scss | 696 +++++++++++++------------ checkbox/lib/checkbox-styles.scss | 9 +- checkbox/lib/checkbox.ts | 268 +++------- checkbox/lib/checkbox_test.ts | 58 +-- checkbox/lib/forced-colors-styles.scss | 47 ++ 10 files changed, 546 insertions(+), 1098 deletions(-) create mode 100644 checkbox/checkbox_test.ts delete mode 100644 checkbox/lib/_checkbox-theme.scss create mode 100644 checkbox/lib/forced-colors-styles.scss diff --git a/checkbox/_checkbox.scss b/checkbox/_checkbox.scss index 3ac881704e..43e61c10e5 100644 --- a/checkbox/_checkbox.scss +++ b/checkbox/_checkbox.scss @@ -1 +1,6 @@ -@forward './lib/checkbox-theme' show theme, theme-extension; +// +// Copyright 2022 Google LLC +// SPDX-License-Identifier: Apache-2.0 +// + +@forward './lib/checkbox' show theme; diff --git a/checkbox/checkbox.ts b/checkbox/checkbox.ts index 7c8ebeac70..92d5ab9398 100644 --- a/checkbox/checkbox.ts +++ b/checkbox/checkbox.ts @@ -8,6 +8,7 @@ import {customElement} from 'lit/decorators.js'; import {Checkbox} from './lib/checkbox.js'; import {styles} from './lib/checkbox-styles.css.js'; +import {styles as forcedColorsStyles} from './lib/forced-colors-styles.css.js'; declare global { interface HTMLElementTagNameMap { @@ -15,8 +16,11 @@ declare global { } } -/** @soyCompatible */ +/** + * Checkboxes allow users to select one or more items from a set. Checkboxes can + * turn an option on or off. + */ @customElement('md-checkbox') export class MdCheckbox extends Checkbox { - static override styles = [styles]; + static override styles = [styles, forcedColorsStyles]; } diff --git a/checkbox/checkbox_test.ts b/checkbox/checkbox_test.ts new file mode 100644 index 0000000000..f80312ebbe --- /dev/null +++ b/checkbox/checkbox_test.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// import 'jasmine'; (google3-only) + +import {createTokenTests} from '../testing/tokens.js'; + +import {MdCheckbox} from './checkbox.js'; + +describe('', () => { + describe('.styles', () => { + createTokenTests(MdCheckbox.styles); + }); +}); diff --git a/checkbox/harness.ts b/checkbox/harness.ts index c985a33e97..f578569405 100644 --- a/checkbox/harness.ts +++ b/checkbox/harness.ts @@ -14,6 +14,6 @@ import {Checkbox} from './lib/checkbox.js'; export class CheckboxHarness extends Harness { override async getInteractiveElement() { await this.element.updateComplete; - return this.element.renderRoot.querySelector('input') as HTMLInputElement; + return this.element.renderRoot.querySelector('input')!; } } diff --git a/checkbox/lib/_checkbox-theme.scss b/checkbox/lib/_checkbox-theme.scss deleted file mode 100644 index 5ebab94af8..0000000000 --- a/checkbox/lib/_checkbox-theme.scss +++ /dev/null @@ -1,532 +0,0 @@ -// -// Copyright 2021 Google LLC -// SPDX-License-Identifier: Apache-2.0 -// - -// stylelint-disable selector-class-pattern -- -// Selector '.md3-*' should only be used in this project. - -@use 'sass:map'; - -@use '../../ripple/ripple-theme'; -@use '../../sass/color'; -@use '../../sass/theme'; -@use '../../tokens'; - -$custom-property-prefix: 'checkbox'; - -@mixin theme($theme) { - $theme: theme.validate-theme(tokens.md-comp-checkbox-values(), $theme); - @include theme.emit-theme-vars( - theme.create-theme-vars($theme, $custom-property-prefix) - ); -} - -@mixin theme-styles($theme) { - $theme: theme.validate-theme(tokens.md-comp-checkbox-values(), $theme); - $theme: _flatten-disable-colors($theme); - // Set touch target manually until tokens provide this information. - $theme: map.set($theme, _touch-target-size, 48px); - $theme: theme.create-theme-vars($theme, $prefix: $custom-property-prefix); - - @include _theme-styles-internal($theme); -} - -$_theme-extension-keys: ( - touch-target-size: null, -); - -@mixin theme-extension($theme) { - $theme: theme.validate-theme($_theme-extension-keys, $theme); - - .md3-checkbox { - @include _touch-target(map.get($theme, touch-target-size)); - } -} - -@mixin high-contrast-styles() { - $high-contrast-theme: ( - selected-container-color: CanvasText, - selected-hover-container-color: CanvasText, - selected-focus-container-color: CanvasText, - selected-pressed-container-color: CanvasText, - selected-disabled-container-color: GrayText, - selected-icon-color: Canvas, - selected-hover-icon-color: Canvas, - selected-focus-icon-color: Canvas, - selected-pressed-icon-color: Canvas, - selected-disabled-icon-color: Canvas, - unselected-outline-color: CanvasText, - unselected-hover-outline-color: CanvasText, - unselected-focus-outline-color: CanvasText, - unselected-pressed-outline-color: CanvasText, - unselected-disabled-outline-color: GrayText, - ); - $high-contrast-theme: theme.validate-theme( - tokens.md-comp-checkbox-values(), - $high-contrast-theme - ); - - @media (forced-colors: active) { - @include _theme-styles-internal($high-contrast-theme); - } -} - -@mixin _theme-styles-internal($theme) { - .md3-checkbox { - @include _icon-size(map.get($theme, icon-size)); - @include _container-height(map.get($theme, container-height)); - @include _container-width(map.get($theme, container-width)); - @include _container-shape(map.get($theme, container-shape)); - @include _unselected-outline-color( - ( - default: map.get($theme, unselected-outline-color), - hover: map.get($theme, unselected-hover-outline-color), - focus: map.get($theme, unselected-focus-outline-color), - pressed: map.get($theme, unselected-pressed-outline-color), - disabled: map.get($theme, unselected-disabled-outline-color), - ) - ); - @include _selected-container-color( - ( - default: map.get($theme, selected-container-color), - hover: map.get($theme, selected-hover-container-color), - focus: map.get($theme, selected-focus-container-color), - pressed: map.get($theme, selected-pressed-container-color), - disabled: map.get($theme, selected-disabled-container-color), - ) - ); - @include _animation-styles( - map.get($theme, unselected-outline-color), - map.get($theme, selected-container-color) - ); - @include _selected-outline-width( - ( - default: map.get($theme, selected-outline-width), - hover: map.get($theme, selected-hover-outline-width), - focus: map.get($theme, selected-focus-outline-width), - pressed: map.get($theme, selected-pressed-outline-width), - disabled: map.get($theme, selected-disabled-container-outline-width), - ) - ); - @include _unselected-outline-width( - ( - default: map.get($theme, unselected-outline-width), - hover: map.get($theme, unselected-hover-outline-width), - focus: map.get($theme, unselected-focus-outline-width), - pressed: map.get($theme, unselected-pressed-outline-width), - disabled: map.get($theme, unselected-disabled-outline-width), - ) - ); - @include _selected-icon-color( - ( - default: map.get($theme, selected-icon-color), - hover: map.get($theme, selected-hover-icon-color), - focus: map.get($theme, selected-focus-icon-color), - pressed: map.get($theme, selected-pressed-icon-color), - disabled: map.get($theme, selected-disabled-icon-color), - ) - ); - @include _state-layer-size(map.get($theme, state-layer-size)); - @include _touch-target(map.get($theme, state-layer-size)); - } - - .md3-checkbox--touch { - @include _touch-target(map.get($theme, _touch-target-size)); - } - - @include ripple-theme.theme( - ( - hover-state-layer-color: - map.get($theme, unselected-hover-state-layer-color), - focus-state-layer-color: - map.get($theme, unselected-focus-state-layer-color), - pressed-state-layer-color: - map.get($theme, unselected-pressed-state-layer-color), - hover-state-layer-opacity: - map.get($theme, unselected-hover-state-layer-opacity), - focus-state-layer-opacity: - map.get($theme, unselected-focus-state-layer-opacity), - pressed-state-layer-opacity: - map.get($theme, unselected-pressed-state-layer-opacity), - ) - ); - - @include _if-selected() { - .md3-checkbox__ripple { - @include ripple-theme.theme( - ( - hover-state-layer-color: - map.get($theme, selected-hover-state-layer-color), - focus-state-layer-color: - map.get($theme, selected-focus-state-layer-color), - pressed-state-layer-color: - map.get($theme, selected-pressed-state-layer-color), - hover-state-layer-opacity: - map.get($theme, selected-hover-state-layer-opacity), - focus-state-layer-opacity: - map.get($theme, selected-focus-state-layer-opacity), - pressed-state-layer-opacity: - map.get($theme, selected-pressed-state-layer-opacity), - ) - ); - } - } -} - -@function _flatten-disable-colors($theme) { - @return color.join-color-and-opacity-pairs( - $theme, - ( - ( - color-key: disabled-selected-icon-color, - opacity-key: disabled-selected-icon-opacity - ), - ( - color-key: disabled-unselected-icon-color, - opacity-key: disabled-unselected-icon-opacity - ), - ( - color-key: selected-disabled-container-color, - opacity-key: selected-disabled-container-opacity - ), - ( - color-key: unselected-disabled-outline-color, - opacity-key: unselected-disabled-container-opacity - ) - ) - ); -} - -@mixin _container-shape($shape) { - .md3-checkbox__background { - border-radius: $shape; - } -} - -@mixin _icon-size($size) { - @include _container-height($size); - @include _container-width($size); -} - -@mixin _selected-icon-color($color) { - // NOTE: Target checkmark when checkbox is selected and unselected so that the - // checkmark will have the same color during checked to uncheck animation. - .md3-checkbox__native-control ~ { - @include _checkmark-color(map.get($color, default)); - } - - .md3-checkbox__native-control:hover ~ { - @include _checkmark-color(map.get($color, hover)); - } - - .md3-checkbox__native-control:focus ~ { - @include _checkmark-color(map.get($color, focus)); - } - - .md3-checkbox__native-control:active ~ { - @include _checkmark-color(map.get($color, pressed)); - } - - .md3-checkbox__native-control:disabled ~ { - @include _checkmark-color(map.get($color, disabled)); - } -} - -@mixin _unselected-outline-width($width) { - @include _if-unselected { - @include _set-unselected-outline-width(map.get($width, default)); - } - - @include _if-unselected-hover { - @include _set-unselected-outline-width(map.get($width, hover)); - } - - @include _if-unselected-focus { - @include _set-unselected-outline-width(map.get($width, focus)); - } - - @include _if-unselected-pressed { - @include _set-unselected-outline-width(map.get($width, pressed)); - } - - @include _if-unselected-disabled { - @include _set-unselected-outline-width(map.get($width, disabled)); - } -} - -@mixin _set-unselected-outline-width($width) { - .md3-checkbox__background { - border-width: $width; - } -} - -@mixin _selected-outline-width($width) { - @include _if-selected { - @include _set-selected-outline-width(map.get($width, default)); - } - - @include _if-selected-hover { - @include _set-selected-outline-width(map.get($width, hover)); - } - - @include _if-selected-focus { - @include _set-selected-outline-width(map.get($width, focus)); - } - - @include _if-selected-pressed { - @include _set-selected-outline-width(map.get($width, pressed)); - } - - @include _if-selected-disabled { - @include _set-selected-outline-width(map.get($width, disabled)); - } -} - -@mixin _set-selected-outline-width($width) { - .md3-checkbox__background { - border-width: $width; - } -} - -@mixin _unselected-outline-color($color) { - @include _if-unselected { - @include _outline-color(map.get($color, default)); - } - - @include _if-unselected-hover { - @include _outline-color(map.get($color, hover)); - } - - @include _if-unselected-focus { - @include _outline-color(map.get($color, focus)); - } - - @include _if-unselected-pressed { - @include _outline-color(map.get($color, pressed)); - } - - @include _if-unselected-disabled { - @include _outline-color(map.get($color, disabled)); - } -} - -@mixin _selected-container-color($color) { - @include _if-selected { - @include _container-color(map.get($color, default)); - @include _outline-color(map.get($color, default)); - } - - @include _if-selected-hover { - @include _container-color(map.get($color, hover)); - @include _outline-color(map.get($color, hover)); - } - - @include _if-selected-focus { - @include _container-color(map.get($color, focus)); - @include _outline-color(map.get($color, focus)); - } - - @include _if-selected-pressed { - @include _container-color(map.get($color, pressed)); - @include _outline-color(map.get($color, pressed)); - } - - @include _if-selected-disabled { - @include _container-color(map.get($color, disabled)); - @include _outline-color(map.get($color, disabled)); - } -} - -@mixin _container-height($height) { - .md3-checkbox__background { - height: $height; - } -} - -@mixin _container-width($width) { - .md3-checkbox__background { - width: $width; - } -} - -@mixin _state-layer-size($size) { - .md3-checkbox__ripple { - height: $size; - width: $size; - } -} - -@mixin _touch-target($size) { - height: $size; - width: $size; - - .md3-checkbox__native-control { - width: $size; - height: $size; - } -} - -@mixin _animation-styles($unselected-outline-color, $selected-container-color) { - @if ($unselected-outline-color or $selected-container-color) { - @if ( - $unselected-outline-color == null or $selected-container-color == null - ) { - @error 'Both unselected outline and selected container should be provided.'; - } - - @include _container-keyframes( - $from-outline-color: $unselected-outline-color, - $to-outline-color: $selected-container-color, - $from-container-color: transparent, - $to-container-color: $selected-container-color - ); - - .md3-checkbox--anim { - &-unchecked-checked, - &-unchecked-indeterminate { - .md3-checkbox__native-control:enabled ~ .md3-checkbox__background { - animation-name: md3-checkbox-fade-in-background; - } - } - - &-checked-unchecked, - &-indeterminate-unchecked { - .md3-checkbox__native-control:enabled ~ .md3-checkbox__background { - animation-name: md3-checkbox-fade-out-background; - } - } - } - } -} - -$_unselected-selector: ".md3-checkbox__native-control:not(:checked):not(:indeterminate):not([data-indeterminate='true'])"; -$_unselected-enabled-selector: '#{$_unselected-selector}:enabled'; - -@mixin _if-unselected { - #{$_unselected-enabled-selector} ~ { - @content; - } -} - -@mixin _if-unselected-hover { - #{$_unselected-enabled-selector}:hover ~ { - @content; - } -} - -@mixin _if-unselected-focus { - #{$_unselected-enabled-selector}:focus ~ { - @content; - } -} - -@mixin _if-unselected-pressed { - #{$_unselected-enabled-selector}:active ~ { - @content; - } -} - -@mixin _if-unselected-disabled { - #{$_unselected-selector}:disabled ~ { - @content; - } -} - -$_selected-selector: ".md3-checkbox__native-control:checked, .md3-checkbox__native-control:indeterminate, .md3-checkbox__native-control[data-indeterminate='true']"; - -@mixin _if-selected { - #{$_selected-selector} { - &:enabled ~ { - @content; - } - } -} - -@mixin _if-selected-hover { - #{$_selected-selector} { - &:enabled:hover ~ { - @content; - } - } -} - -@mixin _if-selected-focus { - #{$_selected-selector} { - &:enabled:focus ~ { - @content; - } - } -} - -@mixin _if-selected-pressed { - #{$_selected-selector} { - &:enabled:active ~ { - @content; - } - } -} - -@mixin _if-selected-disabled { - #{$_selected-selector} { - &:disabled ~ { - @content; - } - } -} - -@mixin _outline-color($color) { - .md3-checkbox__background { - border-color: $color; - } -} - -@mixin _container-color($color) { - .md3-checkbox__background { - background-color: $color; - } -} - -@mixin _checkmark-color($color) { - .md3-checkbox__background { - .md3-checkbox__checkmark { - color: $color; - } - - .md3-checkbox__mixedmark { - color: $color; - } - } -} - -@mixin _container-keyframes( - $from-outline-color, - $to-outline-color, - $from-container-color, - $to-container-color -) { - @keyframes md3-checkbox-fade-in-background { - 0% { - border-color: $from-outline-color; - background-color: $from-outline-color; - } - - 50% { - border-color: $to-outline-color; - background-color: $to-container-color; - } - } - - @keyframes md3-checkbox-fade-out-background { - 0%, - 80% { - border-color: $to-outline-color; - background-color: $to-outline-color; - } - - 100% { - border-color: $from-outline-color; - background-color: $from-container-color; - } - } -} diff --git a/checkbox/lib/_checkbox.scss b/checkbox/lib/_checkbox.scss index 0d580214b7..54ec737409 100644 --- a/checkbox/lib/_checkbox.scss +++ b/checkbox/lib/_checkbox.scss @@ -3,424 +3,456 @@ // SPDX-License-Identifier: Apache-2.0 // -// stylelint-disable selector-class-pattern -- -// Selector '.md3-*' should only be used in this project. - @use 'sass:math'; @use 'sass:map'; @use '../../focus/focus-ring'; -@use '../../motion/animation'; -@use './checkbox-theme'; +@use '../../ripple/ripple'; +@use '../../sass/theme'; @use '../../tokens'; -$_transition-duration: 90ms; -$_mark-stroke-size: 4px; -$_unselected-outline-width: 2px; -$_selected-checkmark-padding: 2.8px; - -// Manual calculation done on SVG -$_mark-path-length: 29.7833385 !default; -$_indeterminate-checked-easing-function: cubic-bezier(0.14, 0, 0, 1) !default; - -@function transition-enter( - $property, - $delay: 0ms, - $duration: $_transition-duration -) { - @return animation.deceleration($property, $duration, $delay); -} +// 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); -@function transition-exit( - $property, - $delay: 0ms, - $duration: $_transition-duration -) { - @return animation.sharp($property, $duration, $delay); + @include theme.emit-theme-vars($tokens); } -@mixin static-styles() { - @include _mark-keyframes(); - @include _animation(); +@mixin styles() { + $tokens: _resolve-tokens(tokens.md-comp-checkbox-values()); + $tokens: theme.create-theme-vars($tokens, checkbox); - .md3-checkbox { - @include _base(); + :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; } - .md3-checkbox__background { - @include _background(); + input { + appearance: none; + inset: 0; + margin: 0; + outline: none; + position: absolute; } - .md3-checkbox__checkmark { - @include _checkmark(); + .container { + border-radius: inherit; + height: 100%; + position: relative; + width: 100%; } - .md3-checkbox__checkmark-path { - @include _checkmark-path(); + // Center elements within the container. + .outline, + .background, + md-ripple, + .icon { + inset: 0; + margin: auto; + position: absolute; } - .md3-checkbox__mixedmark { - @include _mixedmark(); + .outline, + .background { + border-radius: inherit; + height: var(--_container-height); + width: var(--_container-width); } - .md3-checkbox__native-control:checked ~ .md3-checkbox__background, - .md3-checkbox__native-control:indeterminate ~ .md3-checkbox__background, - .md3-checkbox__native-control[data-indeterminate='true'] - ~ .md3-checkbox__background { - @include _background--marked(); + .outline { + border-color: var(--_unselected-outline-color); + border-style: solid; + border-width: var(--_unselected-outline-width); + box-sizing: border-box; + } - .md3-checkbox__checkmark-path { - @include _checkmark-path--marked(); - } + .background { + background-color: var(--_selected-container-color); } - .md3-checkbox__native-control { - @include _native-control(); + // 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%. } - .md3-checkbox--disabled, - .md3-checkbox__native-control:disabled { - @include _disabled(); + .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); } - .md3-checkbox__native-control:checked ~ .md3-checkbox__background { - .md3-checkbox__checkmark { - @include _checkmark--checked(); - } + // Focus ring - .md3-checkbox__mixedmark { - @include _mixedmark--checked(); - } + md-focus-ring { + @include focus-ring.theme( + ( + offset-vertical: -2px, + offset-horizontal: -2px, + shape: map.get(tokens.md-sys-shape-values(), corner-small), + ) + ); } - .md3-checkbox__native-control:indeterminate ~ .md3-checkbox__background, - .md3-checkbox__native-control[data-indeterminate='true'] - ~ .md3-checkbox__background { - .md3-checkbox__checkmark { - @include _checkmark--indeterminate(); - } - .md3-checkbox__mixedmark { - @include _mixedmark--indeterminate(); - } + // Ripple + + md-ripple { + height: var(--_state-layer-size); + width: var(--_state-layer-size); + + @include ripple.theme( + ( + focus-state-layer-color: var(--_unselected-focus-state-layer-color), + focus-state-layer-opacity: var(--_unselected-focus-state-layer-opacity), + hover-state-layer-color: var(--_unselected-hover-state-layer-color), + hover-state-layer-opacity: var(--_unselected-hover-state-layer-opacity), + pressed-state-layer-color: var(--_unselected-pressed-state-layer-color), + pressed-state-layer-opacity: + var(--_unselected-pressed-state-layer-opacity), + state-layer-shape: var(--_state-layer-shape), + ) + ); } - .md3-checkbox__ripple { - position: absolute; - display: inline-flex; - z-index: -1; + .selected md-ripple { + @include ripple.theme( + ( + focus-state-layer-color: var(--_selected-focus-state-layer-color), + focus-state-layer-opacity: var(--_selected-focus-state-layer-opacity), + hover-state-layer-color: var(--_selected-hover-state-layer-color), + hover-state-layer-opacity: var(--_selected-hover-state-layer-opacity), + pressed-state-layer-color: var(--_selected-pressed-state-layer-color), + pressed-state-layer-opacity: + var(--_selected-pressed-state-layer-opacity), + ) + ); } - @include focus-ring.theme( - ( - offset-vertical: -2px, - offset-horizontal: -2px, - shape: map.get(tokens.md-sys-shape-values(), corner-small), - ) - ); - @include checkbox-theme.high-contrast-styles(); -} - -@mixin _base() { - display: inline-flex; - position: relative; - line-height: 0; - white-space: nowrap; - cursor: pointer; - vertical-align: bottom; - z-index: 0; - align-items: center; - justify-content: center; - flex-direction: column; -} - -@mixin _disabled() { - cursor: default; - pointer-events: none; -} - -@mixin _animation() { - $md3-checkbox-indeterminate-change-duration_: 500ms; + .error md-ripple { + @include ripple.theme( + ( + focus-state-layer-color: var(--_error-focus-state-layer-color), + // TODO(b/262409702): add once token is added + // focus-state-layer-opacity: var(--_error-focus-state-layer-opacity), + hover-state-layer-color: var(--_error-hover-state-layer-color), + hover-state-layer-opacity: var(--_error-hover-state-layer-opacity), + pressed-state-layer-color: var(--_error-pressed-state-layer-color), + pressed-state-layer-opacity: var(--_error-pressed-state-layer-opacity), + ) + ); + } - .md3-checkbox--anim-unchecked-checked, - .md3-checkbox--anim-unchecked-indeterminate, - .md3-checkbox--anim-checked-unchecked, - .md3-checkbox--anim-indeterminate-unchecked { - .md3-checkbox__background { - animation-duration: $_transition-duration * 2; - animation-timing-function: linear; - } + // Icon and icon marks + + .icon { + // The icon is created with two 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); } - // stylelint-disable no-unknown-animations -- Asterisk is treated as - // animation name. Supress the lint warning. - .md3-checkbox--anim-unchecked-checked { - .md3-checkbox__checkmark-path { - // Instead of delaying the animation, we simply multiply its length by 2 and begin the - // animation at 50% in order to prevent a flash of styles applied to a checked checkmark - // as the background is fading in before the animation begins. - animation: md3-checkbox-unchecked-checked-checkmark-path - $_transition-duration * 2 linear 0s; - transition: none; - } + // The short end of the checkmark. Initially hidden underneath the + // indeterminate mark. + .mark.short { + height: $_mark-stroke; + transition-property: transform, height; + width: $_mark-stroke; } - .md3-checkbox--anim-unchecked-indeterminate { - .md3-checkbox__mixedmark { - animation: md3-checkbox-unchecked-indeterminate-mixedmark - $_transition-duration linear 0s; - transition: none; - } + // The long end of the checkmark. Initially the indeterminate mark. + .mark.long { + height: $_mark-stroke; + transition-property: transform, width; + width: 10px; } - .md3-checkbox--anim-checked-unchecked { - .md3-checkbox__checkmark-path { - animation: md3-checkbox-checked-unchecked-checkmark-path - $_transition-duration linear 0s; - transition: none; - } + // 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 + ); } - .md3-checkbox--anim-checked-indeterminate { - .md3-checkbox__checkmark { - animation: md3-checkbox-checked-indeterminate-checkmark - $_transition-duration linear 0s; - transition: none; - } + // 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 + ); + } - .md3-checkbox__mixedmark { - animation: md3-checkbox-checked-indeterminate-mixedmark - $_transition-duration linear 0s; - transition: none; + // 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); } } - .md3-checkbox--anim-indeterminate-checked { - .md3-checkbox__checkmark { - animation: md3-checkbox-indeterminate-checked-checkmark - $md3-checkbox-indeterminate-change-duration_ linear 0s; - transition: none; + // 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); } + } - .md3-checkbox__mixedmark { - animation: md3-checkbox-indeterminate-checked-mixedmark - $md3-checkbox-indeterminate-change-duration_ linear 0s; - transition: none; - } + // 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; } - .md3-checkbox--anim-indeterminate-unchecked { - .md3-checkbox__mixedmark { - animation: md3-checkbox-indeterminate-unchecked-mixedmark - $md3-checkbox-indeterminate-change-duration_ * 0.6 linear 0s; - transition: 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; } } -} -@mixin _background() { - display: inline-flex; - position: absolute; - align-items: center; - justify-content: center; - box-sizing: border-box; - border-style: solid; - background-color: transparent; - pointer-events: none; - will-change: background-color, border-color; - - transition: transition-exit(background-color), transition-exit(border-color); -} - -@mixin _background--marked() { - transition: transition-enter(border-color), transition-enter(background-color); -} + // States -// Native input - -@mixin _native-control() { - position: absolute; - margin: 0; - padding: 0; - opacity: 0; - cursor: inherit; -} - -// Check mark - -@mixin _checkmark() { - padding: $_selected-checkmark-padding; - position: absolute; - inset: 0; - box-sizing: border-box; - width: 100%; - border: none; - transition: transition-exit(opacity, 0ms, $_transition-duration * 2); -} - -@mixin _checkmark--checked() { - transition: transition-enter(opacity, 0ms, $_transition-duration * 2), - transition-enter(transform, 0ms, $_transition-duration * 2); - - opacity: 1; -} - -@mixin _checkmark--indeterminate() { - transform: rotate(45deg); - opacity: 0; - - transition: transition-exit(opacity, 0ms, $_transition-duration), - transition-exit(transform, 0ms, $_transition-duration); -} + .error .outline { + border-color: var(--_unselected-error-outline-color); + // TODO(b/262410085): add once token is added + // border-width: var(--_unselected-error-outline-width); + } -// Check mark path + .error .background { + background: var(--_selected-error-container-color); + } -@mixin _checkmark-path() { - transition: transition-exit( - stroke-dashoffset, - 0ms, - $_transition-duration * 2 - ); + .error .icon { + fill: var(--_selected-error-icon-color); + } - stroke: currentColor; - stroke-width: $_mark-stroke-size; - stroke-dashoffset: $_mark-path-length; - stroke-dasharray: $_mark-path-length; -} + :host(:hover) .outline { + border-color: var(--_unselected-hover-outline-color); + border-width: var(--_unselected-hover-outline-width); + } -@mixin _checkmark-path--marked() { - stroke-dashoffset: 0; -} + :host(:hover) .background { + background: var(--_selected-hover-container-color); + } -// Mixed mark - -@mixin _mixedmark() { - width: 100%; - height: 0; - transform: scaleX(0) rotate(0deg); - border-width: math.div(math.floor($_mark-stroke-size), 4); - border-style: solid; - background-color: currentColor; - border-color: currentColor; - opacity: 0; - transition: transition-exit(opacity), transition-exit(transform); - margin: $_unselected-outline-width * 2; -} + :host(:hover) .icon { + fill: var(--_selected-hover-icon-color); + } -@mixin _mixedmark--checked() { - transform: scaleX(1) rotate(-45deg); -} + :host(:hover) .error .outline { + border-color: var(--_unselected-error-hover-outline-color); + border-width: var(--_unselected-error-hover-outline-width); + } -@mixin _mixedmark--indeterminate() { - transform: scaleX(1) rotate(0deg); - opacity: 1; -} + :host(:hover) .error .background { + background: var(--_selected-error-hover-container-color); + } -@mixin _mark-keyframes() { - @keyframes md3-checkbox-unchecked-checked-checkmark-path { - 0%, - 50% { - stroke-dashoffset: $_mark-path-length; - } + :host(:hover) .error .icon { + fill: var(--_selected-error-hover-icon-color); + } - 50% { - animation-timing-function: animation.$deceleration-easing; - } + :host(:focus-within) .outline { + border-color: var(--_unselected-focus-outline-color); + border-width: var(--_unselected-focus-outline-width); + } - 100% { - stroke-dashoffset: 0; - } + :host(:focus-within) .background { + background: var(--_selected-focus-container-color); } - @keyframes md3-checkbox-unchecked-indeterminate-mixedmark { - 0%, - 68.2% { - transform: scaleX(0); - } + :host(:focus-within) .icon { + fill: var(--_selected-focus-icon-color); + } - 68.2% { - animation-timing-function: cubic-bezier(0, 0, 0, 1); - } + :host(:focus-within) .error .outline { + border-color: var(--_unselected-error-focus-outline-color); + border-width: var(--_unselected-error-focus-outline-width); + } - 100% { - transform: scaleX(1); - } + :host(:focus-within) .error .background { + background: var(--_selected-error-focus-container-color); } - @keyframes md3-checkbox-checked-unchecked-checkmark-path { - from { - animation-timing-function: animation.$acceleration-easing; - opacity: 1; - stroke-dashoffset: 0; - } + :host(:focus-within) .error .icon { + fill: var(--_selected-error-focus-icon-color); + } - to { - opacity: 0; - stroke-dashoffset: $_mark-path-length * -1; - } + :host(:active) .outline { + border-color: var(--_unselected-pressed-outline-color); + border-width: var(--_unselected-pressed-outline-width); } - @keyframes md3-checkbox-checked-indeterminate-checkmark { - from { - animation-timing-function: animation.$deceleration-easing; - transform: rotate(0deg); - opacity: 1; - } + :host(:active) .background { + background: var(--_selected-pressed-container-color); + } - to { - transform: rotate(45deg); - opacity: 0; - } + :host(:active) .icon { + fill: var(--_selected-pressed-icon-color); } - @keyframes md3-checkbox-indeterminate-checked-checkmark { - from { - animation-timing-function: $_indeterminate-checked-easing-function; - transform: rotate(45deg); - opacity: 0; - } + :host(:active) .error .outline { + border-color: var(--_unselected-error-pressed-outline-color); + border-width: var(--_unselected-error-pressed-outline-width); + } - to { - transform: rotate(360deg); - opacity: 1; - } + :host(:active) .error .background { + background: var(--_selected-error-pressed-container-color); } - @keyframes md3-checkbox-checked-indeterminate-mixedmark { - from { - animation-timing-function: md3-animation-deceleration-curve-timing-function; - transform: rotate(-45deg); - opacity: 0; - } + :host(:active) .error .icon { + fill: var(--_selected-error-pressed-icon-color); + } - to { - transform: rotate(0deg); - opacity: 1; + // 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; } } - @keyframes md3-checkbox-indeterminate-checked-mixedmark { - from { - animation-timing-function: $_indeterminate-checked-easing-function; - transform: rotate(0deg); - opacity: 1; - } + :host([disabled]) .outline { + border-color: var(--_unselected-disabled-outline-color); + border-width: var(--_unselected-disabled-outline-width); + opacity: var(--_unselected-disabled-container-opacity); + } - to { - transform: rotate(315deg); - opacity: 0; - } + :host([disabled]) .selected .outline { + // Hide the outline behind the transparent selected container color. + // This can be removed once disabled colors are flattened. + visibility: hidden; } - @keyframes md3-checkbox-indeterminate-unchecked-mixedmark { - 0% { - animation-timing-function: linear; - transform: scaleX(1); - opacity: 1; - } + :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); + } - 32.8%, - 100% { - transform: scaleX(0); - opacity: 0; - } + :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-error-focus-outline-width', + 'selected-error-hover-outline-width', + 'selected-error-pressed-outline-width', + 'selected-focus-outline-width', + 'selected-hover-outline-width', + 'selected-outline-width', + 'selected-pressed-outline-width' + ); + @return $tokens; +} diff --git a/checkbox/lib/checkbox-styles.scss b/checkbox/lib/checkbox-styles.scss index 46444eabc7..f58dc44451 100644 --- a/checkbox/lib/checkbox-styles.scss +++ b/checkbox/lib/checkbox-styles.scss @@ -3,13 +3,6 @@ // SPDX-License-Identifier: Apache-2.0 // -@use '../../tokens'; @use './checkbox'; -@use './checkbox-theme'; -:host { - @include checkbox-theme.theme-styles(tokens.md-comp-checkbox-values()); - @include checkbox.static-styles(); - - display: inline-flex; -} +@include checkbox.styles; diff --git a/checkbox/lib/checkbox.ts b/checkbox/lib/checkbox.ts index afa9c6ea87..a85b5611ec 100644 --- a/checkbox/lib/checkbox.ts +++ b/checkbox/lib/checkbox.ts @@ -7,223 +7,121 @@ import '../../focus/focus-ring.js'; import '../../ripple/ripple.js'; -import {html, PropertyValues, TemplateResult} from 'lit'; -import {property, query, state} from 'lit/decorators.js'; +import {html, LitElement, nothing, PropertyValues, TemplateResult} from 'lit'; +import {property, queryAsync, state} from 'lit/decorators.js'; import {classMap} from 'lit/directives/class-map.js'; -import {ifDefined} from 'lit/directives/if-defined.js'; +import {when} from 'lit/directives/when.js'; -import {ActionElement, BeginPressConfig, EndPressConfig} from '../../actionelement/action-element.js'; +import {redispatchEvent} from '../../controller/events.js'; import {ariaProperty} from '../../decorators/aria-property.js'; import {pointerPress, shouldShowStrongFocus} from '../../focus/strong-focus.js'; +import {ripple} from '../../ripple/directive.js'; import {MdRipple} from '../../ripple/ripple.js'; -/** @soyCompatible */ -export class Checkbox extends ActionElement { - @query('input') protected formElement!: HTMLInputElement; - +/** + * A checkbox component. + */ +export class Checkbox extends LitElement { @property({type: Boolean, reflect: true}) checked = false; - - @property({type: Boolean, reflect: true}) indeterminate = false; - @property({type: Boolean, reflect: true}) disabled = false; - - @property({type: String, reflect: true}) name = ''; - - @property({type: String}) value = 'on'; + @property({type: Boolean, reflect: true}) error = false; + @property({type: Boolean, reflect: true}) indeterminate = false; @ariaProperty // tslint:disable-line:no-new-decorators @property({type: String, attribute: 'data-aria-label', noAccessor: true}) override ariaLabel!: string; - @ariaProperty // tslint:disable-line:no-new-decorators - @property({type: String, attribute: 'data-aria-labelledby', noAccessor: true}) - ariaLabelledBy?: string; - - @ariaProperty // tslint:disable-line:no-new-decorators - @property( - {type: String, attribute: 'data-aria-describedby', noAccessor: true}) - ariaDescribedBy?: string; - - /** - * Touch target extends beyond visual boundary of a component by default. - * Set to `true` to remove touch target added to the component. - * @see https://material.io/design/usability/accessibility.html - */ - @property({type: Boolean}) reducedTouchTarget = false; - - @state() protected animationClass = ''; - - @state() protected showFocusRing = false; - - @query('md-ripple') ripple!: MdRipple; - - protected override update(changedProperties: PropertyValues) { - const oldIndeterminate = changedProperties.get('indeterminate'); - const oldChecked = changedProperties.get('checked'); - const oldDisabled = changedProperties.get('disabled'); - if (oldIndeterminate !== undefined || oldChecked !== undefined || - oldDisabled !== undefined) { - const oldState = this.calculateAnimationStateName( - !!oldChecked, !!oldIndeterminate, !!oldDisabled); - const newState = this.calculateAnimationStateName( - this.checked, this.indeterminate, this.disabled); - this.animationClass = `${oldState}-${newState}`; + @state() private prevChecked = false; + @state() private prevDisabled = false; + @state() private prevIndeterminate = false; + @queryAsync('md-ripple') private readonly ripple!: Promise; + @state() private showFocusRing = false; + @state() private showRipple = false; + + protected override update(changed: PropertyValues) { + if (changed.has('checked') || changed.has('disabled') || + changed.has('indeterminate')) { + this.prevChecked = changed.get('checked') ?? this.checked; + this.prevDisabled = changed.get('disabled') ?? this.disabled; + this.prevIndeterminate = + changed.get('indeterminate') ?? this.indeterminate; } - super.update(changedProperties); - } - protected calculateAnimationStateName( - checked: boolean, indeterminate: boolean, disabled: boolean): string { - if (disabled) { - return 'disabled'; - } else if (indeterminate) { - return 'indeterminate'; - } else if (checked) { - return 'checked'; - } else { - return 'unchecked'; - } + super.update(changed); } - /** @soyTemplate */ - protected renderRipple(): TemplateResult { - return html``; - } - - /** @soyTemplate */ - protected renderFocusRing(): TemplateResult { - return html``; - } - - /** - * @soyTemplate - * @soyAttributes checkboxAttributes: input - * @soyClasses checkboxClasses: .md3-checkbox - */ protected override render(): TemplateResult { - /** @classMap */ - const classes = { - 'md3-checkbox--disabled': this.disabled, - 'md3-checkbox--touch': !this.reducedTouchTarget, - // transition animiation classes - 'md3-checkbox--anim-checked-indeterminate': - this.animationClass === 'checked-indeterminate', - 'md3-checkbox--anim-checked-unchecked': - this.animationClass === 'checked-unchecked', - 'md3-checkbox--anim-indeterminate-checked': - this.animationClass === 'indeterminate-checked', - 'md3-checkbox--anim-indeterminate-unchecked': - this.animationClass === 'indeterminate-unchecked', - 'md3-checkbox--anim-unchecked-checked': - this.animationClass === 'unchecked-checked', - 'md3-checkbox--anim-unchecked-indeterminate': - this.animationClass === 'unchecked-indeterminate', - }; - const ariaChecked = this.indeterminate ? 'mixed' : undefined; - return html` -
- ${this.renderFocusRing()} - -
- -
-
-
- ${this.renderRipple()} -
-
`; - } - - protected setFormData(formData: FormData) { - if (this.name && this.checked) { - formData.append(this.name, this.value); - } - } - - override beginPress({positionEvent}: BeginPressConfig) { - this.ripple.beginPress(positionEvent); - } - - override endPress({cancelled}: EndPressConfig) { - this.ripple.endPress(); - - if (cancelled) { - return; - } - - super.endPress({ - cancelled, - actionData: - {checked: this.formElement.checked, value: this.formElement.value} + const prevNone = !this.prevChecked && !this.prevIndeterminate; + const prevChecked = this.prevChecked && !this.prevIndeterminate; + const prevIndeterminate = this.prevIndeterminate; + const isChecked = this.checked && !this.indeterminate; + const isIndeterminate = this.indeterminate; + + const containerClasses = classMap({ + 'selected': isChecked || isIndeterminate, + 'unselected': !isChecked && !isIndeterminate, + 'checked': isChecked, + 'indeterminate': isIndeterminate, + 'error': this.error && !this.disabled, + 'prev-unselected': prevNone, + 'prev-checked': prevChecked, + 'prev-indeterminate': prevIndeterminate, + 'prev-disabled': this.prevDisabled, }); - } - protected handleFocus() { - this.showFocusRing = shouldShowStrongFocus(); + return html` +
+
+
+ + ${when(this.showRipple, this.renderRipple)} + + + + +
+ + `; } - protected handleBlur() { + private handleBlur() { this.showFocusRing = false; } - protected handlePointerEnter(e: PointerEvent) { - this.ripple.beginHover(e); - } + private handleChange(event: Event) { + const target = event.target as HTMLInputElement; + this.checked = target.checked; + this.indeterminate = target.indeterminate; - override handlePointerLeave(e: PointerEvent) { - super.handlePointerLeave(e); - this.ripple.endHover(); + redispatchEvent(this, event); } - override handlePointerDown(e: PointerEvent) { - super.handlePointerDown(e); + private handleFocus() { + this.showFocusRing = shouldShowStrongFocus(); + } + private handlePointerDown() { pointerPress(); this.showFocusRing = shouldShowStrongFocus(); } - protected handleChange() { - this.checked = this.formElement.checked; - this.indeterminate = this.formElement.indeterminate; - - this.dispatchEvent(new Event('change', { - bubbles: true, - composed: true, - })); - } + private readonly getRipple = () => { // bind to this + this.showRipple = true; + return this.ripple; + }; - protected resetAnimationClass() { - this.animationClass = ''; - } + private readonly renderRipple = () => { // bind to this + return html``; + }; } diff --git a/checkbox/lib/checkbox_test.ts b/checkbox/lib/checkbox_test.ts index b961788cc5..c987174457 100644 --- a/checkbox/lib/checkbox_test.ts +++ b/checkbox/lib/checkbox_test.ts @@ -5,21 +5,17 @@ */ import {html} from 'lit'; -import {customElement} from 'lit/decorators.js'; -import {MdFocusRing} from '../../focus/focus-ring.js'; import {Environment} from '../../testing/environment.js'; import {CheckboxHarness} from '../harness.js'; import {Checkbox} from './checkbox.js'; -@customElement('md-test-checkbox') -class TestCheckbox extends Checkbox { -} +customElements.define('md-test-checkbox', Checkbox); declare global { interface HTMLElementTagNameMap { - 'md-test-checkbox': TestCheckbox; + 'md-test-checkbox': Checkbox; } } @@ -39,8 +35,7 @@ describe('checkbox', () => { throw new Error('Could not query rendered .'); } - const focusRing = - element.renderRoot.querySelector('md-focus-ring'); + const focusRing = element.renderRoot.querySelector('md-focus-ring'); if (!focusRing) { throw new Error('Could not query rendered .'); } @@ -55,11 +50,13 @@ describe('checkbox', () => { describe('basic', () => { it('initializes as an checkbox', async () => { const {harness} = await setupTest(); - expect(harness.element).toBeInstanceOf(TestCheckbox); + expect(harness.element).toBeInstanceOf(Checkbox); expect(harness.element.checked).toEqual(false); expect(harness.element.indeterminate).toEqual(false); expect(harness.element.disabled).toEqual(false); - expect(harness.element.value).toEqual('on'); + expect(harness.element.error).toEqual(false); + // TODO(b/261219117): re-add with FormController + // expect(harness.element.value).toEqual('on'); }); it('user input updates checked state', async () => { @@ -80,21 +77,6 @@ describe('checkbox', () => { expect(changeHandler).toHaveBeenCalledTimes(1); expect(changeHandler).toHaveBeenCalledWith(jasmine.any(Event)); }); - - it('should trigger an event with event detail including checked status', - async () => { - const {harness} = await setupTest(); - const pressEndSpy = jasmine.createSpy('pressEndHandler'); - harness.element.addEventListener('action', pressEndSpy); - - await harness.clickWithMouse(); - await env.waitForStability(); - - expect(pressEndSpy).toHaveBeenCalledWith(jasmine.any(CustomEvent)); - expect(pressEndSpy).toHaveBeenCalledWith(jasmine.objectContaining({ - detail: jasmine.objectContaining({checked: true}), - })); - }); }); describe('checked', () => { @@ -165,18 +147,20 @@ describe('checkbox', () => { }); }); - describe('value', () => { - it('get/set updates the value of the native checkbox element', async () => { - const {harness, input} = await setupTest(); - harness.element.value = 'new value'; - await env.waitForStability(); - - expect(input.value).toEqual('new value'); - harness.element.value = 'new value 2'; - await env.waitForStability(); - expect(input.value).toEqual('new value 2'); - }); - }); + // TODO(b/261219117): re-add with FormController + // describe('value', () => { + // it('get/set updates the value of the native checkbox element', async () + // => { + // const {harness, input} = await setupTest(); + // harness.element.value = 'new value'; + // await env.waitForStability(); + + // expect(input.value).toEqual('new value'); + // harness.element.value = 'new value 2'; + // await env.waitForStability(); + // expect(input.value).toEqual('new value 2'); + // }); + // }); describe('focus ring', () => { it('hidden on non-keyboard focus', async () => { diff --git a/checkbox/lib/forced-colors-styles.scss b/checkbox/lib/forced-colors-styles.scss new file mode 100644 index 0000000000..bf1d597e23 --- /dev/null +++ b/checkbox/lib/forced-colors-styles.scss @@ -0,0 +1,47 @@ +// +// Copyright 2022 Google LLC +// SPDX-License-Identifier: Apache-2.0 +// + +@use './checkbox'; + +@media (forced-colors: active) { + :host { + $container-color: CanvasText; + $icon-color: Canvas; + $outline-color: CanvasText; + @include checkbox.theme( + ( + selected-container-color: $container-color, + selected-disabled-container-color: GrayText, + selected-disabled-container-opacity: 1, + selected-disabled-icon-color: $icon-color, + selected-error-container-color: $container-color, + selected-error-focus-container-color: $container-color, + selected-error-focus-icon-color: $icon-color, + selected-error-hover-container-color: $container-color, + selected-error-hover-icon-color: $icon-color, + selected-error-icon-color: $icon-color, + selected-error-pressed-container-color: $container-color, + selected-error-pressed-icon-color: $icon-color, + selected-focus-container-color: $container-color, + selected-focus-icon-color: $icon-color, + selected-hover-container-color: $container-color, + selected-hover-icon-color: $icon-color, + selected-icon-color: $icon-color, + selected-pressed-container-color: $container-color, + selected-pressed-icon-color: $icon-color, + unselected-disabled-container-opacity: 1, + unselected-disabled-outline-color: GrayText, + unselected-error-focus-outline-color: $outline-color, + unselected-error-hover-outline-color: $outline-color, + unselected-error-outline-color: $outline-color, + unselected-error-pressed-outline-color: $outline-color, + unselected-focus-outline-color: $outline-color, + unselected-hover-outline-color: $outline-color, + unselected-outline-color: $outline-color, + unselected-pressed-outline-color: $outline-color, + ) + ); + } +}