From 3b6cab01c7ff44c999ffcdc99d2ec9a2fb63e96a Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 27 Jan 2017 01:55:17 +0100 Subject: [PATCH] feat: add simplified checkbox component for usage in other components (#2619) * feat: add simplified checkbox component for usage in other components Adds the `md-pseudo-checkbox`, which is a simplified version of `md-checkbox`, that doesn't have the expensive SVG animations or the form control logic. This will be useful for multiple selection in `md-select`, as well as other components in the future. Relates to #2412. * Remove unnecessary template. * Fix linter error. * Address PR feedback. * Trailing comma. --- src/lib/checkbox/checkbox.scss | 43 ++++++-------- src/lib/core/_core.scss | 2 + src/lib/core/core.ts | 10 +++- src/lib/core/selection/index.ts | 10 ++++ .../_pseudo-checkbox-theme.scss | 44 ++++++++++++++ .../pseudo-checkbox/pseudo-checkbox.scss | 53 +++++++++++++++++ .../pseudo-checkbox/pseudo-checkbox.ts | 59 +++++++++++++++++++ src/lib/core/style/_checkbox-common.scss | 10 ++++ 8 files changed, 205 insertions(+), 26 deletions(-) create mode 100644 src/lib/core/selection/index.ts create mode 100644 src/lib/core/selection/pseudo-checkbox/_pseudo-checkbox-theme.scss create mode 100644 src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.scss create mode 100644 src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.ts create mode 100644 src/lib/core/style/_checkbox-common.scss diff --git a/src/lib/checkbox/checkbox.scss b/src/lib/checkbox/checkbox.scss index 213663d7123d..0dbb96babe47 100644 --- a/src/lib/checkbox/checkbox.scss +++ b/src/lib/checkbox/checkbox.scss @@ -1,26 +1,21 @@ @import '../core/theming/theming'; @import '../core/style/elevation'; -@import '../core/style/variables'; +@import '../core/style/checkbox-common'; @import '../core/ripple/ripple'; - -// The width/height of the checkbox element. -$md-checkbox-size: $md-toggle-size !default; -// The width of the line used to draw the checkmark / mixedmark. -$md-checkbox-mark-stroke-size: 2/15 * $md-checkbox-size !default; -// The width of the checkbox border shown when the checkbox is unchecked. -$md-checkbox-border-width: 2px; -// The base duration used for the majority of transitions for the checkbox. -$md-checkbox-transition-duration: 90ms; -// The amount of spacing between the checkbox and its label. -$md-checkbox-item-spacing: $md-toggle-padding; - // Manual calculation done on SVG $_md-checkbox-mark-path-length: 22.910259; $_md-checkbox-indeterminate-checked-easing-function: cubic-bezier(0.14, 0, 0, 1); // The ripple size of the checkbox -$md-checkbox-ripple-size: 15px; +$_md-checkbox-ripple-size: 15px; + +// The amount of spacing between the checkbox and its label. +$_md-checkbox-item-spacing: $md-toggle-padding; + +// The width of the line used to draw the checkmark / mixedmark. +$_md-checkbox-mark-stroke-size: 2 / 15 * $md-checkbox-size !default; + // Fades in the background of the checkbox when it goes from unchecked -> {checked,indeterminate}. @keyframes md-checkbox-fade-in-background { @@ -213,7 +208,7 @@ md-checkbox { height: $md-checkbox-size; line-height: 0; margin: auto; - margin-right: $md-checkbox-item-spacing; + margin-right: $_md-checkbox-item-spacing; order: 0; position: relative; vertical-align: middle; @@ -223,7 +218,7 @@ md-checkbox { [dir='rtl'] & { margin: { - left: $md-checkbox-item-spacing; + left: $_md-checkbox-item-spacing; right: auto; } } @@ -264,14 +259,14 @@ md-checkbox { stroke: { dashoffset: $_md-checkbox-mark-path-length; dasharray: $_md-checkbox-mark-path-length; - width: $md-checkbox-mark-stroke-size; + width: $_md-checkbox-mark-stroke-size; } } .md-checkbox-mixedmark { @extend %md-checkbox-mark; - height: floor($md-checkbox-mark-stroke-size); + height: floor($_md-checkbox-mark-stroke-size); opacity: 0; transform: scaleX(0) rotate(0deg); } @@ -280,14 +275,14 @@ md-checkbox { .md-checkbox-inner-container { order: 1; margin: { - left: $md-checkbox-item-spacing; + left: $_md-checkbox-item-spacing; right: auto; } [dir='rtl'] & { margin: { left: auto; - right: $md-checkbox-item-spacing; + right: $_md-checkbox-item-spacing; } } } @@ -419,10 +414,10 @@ md-checkbox { .md-checkbox-ripple { position: absolute; - left: -$md-checkbox-ripple-size; - top: -$md-checkbox-ripple-size; - right: -$md-checkbox-ripple-size; - bottom: -$md-checkbox-ripple-size; + left: -$_md-checkbox-ripple-size; + top: -$_md-checkbox-ripple-size; + right: -$_md-checkbox-ripple-size; + bottom: -$_md-checkbox-ripple-size; border-radius: 50%; z-index: 1; pointer-events: none; diff --git a/src/lib/core/_core.scss b/src/lib/core/_core.scss index cc4c22ed2528..c8c97c5ed129 100644 --- a/src/lib/core/_core.scss +++ b/src/lib/core/_core.scss @@ -5,6 +5,7 @@ @import 'ripple/ripple'; @import 'option/option'; @import 'option/option-theme'; +@import 'selection/pseudo-checkbox/pseudo-checkbox-theme'; // Mixin that renders all of the core styles that are not theme-dependent. @mixin md-core() { @@ -27,6 +28,7 @@ @mixin md-core-theme($theme) { @include md-ripple-theme($theme); @include md-option-theme($theme); + @include md-pseudo-checkbox-theme($theme); // Wrapper element that provides the theme background when the // user's content isn't inside of a `md-sidenav-container`. diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts index 122ce9ecba53..b0c08443f1b0 100644 --- a/src/lib/core/core.ts +++ b/src/lib/core/core.ts @@ -7,6 +7,7 @@ import {MdRippleModule} from './ripple/ripple'; import {PortalModule} from './portal/portal-directives'; import {OverlayModule} from './overlay/overlay-directives'; import {A11yModule} from './a11y/index'; +import {MdSelectionModule} from './selection/index'; // RTL @@ -114,6 +115,9 @@ export * from './compatibility/compatibility'; // Animation export * from './animation/animation'; +// Selection +export * from './selection/index'; + // Coercion export {coerceBooleanProperty} from './coercion/boolean-property'; export {coerceNumberProperty} from './coercion/number-property'; @@ -131,7 +135,8 @@ export {CompatibilityModule, NoConflictStyleCompatibilityMode} from './compatibi PortalModule, OverlayModule, A11yModule, - MdOptionModule + MdOptionModule, + MdSelectionModule, ], exports: [ MdLineModule, @@ -141,7 +146,8 @@ export {CompatibilityModule, NoConflictStyleCompatibilityMode} from './compatibi PortalModule, OverlayModule, A11yModule, - MdOptionModule + MdOptionModule, + MdSelectionModule, ], }) export class MdCoreModule { diff --git a/src/lib/core/selection/index.ts b/src/lib/core/selection/index.ts new file mode 100644 index 000000000000..4ebd17960d72 --- /dev/null +++ b/src/lib/core/selection/index.ts @@ -0,0 +1,10 @@ +import {NgModule} from '@angular/core'; +import {MdPseudoCheckbox} from './pseudo-checkbox/pseudo-checkbox'; + +export * from './pseudo-checkbox/pseudo-checkbox'; + +@NgModule({ + exports: [MdPseudoCheckbox], + declarations: [MdPseudoCheckbox] +}) +export class MdSelectionModule { } diff --git a/src/lib/core/selection/pseudo-checkbox/_pseudo-checkbox-theme.scss b/src/lib/core/selection/pseudo-checkbox/_pseudo-checkbox-theme.scss new file mode 100644 index 000000000000..f806bbed969e --- /dev/null +++ b/src/lib/core/selection/pseudo-checkbox/_pseudo-checkbox-theme.scss @@ -0,0 +1,44 @@ +@import '../../theming/theming'; + + +@mixin md-pseudo-checkbox-theme($theme) { + $is-dark-theme: map-get($theme, is-dark); + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + + // The color of the checkbox's checkmark / mixedmark. + $checkbox-mark-color: md-color($background, background); + + // NOTE(traviskaufman): While the spec calls for translucent blacks/whites for disabled colors, + // this does not work well with elements layered on top of one another. To get around this we + // blend the colors together based on the base color and the theme background. + $white-30pct-opacity-on-dark: #686868; + $black-26pct-opacity-on-light: #b0b0b0; + $disabled-color: if($is-dark-theme, $white-30pct-opacity-on-dark, $black-26pct-opacity-on-light); + + md-pseudo-checkbox::after { + color: $checkbox-mark-color; + } + + .md-pseudo-checkbox-checked, .md-pseudo-checkbox-indeterminate { + border: none; + + &.md-primary { + background: md-color($primary, 500); + } + + &.md-accent { + background: md-color($accent, 500); + } + + &.md-warn { + background: md-color($warn, 500); + } + + &.md-pseudo-checkbox-disabled { + background: $disabled-color; + } + } +} diff --git a/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.scss b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.scss new file mode 100644 index 000000000000..f82b1bb1a7c9 --- /dev/null +++ b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.scss @@ -0,0 +1,53 @@ +@import '../../style/checkbox-common'; + +// Padding inside of a pseudo checkbox. +$_md-pseudo-checkbox-padding: $md-checkbox-border-width * 2; + +// Size of the checkmark in a pseudo checkbox. +$_md-pseudo-checkmark-size: $md-checkbox-size - (2 * $_md-pseudo-checkbox-padding); + + +md-pseudo-checkbox { + width: $md-checkbox-size; + height: $md-checkbox-size; + border: $md-checkbox-border-width solid; + border-radius: 2px; + cursor: pointer; + display: inline-block; + vertical-align: middle; + box-sizing: border-box; + position: relative; + transition: + border-color $md-checkbox-transition-duration $md-linear-out-slow-in-timing-function, + background-color $md-checkbox-transition-duration $md-linear-out-slow-in-timing-function; + + // Used to render the checkmark/mixedmark inside of the box. + &::after { + position: absolute; + opacity: 0; + content: ''; + border-bottom: $md-checkbox-border-width solid currentColor; + transition: opacity $md-checkbox-transition-duration $md-linear-out-slow-in-timing-function; + } +} + +.md-pseudo-checkbox-disabled { + cursor: default; +} + +.md-pseudo-checkbox-indeterminate::after { + top: ($md-checkbox-size - $md-checkbox-border-width) / 2; + left: $md-checkbox-border-width; + width: $md-checkbox-size - ($md-checkbox-border-width * 2); + opacity: 1; +} + +.md-pseudo-checkbox-checked::after { + top: ($md-checkbox-size / 2) - ($_md-pseudo-checkmark-size / 4) - ($md-checkbox-size / 10); + left: $_md-pseudo-checkbox-padding - $md-checkbox-border-width / 2; + width: $_md-pseudo-checkmark-size; + height: ($_md-pseudo-checkmark-size - $md-checkbox-border-width) / 2; + border-left: $md-checkbox-border-width solid currentColor; + transform: rotate(-45deg); + opacity: 1; +} diff --git a/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.ts b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.ts new file mode 100644 index 000000000000..eab9b353bfe6 --- /dev/null +++ b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.ts @@ -0,0 +1,59 @@ +import { + Component, + ViewEncapsulation, + Input, + ElementRef, + Renderer, +} from '@angular/core'; + +export type MdPseudoCheckboxState = 'unchecked' | 'checked' | 'indeterminate'; + +/** + * Component that shows a simplified checkbox without including any kind of "real" checkbox. + * Meant to be used when the checkbox is purely decorative and a large number of them will be + * included, such as for the options in a multi-select. Uses no SVGs or complex animations. + * + * Note that this component will be completely invisible to screen-reader users. This is *not* + * interchangeable with and should *not* be used if the user would directly interact + * with the checkbox. The pseudo-checkbox should only be used as an implementation detail of + * more complex components that appropriately handle selected / checked state. + * @docs-private + */ +@Component({ + moduleId: module.id, + encapsulation: ViewEncapsulation.None, + selector: 'md-pseudo-checkbox', + styleUrls: ['pseudo-checkbox.css'], + template: '', + host: { + '[class.md-pseudo-checkbox-indeterminate]': 'state === "indeterminate"', + '[class.md-pseudo-checkbox-checked]': 'state === "checked"', + '[class.md-pseudo-checkbox-disabled]': 'disabled', + }, +}) +export class MdPseudoCheckbox { + /** Display state of the checkbox. */ + @Input() state: MdPseudoCheckboxState = 'unchecked'; + + /** Whether the checkbox is disabled. */ + @Input() disabled: boolean = false; + + /** Color of the checkbox. */ + @Input() + get color(): string { return this._color; }; + set color(value: string) { + if (value) { + let nativeElement = this._elementRef.nativeElement; + + this._renderer.setElementClass(nativeElement, `md-${this.color}`, false); + this._renderer.setElementClass(nativeElement, `md-${value}`, true); + this._color = value; + } + } + + private _color: string; + + constructor(private _elementRef: ElementRef, private _renderer: Renderer) { + this.color = 'accent'; + } +} diff --git a/src/lib/core/style/_checkbox-common.scss b/src/lib/core/style/_checkbox-common.scss new file mode 100644 index 000000000000..b63f4a5b3eba --- /dev/null +++ b/src/lib/core/style/_checkbox-common.scss @@ -0,0 +1,10 @@ +@import './variables'; + +// The width/height of the checkbox element. +$md-checkbox-size: $md-toggle-size !default; + +// The width of the checkbox border shown when the checkbox is unchecked. +$md-checkbox-border-width: 2px; + +// The base duration used for the majority of transitions for the checkbox. +$md-checkbox-transition-duration: 90ms;