From 876b8984817897f8a7f0e722d0e7faa025991430 Mon Sep 17 00:00:00 2001 From: Vanessa Schmitt Date: Fri, 14 Jun 2019 11:16:45 -0700 Subject: [PATCH] prototype(chips): MDC chip APIs match existing APIs --- package.json | 4 +- .../mdc-chips/mdc-chips-demo-module.ts | 17 + src/dev-app/mdc-chips/mdc-chips-demo.html | 221 ++++++- src/dev-app/mdc-chips/mdc-chips-demo.scss | 32 +- src/dev-app/mdc-chips/mdc-chips-demo.ts | 73 +++ .../mdc-chips/BUILD.bazel | 3 + .../mdc-chips/_mdc-chips.scss | 37 +- .../mdc-chips/chip-cell.ts | 19 - .../mdc-chips/chip-default-options.ts | 20 + .../mdc-chips/chip-grid.html | 1 - .../mdc-chips/chip-grid.ts | 425 +++++++++++++- .../mdc-chips/chip-input.ts | 173 ++++++ .../mdc-chips/chip-listbox.ts | 547 ++++++++++++++++++ .../mdc-chips/chip-option.html | 7 + .../mdc-chips/chip-option.ts | 188 ++++++ .../mdc-chips/chip-row.html | 9 + .../mdc-chips/chip-row.ts | 64 ++ .../mdc-chips/chip-set.ts | 254 ++++++++ .../mdc-chips/chip-text-control.ts | 27 + src/material-experimental/mdc-chips/chip.html | 3 + src/material-experimental/mdc-chips/chip.ts | 416 +++++++++++++ .../mdc-chips/chips.scss | 52 +- src/material-experimental/mdc-chips/module.ts | 47 +- .../mdc-chips/public-api.ts | 8 +- yarn.lock | 509 ++++++++-------- 25 files changed, 2863 insertions(+), 293 deletions(-) delete mode 100644 src/material-experimental/mdc-chips/chip-cell.ts create mode 100644 src/material-experimental/mdc-chips/chip-default-options.ts delete mode 100644 src/material-experimental/mdc-chips/chip-grid.html create mode 100644 src/material-experimental/mdc-chips/chip-input.ts create mode 100644 src/material-experimental/mdc-chips/chip-listbox.ts create mode 100644 src/material-experimental/mdc-chips/chip-option.html create mode 100644 src/material-experimental/mdc-chips/chip-option.ts create mode 100644 src/material-experimental/mdc-chips/chip-row.html create mode 100644 src/material-experimental/mdc-chips/chip-row.ts create mode 100644 src/material-experimental/mdc-chips/chip-set.ts create mode 100644 src/material-experimental/mdc-chips/chip-text-control.ts create mode 100644 src/material-experimental/mdc-chips/chip.html create mode 100644 src/material-experimental/mdc-chips/chip.ts diff --git a/package.json b/package.json index 29e2a56c338a..d5245b0ec984 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ }, "version": "8.0.1", "requiredAngularVersion": "^8.0.0 || ^9.0.0-0", - "requiredMDCVersion": "^1.1.0", + "requiredMDCVersion": "^2.3.1", "dependencies": { "@angular/animations": "^8.0.0", "@angular/common": "^8.0.0", @@ -47,7 +47,7 @@ "@bazel/buildifier": "^0.25.1", "@webcomponents/custom-elements": "^1.1.0", "core-js": "^2.6.1", - "material-components-web": "^1.1.0", + "material-components-web": "^2.3.1", "rxjs": "^6.4.0", "systemjs": "0.19.43", "tsickle": "^0.35.0", diff --git a/src/dev-app/mdc-chips/mdc-chips-demo-module.ts b/src/dev-app/mdc-chips/mdc-chips-demo-module.ts index ca5497af6481..00a0594d851c 100644 --- a/src/dev-app/mdc-chips/mdc-chips-demo-module.ts +++ b/src/dev-app/mdc-chips/mdc-chips-demo-module.ts @@ -6,14 +6,31 @@ * found in the LICENSE file at https://angular.io/license */ +import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatButtonModule} from '@angular/material/button'; +import {MatCardModule} from '@angular/material/card'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatToolbarModule} from '@angular/material/toolbar'; import {MatChipsModule} from '@angular/material-experimental/mdc-chips'; +import {MatIconModule} from '@angular/material/icon'; import {RouterModule} from '@angular/router'; import {MdcChipsDemo} from './mdc-chips-demo'; @NgModule({ imports: [ + CommonModule, + FormsModule, + MatButtonModule, + MatCardModule, + MatCheckboxModule, MatChipsModule, + MatFormFieldModule, + MatIconModule, + MatToolbarModule, + ReactiveFormsModule, RouterModule.forChild([{path: '', component: MdcChipsDemo}]), ], declarations: [MdcChipsDemo], diff --git a/src/dev-app/mdc-chips/mdc-chips-demo.html b/src/dev-app/mdc-chips/mdc-chips-demo.html index 38dbb08da705..48bc859f7cfc 100644 --- a/src/dev-app/mdc-chips/mdc-chips-demo.html +++ b/src/dev-app/mdc-chips/mdc-chips-demo.html @@ -1,2 +1,219 @@ - -Not yet implemented. +
+ + Static Chips + + +

Simple

+ + + Chip 1 + Chip 2 + Chip 3 + + +

Unstyled

+ + + Basic Chip 1 + Basic Chip 2 + Basic Chip 3 + + +

With avatar, icons, and color

+ + + + home + Home + cancel + + + + P + Portel + cancel + + + + M + Molly + + + + Koby + cancel + + + + Razzle + + + + + Mal + + + + + Husi + cancel + + + + Good + star + + + + Bad + star_border + + + + +

With Events

+ + + + With Events + cancel + + +
{{message}}
+ +
+
+ + + Selectable Chips + + + + +

Single selection

+ + + Extra Small + Small + Medium + Large + + +

Multi selection

+ + + Open Now + Takes Reservations + Pet Friendly + Good for Brunch + + +
+
+ + + Input Chips + + +

+ The <mat-chip-grid> component pairs with the matChipInputFor directive + to convert user input text into chips. + They can be used inside a <mat-form-field>. +

+ + + +

Input is last child of chip grid

+ + + + + {{person.name}} + cancel + + + + + +

Input is next sibling child of chip grid

+ + + + + {{person.name}} + cancel + + + + + +

+ The example above has overridden the [separatorKeys] input to allow for + ENTER, COMMA and SEMICOLON keys. +

+ +

Options

+

+ Add on Blur +

+ +
+
+ + + Miscellaneous + +

Stacked

+ +

+ You can also stack the chips if you want them on top of each other. +

+ + + + {{aColor.name}} + + + +

NgModel with multi selection

+ + + + {{aColor.name}} + + + + The selected colors are + + {{color}}{{isLast ? '' : ', '}}. + +

NgModel with single selection

+ + + + {{aColor.name}} + + + + The selected color is {{selectedColor}}. +
+
+
diff --git a/src/dev-app/mdc-chips/mdc-chips-demo.scss b/src/dev-app/mdc-chips/mdc-chips-demo.scss index 0b1af89fb3c5..c69505383b38 100644 --- a/src/dev-app/mdc-chips/mdc-chips-demo.scss +++ b/src/dev-app/mdc-chips/mdc-chips-demo.scss @@ -1 +1,31 @@ -// TODO: copy in demo styles from existing mat-chips demo. +.demo-chips { + .mat-mdc-chip-set-stacked { + display: block; + max-width: 200px; + } + + .mat-card { + padding: 0; + margin: 16px; + + & .mat-toolbar { + margin: 0; + } + + & .mat-card-content { + padding: 24px; + } + } + + mat-basic-chip { + margin: auto 10px; + } + + mat-chip-grid input { + width: 150px; + } +} + +.demo-has-chip-list { + width: 100%; +} diff --git a/src/dev-app/mdc-chips/mdc-chips-demo.ts b/src/dev-app/mdc-chips/mdc-chips-demo.ts index 3f912d7b5307..a3ea5cf4dc4c 100644 --- a/src/dev-app/mdc-chips/mdc-chips-demo.ts +++ b/src/dev-app/mdc-chips/mdc-chips-demo.ts @@ -6,7 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ +import {COMMA, ENTER} from '@angular/cdk/keycodes'; import {Component} from '@angular/core'; +import {ThemePalette} from '@angular/material/core'; +import {MatChipInputEvent} from '@angular/material-experimental/mdc-chips'; + +export interface Person { + name: string; +} + +export interface DemoColor { + name: string; + color: ThemePalette; +} @Component({ moduleId: module.id, @@ -15,4 +27,65 @@ import {Component} from '@angular/core'; styleUrls: ['mdc-chips-demo.css'], }) export class MdcChipsDemo { + visible = true; + selectable = true; + removable = true; + addOnBlur = true; + disabledListboxes = false; + disableInputs = false; + message = ''; + + // Enter, comma, semi-colon + separatorKeysCodes = [ENTER, COMMA, 186]; + + selectedPeople = null; + + people: Person[] = [ + {name: 'Kara'}, + {name: 'Jeremy'}, + {name: 'Topher'}, + {name: 'Elad'}, + {name: 'Kristiyan'}, + {name: 'Paul'} + ]; + + availableColors: DemoColor[] = [ + {name: 'none', color: undefined}, + {name: 'Primary', color: 'primary'}, + {name: 'Accent', color: 'accent'}, + {name: 'Warn', color: 'warn'} + ]; + + displayMessage(message: string): void { + this.message = message; + } + + add(event: MatChipInputEvent): void { + const {input, value} = event; + + // Add our person + if ((value || '').trim()) { + this.people.push({ name: value.trim() }); + } + + // Reset the input value + if (input) { + input.value = ''; + } + } + + remove(person: Person): void { + const index = this.people.indexOf(person); + + if (index >= 0) { + this.people.splice(index, 1); + } + } + + toggleVisible(): void { + this.visible = false; + } + + selectedColors: string[] = ['Primary', 'Warn']; + selectedColor = 'Accent'; } diff --git a/src/material-experimental/mdc-chips/BUILD.bazel b/src/material-experimental/mdc-chips/BUILD.bazel index 096e077509f2..4331d7b77382 100644 --- a/src/material-experimental/mdc-chips/BUILD.bazel +++ b/src/material-experimental/mdc-chips/BUILD.bazel @@ -14,8 +14,10 @@ ng_module( module_name = "@angular/material-experimental/mdc-chips", deps = [ "//src/material/core", + "//src/material/form-field", "@npm//@angular/common", "@npm//@angular/core", + "@npm//@angular/forms", "@npm//material-components-web", ], ) @@ -37,6 +39,7 @@ sass_binary( "external/npm/node_modules", ], deps = [ + "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib", "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib", "//src/material/core:all_themes", ], diff --git a/src/material-experimental/mdc-chips/_mdc-chips.scss b/src/material-experimental/mdc-chips/_mdc-chips.scss index 3a9e81463eb2..795a46c337e7 100644 --- a/src/material-experimental/mdc-chips/_mdc-chips.scss +++ b/src/material-experimental/mdc-chips/_mdc-chips.scss @@ -1,13 +1,44 @@ +@import '@material/chips/mixins'; @import '../mdc-helpers/mdc-helpers'; +@import '@material/theme/functions'; @mixin mat-chips-theme-mdc($theme) { - @include mat-using-mdc-theme($theme) { - // TODO: MDC theme styles here. + @include mdc-chip-set-core-styles($query: $mat-theme-styles-query); + @include mdc-chip-without-ripple($query: $mat-theme-styles-query); + + $primary: mat-color(map-get($theme, primary)); + $accent: mat-color(map-get($theme, accent)); + $warn: mat-color(map-get($theme, warn)); + $background: map-get($theme, background); + + $unselected-background: mat-color($background, unselected-chip); + + .mat-mdc-chip { + @include mdc-chip-fill-color-accessible($unselected-background); + + &.mat-primary { + &.mdc-chip--selected, &.mat-mdc-chip-highlighted { + @include mdc-chip-fill-color-accessible($primary); + } + } + + &.mat-accent { + &.mdc-chip--selected, &.mat-mdc-chip-highlighted { + @include mdc-chip-fill-color-accessible($accent); + } + } + + &.mat-warn { + &.mdc-chip--selected, &.mat-mdc-chip-highlighted { + @include mdc-chip-fill-color-accessible($warn); + } + } } } @mixin mat-chips-typography-mdc($config) { + @include mdc-chip-set-core-styles($query: $mat-typography-styles-query); @include mat-using-mdc-typography($config) { - // TODO: MDC typography styles here. + @include mdc-chip-without-ripple($query: $mat-typography-styles-query); } } diff --git a/src/material-experimental/mdc-chips/chip-cell.ts b/src/material-experimental/mdc-chips/chip-cell.ts deleted file mode 100644 index 4745d0726503..000000000000 --- a/src/material-experimental/mdc-chips/chip-cell.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {Directive} from '@angular/core'; - -@Directive({ - selector: 'mat-chip-cell', - host: { - 'class': 'mat-mdc-chip-cell', - }, -}) -export class MatChipCell { - // TODO: set up MDC foundation class. -} diff --git a/src/material-experimental/mdc-chips/chip-default-options.ts b/src/material-experimental/mdc-chips/chip-default-options.ts new file mode 100644 index 000000000000..2cd5b2838922 --- /dev/null +++ b/src/material-experimental/mdc-chips/chip-default-options.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {InjectionToken} from '@angular/core'; + +/** Default options, for the chips module, that can be overridden. */ +export interface MatChipsDefaultOptions { + /** The list of key codes that will trigger a chipEnd event. */ + separatorKeyCodes: number[] | Set; +} + +/** Injection token to be used to override the default options for the chips module. */ +export const MAT_CHIPS_DEFAULT_OPTIONS = + new InjectionToken('mat-chips-default-options'); + diff --git a/src/material-experimental/mdc-chips/chip-grid.html b/src/material-experimental/mdc-chips/chip-grid.html deleted file mode 100644 index 9c75ddb6f477..000000000000 --- a/src/material-experimental/mdc-chips/chip-grid.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/material-experimental/mdc-chips/chip-grid.ts b/src/material-experimental/mdc-chips/chip-grid.ts index 1ca5fae2adbf..018673077f2a 100644 --- a/src/material-experimental/mdc-chips/chip-grid.ts +++ b/src/material-experimental/mdc-chips/chip-grid.ts @@ -6,19 +6,434 @@ * found in the LICENSE file at https://angular.io/license */ -import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {BACKSPACE} from '@angular/cdk/keycodes'; +import { + AfterContentInit, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DoCheck, + ElementRef, + EventEmitter, + Input, + OnDestroy, + Optional, + Output, + Self, + ViewEncapsulation +} from '@angular/core'; +import {ControlValueAccessor, FormGroupDirective, NgControl, NgForm} from '@angular/forms'; +import { + CanUpdateErrorState, + CanUpdateErrorStateCtor, + ErrorStateMatcher, + mixinErrorState, +} from '@angular/material/core'; +import {MatFormFieldControl} from '@angular/material/form-field'; +import {MatChipTextControl} from './chip-text-control'; +import {merge, Observable, Subscription} from 'rxjs'; +import {startWith, takeUntil} from 'rxjs/operators'; +import {MatChipEvent} from './chip'; +import {MatChipSet} from './chip-set'; + + +/** Change event object that is emitted when the chip grid value has changed. */ +export class MatChipGridChange { + constructor( + /** Chip grid that emitted the event. */ + public source: MatChipGrid, + /** Value of the chip grid when the event was emitted. */ + public value: any) { } +} + +/** + * Boilerplate for applying mixins to MatChipGrid. + * @docs-private + */ +class MatChipGridBase extends MatChipSet { + constructor(_elementRef: ElementRef, + _changeDetectorRef: ChangeDetectorRef, + public _defaultErrorStateMatcher: ErrorStateMatcher, + public _parentForm: NgForm, + public _parentFormGroup: FormGroupDirective, + /** @docs-private */ + public ngControl: NgControl) { + super(_elementRef, _changeDetectorRef); + } +} +const _MatChipGridMixinBase: CanUpdateErrorStateCtor & typeof MatChipGridBase = + mixinErrorState(MatChipGridBase); + +/** + * An extension of the MatChipSet component used with MatChipRow chips and + * the matChipInputFor directive. + */ @Component({ moduleId: module.id, selector: 'mat-chip-grid', - templateUrl: 'chip-grid.html', + template: '', styleUrls: ['chips.css'], host: { - 'class': 'mat-mdc-chip-grid', + 'class': 'mat-mdc-chip-set mat-mdc-chip-grid mdc-chip-set', + 'role': 'grid', + '[tabIndex]': 'empty ? -1 : 0', + // TODO: replace this binding with use of AriaDescriber + '[attr.aria-describedby]': '_ariaDescribedby || null', + '[attr.aria-required]': 'required.toString()', + '[attr.aria-disabled]': 'disabled.toString()', + '[attr.aria-invalid]': 'errorState', + '[class.mat-mdc-chip-list-disabled]': 'disabled', + '[class.mat-mdc-chip-list-invalid]': 'errorState', + '[class.mat-mdc-chip-list-required]': 'required', + '[id]': '_uid', }, + providers: [{provide: MatFormFieldControl, useExisting: MatChipGrid}], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MatChipGrid { - // TODO: set up MDC foundation class. +export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentInit, AfterViewInit, + CanUpdateErrorState, ControlValueAccessor, DoCheck, MatFormFieldControl, OnDestroy { + /** + * Implemented as part of MatFormFieldControl. + * @docs-private + */ + readonly controlType: string = 'mat-chip-grid'; + + /** Subscription to blur changes in the chips. */ + private _chipBlurSubscription: Subscription | null; + + /** The chip input to add more chips */ + protected _chipInput: MatChipTextControl; + + /** + * Function when touched. Set as part of ControlValueAccessor implementation. + * @docs-private + */ + _onTouched = () => {}; + + /** + * Function when changed. Set as part of ControlValueAccessor implementation. + * @docs-private + */ + _onChange: (value: any) => void = () => {}; + + /** + * Implemented as part of MatFormFieldControl. + * @docs-private + */ + @Input() + get disabled(): boolean { return this.ngControl ? !!this.ngControl.disabled : this._disabled; } + set disabled(value: boolean) { + this._disabled = coerceBooleanProperty(value); + this._syncChipsState(); + } + + /** + * Implemented as part of MatFormFieldControl. + * @docs-private + */ + get id(): string { return this._chipInput.id; } + + /** + * Implemented as part of MatFormFieldControl. + * @docs-private + */ + get empty(): boolean { return this._chipInput.empty && this._chips.length === 0; } + + /** + * Implemented as part of MatFormFieldControl. + * @docs-private + */ + @Input() + get placeholder(): string { return this._chipInput.placeholder; } + + /** Whether any chips or the matChipInput inside of this chip-grid has focus. */ + get focused(): boolean { return this._chipInput.focused || this._hasFocusedChip(); } + + /** + * Implemented as part of MatFormFieldControl. + * @docs-private + */ + @Input() + get required(): boolean { return this._required; } + set required(value: boolean) { + this._required = coerceBooleanProperty(value); + this.stateChanges.next(); + } + protected _required: boolean = false; + + /** + * Implemented as part of MatFormFieldControl. + * @docs-private + */ + get shouldLabelFloat(): boolean { return !this.empty || this.focused; } + + /** + * Implemented as part of MatFormFieldControl. + * @docs-private + */ + @Input() + get value(): any { return this._value; } + set value(value: any) { + this._value = value; + } + protected _value: any; + + /** Combined stream of all of the child chips' blur events. */ + get chipBlurChanges(): Observable { + return merge(...this._chips.map(chip => chip._onBlur)); + } + + /** Emits when the chip grid value has been changed by the user. */ + @Output() readonly change: EventEmitter = + new EventEmitter(); + + /** + * Emits whenever the raw value of the chip-grid changes. This is here primarily + * to facilitate the two-way binding for the `value` input. + * @docs-private + */ + @Output() readonly valueChange: EventEmitter = new EventEmitter(); + + constructor(_elementRef: ElementRef, + _changeDetectorRef: ChangeDetectorRef, + @Optional() _parentForm: NgForm, + @Optional() _parentFormGroup: FormGroupDirective, + _defaultErrorStateMatcher: ErrorStateMatcher, + /** @docs-private */ + @Optional() @Self() public ngControl: NgControl) { + super(_elementRef, _changeDetectorRef, _defaultErrorStateMatcher, _parentForm, _parentFormGroup, + ngControl); + if (this.ngControl) { + this.ngControl.valueAccessor = this; + } + } + + ngAfterContentInit() { + super.ngAfterContentInit(); + this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { + // Check to see if we have a destroyed chip and need to refocus + this._updateFocusForDestroyedChips(); + + this.stateChanges.next(); + }); + } + + ngAfterViewInit() { + super.ngAfterViewInit(); + if (!this._chipInput) { + throw Error('mat-chip-grid must be used in combination with matChipInputFor.'); + } + } + + ngDoCheck() { + if (this.ngControl) { + // We need to re-evaluate this on every change detection cycle, because there are some + // error triggers that we can't subscribe to (e.g. parent form submissions). This means + // that whatever logic is in here has to be super lean or we risk destroying the performance. + this.updateErrorState(); + } + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.stateChanges.complete(); + } + + /** Associates an HTML input element with this chip grid. */ + registerInput(inputElement: MatChipTextControl): void { + this._chipInput = inputElement; + this._setMdcClass('mdc-chip-set--input', true); + } + + /** + * Implemented as part of MatFormFieldControl. + * @docs-private + */ + onContainerClick(event: MouseEvent) { + if (!this._originatesFromChip(event) && !this.disabled) { + this.focus(); + } + } + + /** + * Focuses the first chip in this chip grid, or the associated input when there + * are no eligible chips. + */ + focus(): void { + if (this.disabled || this._chipInput.focused) { + return; + } + + if (this._chips.length > 0) { + this._chips.toArray()[0].focus(); + } else { + this._focusInput(); + } + + this.stateChanges.next(); + } + + /** + * Implemented as part of MatFormFieldControl. + * @docs-private + */ + setDescribedByIds(ids: string[]) { this._ariaDescribedby = ids.join(' '); } + + /** + * Implemented as part of ControlValueAccessor. + * @docs-private + */ + writeValue(value: any): void { + // The user is responsible for creating the child chips, so we just store the value. + this._value = value; + } + + /** + * Implemented as part of ControlValueAccessor. + * @docs-private + */ + registerOnChange(fn: (value: any) => void): void { + this._onChange = fn; + } + + /** + * Implemented as part of ControlValueAccessor. + * @docs-private + */ + registerOnTouched(fn: () => void): void { + this._onTouched = fn; + } + + /** When blurred, mark the field as touched when focus moved outside the chip grid. */ + _blur() { + if (this.disabled) { + return; + } + + // Check whether the focus moved to chip input. + // If the focus is not moved to chip input, mark the field as touched. If the focus moved + // to chip input, do nothing. + // Timeout is needed to wait for the focus() event trigger on chip input. + setTimeout(() => { + if (!this.focused) { + this._propagateChanges(); + this._markAsTouched(); + } + }); + } + + /** + * Removes the `tabindex` from the chip grid and resets it back afterwards, allowing the + * user to tab out of it. This prevents the grid from capturing focus and redirecting + * it back to the first chip, creating a focus trap, if it user tries to tab away. + */ + _allowFocusEscape() { + // TODO + } + + /** Handles custom keyboard events. */ + _keydown(event: KeyboardEvent) { + const target = event.target as HTMLElement; + + // If they are on an empty input and hit backspace, focus the last chip + if (event.keyCode === BACKSPACE && this._isEmptyInput(target)) { + if (this._chips.length) { + this._chips.toArray()[this._chips.length - 1].focus(); + } + event.preventDefault(); + } + this.stateChanges.next(); + } + + /** Unsubscribes from all chip events. */ + protected _dropSubscriptions() { + super._dropSubscriptions(); + if (this._chipBlurSubscription) { + this._chipBlurSubscription.unsubscribe(); + this._chipBlurSubscription = null; + } + } + + /** Subscribes to events on the child chips. */ + protected _subscribeToChipEvents() { + super._subscribeToChipEvents(); + this._listenToChipsBlur(); + } + + /** Subscribes to chip blur events. */ + private _listenToChipsBlur(): void { + this._chipBlurSubscription = this.chipBlurChanges.subscribe(() => { + this._blur(); + this.stateChanges.next(); + }); + } + + /** Emits change event to set the model value. */ + private _propagateChanges(fallbackValue?: any): void { + const valueToEmit = this._chips.length ? this._chips.toArray().map( + chip => chip.value) : fallbackValue; + this._value = valueToEmit; + this.change.emit(new MatChipGridChange(this, valueToEmit)); + this.valueChange.emit(valueToEmit); + this._onChange(valueToEmit); + this._changeDetectorRef.markForCheck(); + } + + /** Mark the field as touched */ + private _markAsTouched() { + this._onTouched(); + this._changeDetectorRef.markForCheck(); + this.stateChanges.next(); + } + + /** Checks whether an event comes from inside a chip element. */ + private _originatesFromChip(event: Event): boolean { + let currentElement = event.target as HTMLElement | null; + + while (currentElement && currentElement !== this._elementRef.nativeElement) { + if (currentElement.classList.contains('mdc-chip')) { + return true; + } + + currentElement = currentElement.parentElement; + } + + return false; + } + + /** + * If the amount of chips changed, we need to focus the next closest chip. + */ + private _updateFocusForDestroyedChips() { + // Move focus to the closest chip. If no other chips remain, focus the chip-grid itself. + if (this._lastDestroyedChipIndex != null) { + if (this._chips.length) { + const newChipIndex = Math.min(this._lastDestroyedChipIndex, this._chips.length - 1); + this._chips.toArray()[newChipIndex].focus(); + } else { + this.focus(); + } + } + + this._lastDestroyedChipIndex = null; + } + + /** Focus input element. */ + private _focusInput() { + this._chipInput.focus(); + } + + /** Returns true if element is an input with no value. */ + private _isEmptyInput(element: HTMLElement): boolean { + if (element && element.nodeName.toLowerCase() === 'input') { + let input = element as HTMLInputElement; + return !input.value; + } + + return false; + } } diff --git a/src/material-experimental/mdc-chips/chip-input.ts b/src/material-experimental/mdc-chips/chip-input.ts new file mode 100644 index 000000000000..cbfc6b3241ea --- /dev/null +++ b/src/material-experimental/mdc-chips/chip-input.ts @@ -0,0 +1,173 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {Directive, ElementRef, EventEmitter, Inject, Input, OnChanges, Output} from '@angular/core'; +import {hasModifierKey, TAB} from '@angular/cdk/keycodes'; +import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './chip-default-options'; +import {MatChipGrid} from './chip-grid'; +import {MatChipTextControl} from './chip-text-control'; + +/** Represents an input event on a `matChipInput`. */ +export interface MatChipInputEvent { + /** The native `` element that the event is being fired for. */ + input: HTMLInputElement; + + /** The value of the input. */ + value: string; +} + +// Increasing integer for generating unique ids. +let nextUniqueId = 0; + +/** + * Directive that adds chip-specific behaviors to an input element inside ``. + * May be placed inside or outside of a ``. + */ +@Directive({ + selector: 'input[matChipInputFor]', + exportAs: 'matChipInput, matChipInputFor', + host: { + 'class': 'mat-chip-input mat-input-element', + '(keydown)': '_keydown($event)', + '(blur)': '_blur()', + '(focus)': '_focus()', + '(input)': '_onInput()', + '[id]': 'id', + '[attr.disabled]': 'disabled || null', + '[attr.placeholder]': 'placeholder || null', + '[attr.aria-invalid]': '_chipGrid && _chipGrid.ngControl ? _chipGrid.ngControl.invalid : null', + } +}) +export class MatChipInput implements MatChipTextControl, OnChanges { + /** Whether the control is focused. */ + focused: boolean = false; + _chipGrid: MatChipGrid; + + /** Register input for chip list */ + @Input('matChipInputFor') + set chipGrid(value: MatChipGrid) { + if (value) { + this._chipGrid = value; + this._chipGrid.registerInput(this); + } + } + + /** + * Whether or not the chipEnd event will be emitted when the input is blurred. + */ + @Input('matChipInputAddOnBlur') + get addOnBlur(): boolean { return this._addOnBlur; } + set addOnBlur(value: boolean) { this._addOnBlur = coerceBooleanProperty(value); } + _addOnBlur: boolean = false; + + /** + * The list of key codes that will trigger a chipEnd event. + * + * Defaults to `[ENTER]`. + */ + @Input('matChipInputSeparatorKeyCodes') + separatorKeyCodes: number[] | Set = this._defaultOptions.separatorKeyCodes; + + /** Emitted when a chip is to be added. */ + @Output('matChipInputTokenEnd') + chipEnd: EventEmitter = new EventEmitter(); + + /** The input's placeholder text. */ + @Input() placeholder: string = ''; + + /** Unique id for the input. */ + @Input() id: string = `mat-chip-list-input-${nextUniqueId++}`; + + /** Whether the input is disabled. */ + @Input() + get disabled(): boolean { return this._disabled || (this._chipGrid && this._chipGrid.disabled); } + set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); } + private _disabled: boolean = false; + + /** Whether the input is empty. */ + get empty(): boolean { return !this._inputElement.value; } + + /** The native input element to which this directive is attached. */ + protected _inputElement: HTMLInputElement; + + constructor( + protected _elementRef: ElementRef, + @Inject(MAT_CHIPS_DEFAULT_OPTIONS) private _defaultOptions: MatChipsDefaultOptions) { + this._inputElement = this._elementRef.nativeElement as HTMLInputElement; + } + + ngOnChanges() { + this._chipGrid.stateChanges.next(); + } + + /** Utility method to make host definition/tests more clear. */ + _keydown(event?: KeyboardEvent) { + // Allow the user's focus to escape when they're tabbing forward. Note that we don't + // want to do this when going backwards, because focus should go back to the first chip. + if (event && event.keyCode === TAB && !hasModifierKey(event, 'shiftKey')) { + this._chipGrid._allowFocusEscape(); + } + + this._emitChipEnd(event); + } + + /** Checks to see if the blur should emit the (chipEnd) event. */ + _blur() { + if (this.addOnBlur) { + this._emitChipEnd(); + } + this.focused = false; + // Blur the chip list if it is not focused + if (!this._chipGrid.focused) { + this._chipGrid._blur(); + } + this._chipGrid.stateChanges.next(); + } + + _focus() { + this.focused = true; + this._chipGrid.stateChanges.next(); + } + + /** Checks to see if the (chipEnd) event needs to be emitted. */ + _emitChipEnd(event?: KeyboardEvent) { + if (!this._inputElement.value && !!event) { + this._chipGrid._keydown(event); + } + if (!event || this._isSeparatorKey(event)) { + this.chipEnd.emit({ input: this._inputElement, value: this._inputElement.value }); + + if (event) { + event.preventDefault(); + } + } + } + + _onInput() { + // Let chip list know whenever the value changes. + this._chipGrid.stateChanges.next(); + } + + /** Focuses the input. */ + focus(): void { + this._inputElement.focus(); + } + + /** Checks whether a keycode is one of the configured separators. */ + private _isSeparatorKey(event: KeyboardEvent) { + if (hasModifierKey(event)) { + return false; + } + + const separators = this.separatorKeyCodes; + const keyCode = event.keyCode; + return Array.isArray(separators) ? separators.indexOf(keyCode) > -1 : separators.has(keyCode); + } +} + diff --git a/src/material-experimental/mdc-chips/chip-listbox.ts b/src/material-experimental/mdc-chips/chip-listbox.ts new file mode 100644 index 000000000000..84f5a268909f --- /dev/null +++ b/src/material-experimental/mdc-chips/chip-listbox.ts @@ -0,0 +1,547 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {FocusKeyManager} from '@angular/cdk/a11y'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {HOME, END} from '@angular/cdk/keycodes'; +import { + AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + ElementRef, + EventEmitter, + forwardRef, + Input, + Output, + QueryList, + ViewEncapsulation +} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; +import {MDCChipSetAdapter, MDCChipSetFoundation} from '@material/chips'; +import {merge, Observable, Subscription} from 'rxjs'; +import {startWith, takeUntil} from 'rxjs/operators'; + +import {MatChip, MatChipEvent} from './chip'; +import {MatChipOption, MatChipSelectionChange} from './chip-option'; +import {MatChipSet} from './chip-set'; + + +/** Change event object that is emitted when the chip listbox value has changed. */ +export class MatChipListboxChange { + constructor( + /** Chip listbox that emitted the event. */ + public source: MatChipListbox, + /** Value of the chip listbox when the event was emitted. */ + public value: any) { } +} + +/** + * Provider Expression that allows mat-chip-listbox to register as a ControlValueAccessor. + * This allows it to support [(ngModel)]. + * @docs-private + */ +export const MAT_CHIP_LISTBOX_CONTROL_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MatChipListbox), + multi: true +}; + +/** + * An extension of the MatChipSet component that supports chip selection. + * Used with MatChipOption chips. + */ +@Component({ + moduleId: module.id, + selector: 'mat-chip-listbox', + template: '', + styleUrls: ['chips.css'], + host: { + 'class': 'mat-mdc-chip-set mat-mdc-chip-listbox mdc-chip-set', + 'role': 'listbox', + '[tabIndex]': 'disabled ? null : _tabIndex', + // TODO: replace this binding with use of AriaDescriber + '[attr.aria-describedby]': '_ariaDescribedby || null', + '[attr.aria-required]': 'required.toString()', + '[attr.aria-disabled]': 'disabled.toString()', + '[attr.aria-multiselectable]': 'multiple', + '[class.mat-mdc-chip-list-disabled]': 'disabled', + '[class.mat-mdc-chip-list-required]': 'required', + '(focus)': 'focus()', + '(blur)': '_blur()', + '(keydown)': '_keydown($event)', + '[id]': '_uid', + }, + providers: [MAT_CHIP_LISTBOX_CONTROL_VALUE_ACCESSOR], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MatChipListbox extends MatChipSet implements AfterContentInit, ControlValueAccessor { + + /** Subscription to selection changes in the chips. */ + private _chipSelectionSubscription: Subscription | null; + + /** Subscription to blur changes in the chips. */ + private _chipBlurSubscription: Subscription | null; + + /** Subscription to focus changes in the chips. */ + private _chipFocusSubscription: Subscription | null; + + /** + * Implementation of the MDC chip-set adapter interface. + * These methods are called by the chip set foundation. + * + * Overrides the base MatChipSet adapter to provide a setSelected method. + */ + protected _chipSetAdapter: MDCChipSetAdapter = { + hasClass: (className: string) => this._hasMdcClass(className), + // No-op. We keep track of chips via ContentChildren, which will be updated when a chip is + // removed. + removeChip: () => {}, + setSelected: (chipId: string, selected: boolean) => { + this._setSelected(chipId, selected); + } + }; + + /** Tab index for the chip list. */ + _tabIndex = 0; + + /** The FocusKeyManager which handles focus. */ + _keyManager: FocusKeyManager; + + /** + * Function when touched. Set as part of ControlValueAccessor implementation. + * @docs-private + */ + _onTouched = () => {}; + + /** + * Function when changed. Set as part of ControlValueAccessor implementation. + * @docs-private + */ + _onChange: (value: any) => void = () => {}; + + /** Whether the user should be allowed to select multiple chips. */ + @Input() + get multiple(): boolean { return this._multiple; } + set multiple(value: boolean) { + this._multiple = coerceBooleanProperty(value); + this._updateMdcSelectionClasses(); + this._syncListboxProperties(); + } + private _multiple: boolean = false; + + /** The array of selected chips inside the chip listbox. */ + get selected(): MatChipOption[] | MatChipOption { + const selectedChips = this._optionChips.toArray().filter(chip => chip.selected); + return this.multiple ? selectedChips : selectedChips[0]; + } + + /** + * Whether or not this chip listbox is selectable. + * + * When a chip listbox is not selectable, the selected states for all + * the chips inside the chip listbox are always ignored. + */ + @Input() + get selectable(): boolean { return this._selectable; } + set selectable(value: boolean) { + this._selectable = coerceBooleanProperty(value); + this._updateMdcSelectionClasses(); + this._syncListboxProperties(); + } + protected _selectable: boolean = true; + + /** + * A function to compare the option values with the selected values. The first argument + * is a value from an option. The second is a value from the selection. A boolean + * should be returned. + */ + @Input() + get compareWith(): (o1: any, o2: any) => boolean { return this._compareWith; } + set compareWith(fn: (o1: any, o2: any) => boolean) { + this._compareWith = fn; + this._initializeSelection(); + } + private _compareWith = (o1: any, o2: any) => o1 === o2; + + + /** Whether this chip listbox is required. */ + @Input() + get required(): boolean { return this._required; } + set required(value: boolean) { + this._required = coerceBooleanProperty(value); + } + protected _required: boolean = false; + + /** Combined stream of all of the child chips' selection change events. */ + get chipSelectionChanges(): Observable { + return merge(...this._optionChips.map(chip => chip.selectionChange)); + } + + /** Combined stream of all of the child chips' focus events. */ + get chipFocusChanges(): Observable { + return merge(...this._chips.map(chip => chip._onFocus)); + } + + /** Combined stream of all of the child chips' blur events. */ + get chipBlurChanges(): Observable { + return merge(...this._chips.map(chip => chip._onBlur)); + } + + /** The value of the listbox, which is the combined value of the selected chips. */ + get value(): any { + if (Array.isArray(this.selected)) { + return this.selected.map(chip => chip.value); + } else { + return this.selected ? this.selected.value : null; + } + } + + /** Event emitted when the selected chip listbox value has been changed by the user. */ + @Output() readonly change: EventEmitter = + new EventEmitter(); + + @ContentChildren(MatChipOption, { + // We need to use `descendants: true`, because Ivy will no longer match + // indirect descendants if it's left as false. + descendants: true + }) + _optionChips: QueryList; + + constructor(protected _elementRef: ElementRef, + _changeDetectorRef: ChangeDetectorRef) { + super(_elementRef, _changeDetectorRef); + // Reinitialize the foundation with our overridden adapter + this._chipSetFoundation = new MDCChipSetFoundation(this._chipSetAdapter); + this._updateMdcSelectionClasses(); + } + + ngAfterContentInit() { + super.ngAfterContentInit(); + this._initKeyManager(); + + this._optionChips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { + // Update listbox selectable/multiple properties on chips + this._syncListboxProperties(); + + // Reset chips selected/deselected status + this._initializeSelection(); + + // Check to see if we need to update our tab index + this._updateTabIndex(); + + // Check to see if we have a destroyed chip and need to refocus + this._updateFocusForDestroyedChips(); + }); + } + + /** + * Focuses the first selected chip in this chip listbox, or the first non-disabled chip when there + * are no selected chips. + */ + focus(): void { + if (this.disabled) { + return; + } + + const firstSelectedChip = this._getFirstSelectedChip(); + + if (firstSelectedChip) { + const firstSelectedChipIndex = this._chips.toArray().indexOf(firstSelectedChip); + this._keyManager.setActiveItem(firstSelectedChipIndex); + } else if (this._chips.length > 0) { + this._keyManager.setFirstItemActive(); + } + } + + /** + * Implemented as part of ControlValueAccessor. + * @docs-private + */ + writeValue(value: any): void { + if (this._chips) { + this._setSelectionByValue(value, false); + } + } + + /** + * Implemented as part of ControlValueAccessor. + * @docs-private + */ + registerOnChange(fn: (value: any) => void): void { + this._onChange = fn; + } + + /** + * Implemented as part of ControlValueAccessor. + * @docs-private + */ + registerOnTouched(fn: () => void): void { + this._onTouched = fn; + } + + /** Selects all chips with value. */ + _setSelectionByValue(value: any, isUserInput: boolean = true) { + this._clearSelection(); + + if (Array.isArray(value)) { + value.forEach(currentValue => this._selectValue(currentValue, isUserInput)); + } else { + const correspondingChip = this._selectValue(value, isUserInput); + + // Shift focus to the active item. Note that we shouldn't do this in multiple + // mode, because we don't know what chip the user interacted with last. + if (correspondingChip) { + if (isUserInput) { + this._keyManager.setActiveItem(correspondingChip); + } + } + } + } + + /** Selects or deselects a chip by id. */ + _setSelected(chipId: string, selected: boolean) { + const chip = this._optionChips.find(c => c.id === chipId); + if (chip && chip.selected != selected) { + chip.toggleSelected(true); + } + } + + /** When blurred, marks the field as touched when focus moved outside the chip listbox. */ + _blur() { + if (this.disabled) { + return; + } + + // Wait to see if focus moves to an indivdual chip. + setTimeout(() => { + if (!this.focused) { + this._keyManager.setActiveItem(-1); + this._propagateChanges(); + this._markAsTouched(); + } + }); + } + + /** + * Removes the `tabindex` from the chip listbox and resets it back afterwards, allowing the + * user to tab out of it. This prevents the listbox from capturing focus and redirecting + * it back to the first chip, creating a focus trap, if it user tries to tab away. + */ + _allowFocusEscape() { + if (this._tabIndex !== -1) { + this._tabIndex = -1; + + setTimeout(() => { + this._tabIndex = 0; + this._changeDetectorRef.markForCheck(); + }); + } + } + + /** + * Handles custom keyboard shortcuts, and passes other keyboard events to the keyboard manager. + */ + _keydown(event: KeyboardEvent) { + const target = event.target as HTMLElement; + + if (target && target.classList.contains('mdc-chip')) { + if (event.keyCode === HOME) { + this._keyManager.setFirstItemActive(); + event.preventDefault(); + } else if (event.keyCode === END) { + this._keyManager.setLastItemActive(); + event.preventDefault(); + } else { + this._keyManager.onKeydown(event); + } + } + } + + /** Marks the field as touched */ + private _markAsTouched() { + this._onTouched(); + this._changeDetectorRef.markForCheck(); + } + + /** Emits change event to set the model value. */ + private _propagateChanges(fallbackValue?: any): void { + let valueToEmit: any = this.value || fallbackValue; + this.change.emit(new MatChipListboxChange(this, valueToEmit)); + this._onChange(valueToEmit); + this._changeDetectorRef.markForCheck(); + } + + /** + * Initializes the chip listbox selection state to reflect any chips that were preselected. + */ + private _initializeSelection() { + this._optionChips.forEach(chip => { + if (chip.selected) { + this._chipSetFoundation.select(chip.id); + } + }); + } + + /** + * Deselects every chip in the listbox. + * @param skip Chip that should not be deselected. + */ + private _clearSelection(skip?: MatChip): void { + this._optionChips.forEach(chip => { + if (chip !== skip) { + chip.deselect(); + } + }); + } + + /** + * Finds and selects the chip based on its value. + * @returns Chip that has the corresponding value. + */ + private _selectValue(value: any, isUserInput: boolean = true): MatChip | undefined { + + const correspondingChip = this._optionChips.find(chip => { + return chip.value != null && this._compareWith(chip.value, value); + }); + + if (correspondingChip) { + isUserInput ? correspondingChip.selectViaInteraction() : correspondingChip.select(); + } + + return correspondingChip; + } + + /** Syncs the chip-listbox selection state with the individual chips. */ + private _syncListboxProperties() { + if (this._optionChips) { + // Defer setting the value in order to avoid the "Expression + // has changed after it was checked" errors from Angular. + Promise.resolve().then(() => { + this._optionChips.forEach(chip => { + chip._chipListMultiple = this.multiple; + chip.chipListSelectable = this._selectable; + chip._changeDetectorRef.markForCheck(); + }); + }); + } + } + + /** Sets the mdc classes for single vs multi selection. */ + private _updateMdcSelectionClasses() { + this._setMdcClass('mdc-chip-set--filter', this.selectable && this.multiple); + this._setMdcClass('mdc-chip-set--choice', this.selectable && !this.multiple); + } + + /** Initializes the key manager to manage focus. */ + private _initKeyManager() { + this._keyManager = new FocusKeyManager(this._chips) + .withWrap() + .withVerticalOrientation() + .withHorizontalOrientation('ltr'); + + this._keyManager.tabOut.pipe(takeUntil(this._destroyed)).subscribe(() => { + this._allowFocusEscape(); + }); + } + + /** Returns the first selected chip in this listbox, or undefined if no chips are selected. */ + private _getFirstSelectedChip(): MatChipOption | undefined { + if (Array.isArray(this.selected)) { + return this.selected.length ? this.selected[0] : undefined; + } else { + return this.selected; + } + } + + /** Unsubscribes from all chip events. */ + protected _dropSubscriptions() { + super._dropSubscriptions(); + if (this._chipSelectionSubscription) { + this._chipSelectionSubscription.unsubscribe(); + this._chipSelectionSubscription = null; + } + + if (this._chipBlurSubscription) { + this._chipBlurSubscription.unsubscribe(); + this._chipBlurSubscription = null; + } + + if (this._chipFocusSubscription) { + this._chipFocusSubscription.unsubscribe(); + this._chipFocusSubscription = null; + } + } + + /** Subscribes to events on the child chips. */ + protected _subscribeToChipEvents() { + super._subscribeToChipEvents(); + this._listenToChipsSelection(); + this._listenToChipsFocus(); + this._listenToChipsBlur(); + } + + /** Subscribes to chip focus events. */ + private _listenToChipsFocus(): void { + this._chipFocusSubscription = this.chipFocusChanges.subscribe((event: MatChipEvent) => { + let chipIndex: number = this._chips.toArray().indexOf(event.chip); + + if (this._isValidIndex(chipIndex)) { + this._keyManager.updateActiveItemIndex(chipIndex); + } + }); + } + + /** Subscribes to chip blur events. */ + private _listenToChipsBlur(): void { + this._chipBlurSubscription = this.chipBlurChanges.subscribe(() => { + this._blur(); + }); + } + + /** Subscribes to selection changes in the option chips. */ + private _listenToChipsSelection(): void { + this._chipSelectionSubscription = this.chipSelectionChanges.subscribe( + (chipSelectionChange: MatChipSelectionChange) => { + this._chipSetFoundation.handleChipSelection( + chipSelectionChange.source.id, chipSelectionChange.selected); + if (chipSelectionChange.isUserInput) { + this._propagateChanges(); + } + }); + } + + /** + * Check the tab index as you should not be allowed to focus an empty list. + */ + protected _updateTabIndex(): void { + // If we have 0 chips, we should not allow keyboard focus + this._tabIndex = this._chips.length === 0 ? -1 : 0; + } + + /** + * If the amount of chips changed, we need to update the + * key manager state and focus the next closest chip. + */ + private _updateFocusForDestroyedChips() { + // Move focus to the closest chip. If no other chips remain, focus the chip-listbox itself. + if (this._lastDestroyedChipIndex != null) { + if (this._chips.length) { + const newChipIndex = Math.min(this._lastDestroyedChipIndex, this._chips.length - 1); + this._keyManager.setActiveItem(newChipIndex); + } else { + this.focus(); + } + } + + this._lastDestroyedChipIndex = null; + } +} + diff --git a/src/material-experimental/mdc-chips/chip-option.html b/src/material-experimental/mdc-chips/chip-option.html new file mode 100644 index 000000000000..cae0fd013d27 --- /dev/null +++ b/src/material-experimental/mdc-chips/chip-option.html @@ -0,0 +1,7 @@ + + + + +
+ \ No newline at end of file diff --git a/src/material-experimental/mdc-chips/chip-option.ts b/src/material-experimental/mdc-chips/chip-option.ts new file mode 100644 index 000000000000..c371544926cd --- /dev/null +++ b/src/material-experimental/mdc-chips/chip-option.ts @@ -0,0 +1,188 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {SPACE} from '@angular/cdk/keycodes'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, + ViewEncapsulation +} from '@angular/core'; + +import {MatChip} from './chip'; + + +/** Event object emitted by MatChipOption when selected or deselected. */ +export class MatChipSelectionChange { + constructor( + /** Reference to the chip that emitted the event. */ + public source: MatChipOption, + /** Whether the chip that emitted the event is selected. */ + public selected: boolean, + /** Whether the selection change was a result of a user interaction. */ + public isUserInput = false) { } +} + +/** + * An extension of the MatChip component that supports chip selection. + * Used with MatChipListbox. + */ +@Component({ + moduleId: module.id, + selector: 'mat-basic-chip-option, mat-chip-option', + templateUrl: 'chip-option.html', + styleUrls: ['chips.css'], + inputs: ['color', 'disabled', 'disableRipple'], + host: { + 'role': 'option', + '[class.mat-mdc-chip-disabled]': 'disabled', + '[class.mat-mdc-chip-highlighted]': 'highlighted', + '[class.mat-mdc-chip-with-avatar]': 'leadingIcon', + '[class.mat-mdc-chip-with-trailing-icon]': 'trailingIcon', + '[class.mat-mdc-chip-selected]': 'selected', + '[id]': 'id', + '[tabIndex]': 'disabled ? null : -1', + '[attr.disabled]': 'disabled || null', + '[attr.aria-disabled]': 'disabled.toString()', + '[attr.aria-selected]': 'ariaSelected', + '(click)': '_handleClick($event)', + '(keydown)': '_handleKeydown($event)', + '(focus)': 'focus()', + '(blur)': '_blur()', + '(transitionend)': '_chipFoundation.handleTransitionEnd($event)' + }, + providers: [{provide: MatChip, useExisting: MatChipOption}], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MatChipOption extends MatChip { + + /** Whether the chip list is selectable. */ + chipListSelectable: boolean = true; + + /** Whether the chip list is in multi-selection mode. */ + _chipListMultiple: boolean = false; + + /** + * Whether or not the chip is selectable. + * + * When a chip is not selectable, changes to its selected state are always + * ignored. By default an option chip is selectable, and it becomes + * non-selectable if its parent chip list is not selectable. + */ + @Input() + get selectable(): boolean { + return this._selectable && this.chipListSelectable; + } + set selectable(value: boolean) { + this._selectable = coerceBooleanProperty(value); + } + protected _selectable: boolean = true; + + /** Whether the chip is selected. */ + @Input() + get selected(): boolean { + return this._chipFoundation.isSelected(); + } + set selected(value: boolean) { + if (!this.selectable) { + return; + } + const coercedValue = coerceBooleanProperty(value); + if (coercedValue != this._chipFoundation.isSelected()) { + this._chipFoundation.setSelected(coerceBooleanProperty(value)); + this._dispatchSelectionChange(); + } + } + + /** The ARIA selected applied to the chip. */ + get ariaSelected(): string | null { + // Remove the `aria-selected` when the chip is deselected in single-selection mode, because + // it adds noise to NVDA users where "not selected" will be read out for each chip. + return this.selectable && (this._chipListMultiple || this.selected) ? + this.selected.toString() : null; + } + + /** The unstyled chip selector for this component. */ + protected basicChipAttrName = 'mat-basic-chip-option'; + + /** Emitted when the chip is selected or deselected. */ + @Output() readonly selectionChange: EventEmitter = + new EventEmitter(); + + /** Selects the chip. */ + select(): void { + if (!this.selectable) { + return; + } else if (!this.selected) { + this._chipFoundation.setSelected(true); + this._dispatchSelectionChange(); + } + } + + /** Deselects the chip. */ + deselect(): void { + if (!this.selectable) { + return; + } else if (this.selected) { + this._chipFoundation.setSelected(false); + this._dispatchSelectionChange(); + } + } + + /** Selects this chip and emits userInputSelection event */ + selectViaInteraction(): void { + if (!this.selectable) { + return; + } else if (!this.selected) { + this._chipFoundation.setSelected(true); + this._dispatchSelectionChange(true); + } + } + + /** Toggles the current selected state of this chip. */ + toggleSelected(isUserInput: boolean = false): boolean { + if (!this.selectable) { + return this.selected; + } + + this._chipFoundation.setSelected(!this.selected); + this._dispatchSelectionChange(isUserInput); + return this.selected; + } + + /** Emits a selection change event. */ + private _dispatchSelectionChange(isUserInput = false) { + this.selectionChange.emit({ + source: this, + isUserInput, + selected: this.selected + }); + } + + /** Handles custom key presses. */ + _handleKeydown(event: KeyboardEvent): void { + if (this.disabled) { + return; + } + + switch (event.keyCode) { + case SPACE: + this.toggleSelected(true); + + // Always prevent space from scrolling the page since the list has focus + event.preventDefault(); + break; + default: + this._handleInteraction(event); + } + } +} diff --git a/src/material-experimental/mdc-chips/chip-row.html b/src/material-experimental/mdc-chips/chip-row.html new file mode 100644 index 000000000000..4f6bc3bf3987 --- /dev/null +++ b/src/material-experimental/mdc-chips/chip-row.html @@ -0,0 +1,9 @@ +
+ +
+
+ +
+
+ +
diff --git a/src/material-experimental/mdc-chips/chip-row.ts b/src/material-experimental/mdc-chips/chip-row.ts new file mode 100644 index 000000000000..b3904c24a643 --- /dev/null +++ b/src/material-experimental/mdc-chips/chip-row.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; +import {BACKSPACE, DELETE} from '@angular/cdk/keycodes'; +import {MatChip} from './chip'; + +/** + * An extension of the MatChip component used with MatChipGrid and + * the matChipInputFor directive. + */ +@Component({ + moduleId: module.id, + selector: 'mat-chip-row, mat-basic-chip-row', + templateUrl: 'chip-row.html', + styleUrls: ['chips.css'], + inputs: ['color', 'disabled', 'disableRipple'], + host: { + 'role': 'row', + '[class.mat-mdc-chip-disabled]': 'disabled', + '[class.mat-mdc-chip-highlighted]': 'highlighted', + '[class.mat-mdc-chip-with-avatar]': 'leadingIcon', + '[class.mat-mdc-chip-with-trailing-icon]': 'trailingIcon', + '[id]': 'id', + '[tabIndex]': 'disabled ? null : -1', + '[attr.disabled]': 'disabled || null', + '[attr.aria-disabled]': 'disabled.toString()', + '(click)': '_handleClick($event)', + '(keydown)': '_handleKeydown($event)', + '(focus)': 'focus()', + '(blur)': '_blur()', + '(transitionend)': '_chipFoundation.handleTransitionEnd($event)' + }, + providers: [{provide: MatChip, useExisting: MatChipRow}], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MatChipRow extends MatChip { + protected basicChipAttrName = 'mat-basic-chip-row'; + + /** Handle custom key presses. */ + _handleKeydown(event: KeyboardEvent): void { + if (this.disabled) { + return; + } + + switch (event.keyCode) { + case DELETE: + case BACKSPACE: + // Remove the focused chip + this.remove(); + // Always prevent so page navigation does not occur + event.preventDefault(); + break; + default: + this._handleInteraction(event); + } + } +} diff --git a/src/material-experimental/mdc-chips/chip-set.ts b/src/material-experimental/mdc-chips/chip-set.ts new file mode 100644 index 000000000000..450cfc6373d4 --- /dev/null +++ b/src/material-experimental/mdc-chips/chip-set.ts @@ -0,0 +1,254 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import { + AfterContentInit, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + ElementRef, + Input, + OnDestroy, + QueryList, + ViewEncapsulation +} from '@angular/core'; +import {MDCChipSetAdapter, MDCChipSetFoundation} from '@material/chips'; +import {MatChip, MatChipEvent} from './chip'; +import {merge, Observable, Subject, Subscription} from 'rxjs'; +import {startWith, takeUntil} from 'rxjs/operators'; + +let uid = 0; + +/** + * Basic container component for the MatChip component. + * + * Extended by MatChipListbox and MatChipGrid for different interaction patterns. + */ +@Component({ + moduleId: module.id, + selector: 'mat-chip-set', + template: '', + styleUrls: ['chips.css'], + host: { + 'class': 'mat-mdc-chip-set mdc-chip-set', + 'role': 'presentation', + // TODO: replace this binding with use of AriaDescriber + '[attr.aria-describedby]': '_ariaDescribedby || null', + '[id]': '_uid', + }, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MatChipSet implements AfterContentInit, AfterViewInit, OnDestroy { + /** Subscription to remove changes in chips. */ + private _chipRemoveSubscription: Subscription | null; + + /** Subscription to chip interactions. */ + private _chipInteractionSubscription: Subscription | null; + + /** + * When a chip is destroyed, we store the index of the destroyed chip until the chips + * query list notifies about the update. This is necessary because we cannot determine an + * appropriate chip that should receive focus until the array of chips updated completely. + */ + protected _lastDestroyedChipIndex: number | null = null; + + /** The MDC foundation containing business logic for MDC chip-set. */ + protected _chipSetFoundation: MDCChipSetFoundation; + + /** Subject that emits when the component has been destroyed. */ + protected _destroyed = new Subject(); + + /** + * Implementation of the MDC chip-set adapter interface. + * These methods are called by the chip set foundation. + */ + protected _chipSetAdapter: MDCChipSetAdapter = { + hasClass: (className) => this._hasMdcClass(className), + // No-op. We keep track of chips via ContentChildren, which will be updated when a chip is + // removed. + removeChip: () => {}, + // No-op for base chip set. MatChipListbox overrides the adapter to provide this method. + setSelected: () => {} + }; + + /** The aria-describedby attribute on the chip list for improved a11y. */ + _ariaDescribedby: string; + + /** Uid of the chip set */ + _uid: string = `mat-mdc-chip-set-${uid++}`; + + /** + * Map from class to whether the class is enabled. + * Enabled classes are set on the MDC chip-set div. + */ + _mdcClasses: {[key: string]: boolean} = {}; + + /** Whether the chip set is disabled. */ + @Input() + get disabled(): boolean { return this._disabled; } + set disabled(value: boolean) { + this._disabled = coerceBooleanProperty(value); + this._syncChipsState(); + } + protected _disabled: boolean = false; + + /** Whether the chip list contains chips or not. */ + get empty(): boolean { return this._chips.length === 0; } + + /** Whether any of the chips inside of this chip-set has focus. */ + get focused(): boolean { return this._hasFocusedChip(); } + + /** Combined stream of all of the child chips' remove events. */ + get chipRemoveChanges(): Observable { + return merge(...this._chips.map(chip => chip.removed)); + } + + /** Combined stream of all of the child chips' interaction events. */ + get chipInteractionChanges(): Observable { + return merge(...this._chips.map(chip => chip.interaction)); + } + + /** The chips that are part of this chip set. */ + @ContentChildren(MatChip) _chips: QueryList; + + constructor(protected _elementRef: ElementRef, + protected _changeDetectorRef: ChangeDetectorRef) { + this._chipSetFoundation = new MDCChipSetFoundation(this._chipSetAdapter); + } + + ngAfterViewInit() { + this._chipSetFoundation.init(); + } + + ngAfterContentInit() { + this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { + if (this.disabled) { + // Since this happens after the content has been + // checked, we need to defer it to the next tick. + Promise.resolve().then(() => { + this._syncChipsState(); + }); + } + + this._resetChips(); + }); + } + + ngOnDestroy() { + this._dropSubscriptions(); + this._destroyed.next(); + this._destroyed.complete(); + this._chipSetFoundation.destroy(); + } + + /** Checks whether any of the chips is focused. */ + protected _hasFocusedChip() { + return this._chips.some(chip => chip._hasFocus); + } + + /** Syncs the chip-set's state with the individual chips. */ + protected _syncChipsState() { + if (this._chips) { + this._chips.forEach(chip => { + chip.disabled = this._disabled; + chip._changeDetectorRef.markForCheck(); + }); + } + } + + /** Sets whether the given CSS class should be applied to the MDC chip. */ + protected _setMdcClass(cssClass: string, active: boolean) { + const classes = this._elementRef.nativeElement.classList; + active ? classes.add(cssClass) : classes.remove(cssClass); + this._changeDetectorRef.markForCheck(); + } + + /** Adapter method that returns true if the chip set has the given MDC class. */ + protected _hasMdcClass(className: string) { + return this._elementRef.nativeElement.classList.contains(className); + } + + /** Updates subscriptions to chip events. */ + private _resetChips() { + this._dropSubscriptions(); + this._subscribeToChipEvents(); + } + + /** Subscribes to events on the child chips. */ + protected _subscribeToChipEvents() { + this._listenToChipsRemove(); + this._listenToChipsInteraction(); + } + + /** Subscribes to chip removal events. */ + private _listenToChipsRemove() { + this._chipRemoveSubscription = this.chipRemoveChanges.subscribe((event: MatChipEvent) => { + this._handleChipRemove(event); + }); + } + + /** Subscribes to chip interaction events. */ + private _listenToChipsInteraction() { + this._chipInteractionSubscription = this.chipInteractionChanges.subscribe((id: string) => { + this._handleChipInteraction(id); + }); + } + + /** + * Called when one of the chips is about to be removed. + * If the removed chip has focus, stores its index so we can refocus. + */ + protected _handleChipRemove(event: MatChipEvent) { + this._chipSetFoundation.handleChipRemoval(event.chip.id); + const chip = event.chip; + const chipIndex: number = this._chips.toArray().indexOf(event.chip); + + // In case the chip that will be removed is currently focused, we temporarily store + // the index in order to be able to determine an appropriate sibling chip that will + // receive focus. + if (this._isValidIndex(chipIndex) && chip._hasFocus) { + this._lastDestroyedChipIndex = chipIndex; + } + } + + /** Notifies the chip set foundation when the user interacts with a chip. */ + protected _handleChipInteraction(id: string) { + this._chipSetFoundation.handleChipInteraction(id); + } + + /** Unsubscribes from all chip events. */ + protected _dropSubscriptions() { + if (this._chipRemoveSubscription) { + this._chipRemoveSubscription.unsubscribe(); + this._chipRemoveSubscription = null; + } + + if (this._chipInteractionSubscription) { + this._chipInteractionSubscription.unsubscribe(); + this._chipInteractionSubscription = null; + } + } + + /** Dummy method for subclasses to override. Base chip set cannot be focused. */ + focus() {} + + /** + * Utility to ensure all indexes are valid. + * + * @param index The index to be checked. + * @returns True if the index is valid for our list of chips. + */ + protected _isValidIndex(index: number): boolean { + return index >= 0 && index < this._chips.length; + } +} + diff --git a/src/material-experimental/mdc-chips/chip-text-control.ts b/src/material-experimental/mdc-chips/chip-text-control.ts new file mode 100644 index 000000000000..0431a10c1983 --- /dev/null +++ b/src/material-experimental/mdc-chips/chip-text-control.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + + +/** Interface for a text control that is used to drive interaction with a mat-chip-list. */ +export interface MatChipTextControl { + /** Unique identifier for the text control. */ + id: string; + + /** The text control's placeholder text. */ + placeholder: string; + + /** Whether the text control has browser focus. */ + focused: boolean; + + /** Whether the text control is empty. */ + empty: boolean; + + /** Focuses the text control. */ + focus(): void; +} + diff --git a/src/material-experimental/mdc-chips/chip.html b/src/material-experimental/mdc-chips/chip.html new file mode 100644 index 000000000000..f7df2f5d98e6 --- /dev/null +++ b/src/material-experimental/mdc-chips/chip.html @@ -0,0 +1,3 @@ + +
+ \ No newline at end of file diff --git a/src/material-experimental/mdc-chips/chip.ts b/src/material-experimental/mdc-chips/chip.ts new file mode 100644 index 000000000000..45ca8867c194 --- /dev/null +++ b/src/material-experimental/mdc-chips/chip.ts @@ -0,0 +1,416 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {Platform} from '@angular/cdk/platform'; +import { + AfterContentInit, + AfterViewInit, + Component, + ChangeDetectionStrategy, + ChangeDetectorRef, + ContentChild, + Directive, + ElementRef, + EventEmitter, + Input, + NgZone, + OnDestroy, + Output, + ViewEncapsulation +} from '@angular/core'; +import { + CanColor, + CanColorCtor, + CanDisable, + CanDisableCtor, + CanDisableRipple, + CanDisableRippleCtor, + mixinColor, + mixinDisabled, + mixinDisableRipple, + RippleConfig, + RippleRenderer, + RippleTarget, +} from '@angular/material/core'; +import {MDCChipAdapter, MDCChipFoundation} from '@material/chips'; +import {Subject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; + + +let uid = 0; + +/** Represents an event fired on an individual `mat-chip`. */ +export interface MatChipEvent { + /** The chip the event was fired on. */ + chip: MatChip; +} + +/** + * Directive to add CSS classes to chip leading icon. + * @docs-private + */ +@Directive({ + selector: 'mat-chip-avatar, [matChipAvatar]', + host: { + 'class': 'mat-mdc-chip-avatar mdc-chip__icon mdc-chip__icon--leading', + 'role': 'img' + } +}) +export class MatChipAvatar { + constructor(private _changeDetectorRef: ChangeDetectorRef, + private _elementRef: ElementRef) {} + + /** Sets whether the given CSS class should be applied to the leading icon. */ + setClass(cssClass: string, active: boolean) { + const element = this._elementRef.nativeElement; + active ? element.addClass(cssClass) : element.removeClass(cssClass); + this._changeDetectorRef.markForCheck(); + } +} + +/** + * Directive to add CSS class to chip trailing icon and notify parent chip + * about trailing icon interactions. + * + * If matChipRemove is used to add this directive, the parent chip will be + * removed when the trailing icon is clicked. + * + * @docs-private + */ +@Directive({ + selector: 'mat-chip-trailing-icon, [matChipTrailingIcon], [matChipRemove]', + host: { + 'class': 'mat-mdc-chip-trailing-icon mdc-chip__icon mdc-chip__icon--trailing', + '[tabIndex]': 'tabIndex', + '[attr.aria-hidden]': '!shouldRemove', + '[attr.role]': 'shouldRemove ? "button" : null', + '(click)': 'interaction.emit($event)', + '(keydown)': 'interaction.emit($event)', + '(blur)': 'parentChip ? parentChip._blur() : {}', + '(focus)': 'parentChip ? parentChip._hasFocus = true : {}' + } +}) +export class MatChipTrailingIcon { + /** Whether interaction with this icon should remove the parent chip. */ + shouldRemove!: boolean; + + /** The MatChip component associated with this icon. */ + @Input() parentChip?: MatChip; + + /** The tab index for this icon. */ + get tabIndex(): number|null { + if ((this.parentChip && this.parentChip.disabled) || !this.shouldRemove) { + return -1; + } + return 0; + } + + /** Emits when the user interacts with the icon. */ + @Output() interaction = new EventEmitter(); + + constructor(private _elementRef: ElementRef) { + this.shouldRemove = this._isMatChipRemoveIcon(); + } + + /** Returns true if the icon was created with the matChipRemove directive. */ + _isMatChipRemoveIcon(): boolean { + return this._elementRef.nativeElement.getAttribute('matChipRemove') !== null; + } +} + +/** + * Directive to add MDC CSS to non-basic chips. + * @docs-private + */ +@Directive({ + selector: 'mat-chip, mat-chip-option, mat-chip-row, [mat-chip], [mat-chip-option], [mat-chip-row]', + host: {'class': 'mat-mdc-chip mdc-chip'} +}) +export class MatChipCssInternalOnly { } + +/** + * Boilerplate for applying mixins to MatChip. + * @docs-private + */ +class MatChipBase { + constructor(public _elementRef: ElementRef) {} +} + +const _MatChipMixinBase: CanColorCtor & CanDisableRippleCtor & CanDisableCtor & typeof MatChipBase = + mixinColor(mixinDisableRipple(mixinDisabled(MatChipBase)), 'primary'); + +/** + * Material design styled Chip base component. Used inside the MatChipSet component. + * + * Extended by MatChipOption and MatChipRow for different interaction patterns. + */ +@Component({ + moduleId: module.id, + selector: 'mat-basic-chip, mat-chip', + inputs: ['color', 'disabled', 'disableRipple'], + exportAs: 'matChip', + templateUrl: 'chip.html', + styleUrls: ['chips.css'], + host: { + '[class.mat-mdc-chip-disabled]': 'disabled', + '[class.mat-mdc-chip-highlighted]': 'highlighted', + '[class.mat-mdc-chip-with-avatar]': 'leadingIcon', + '[class.mat-mdc-chip-with-trailing-icon]': 'trailingIcon', + '[id]': 'id', + '[attr.disabled]': 'disabled || null', + '[attr.aria-disabled]': 'disabled.toString()', + '(transitionend)': '_chipFoundation.handleTransitionEnd($event)' + }, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MatChip extends _MatChipMixinBase implements AfterContentInit, AfterViewInit, + CanColor, CanDisable, CanDisableRipple, RippleTarget, OnDestroy { + /** Emits when the chip is focused. */ + readonly _onFocus = new Subject(); + + /** Emits when the chip is blurred. */ + readonly _onBlur = new Subject(); + + /** Whether the chip has focus. */ + _hasFocus: boolean = false; + + /** Default unique id for the chip. */ + private _uniqueId = `mat-mdc-chip-${uid++}`; + + /** A unique id for the chip. If none is supplied, it will be auto-generated. */ + @Input() id: string = this._uniqueId; + + /** The value of the chip. Defaults to the content inside `` tags. */ + @Input() + get value(): any { + return this._value != undefined + ? this._value + : this._elementRef.nativeElement.textContent; + } + set value(value: any) { this._value = value; } + protected _value: any; + + /** + * Determines whether or not the chip displays the remove styling and emits (removed) events. + */ + @Input() + get removable(): boolean { return this._removable; } + set removable(value: boolean) { + this._removable = coerceBooleanProperty(value); + } + protected _removable: boolean = true; + + /** + * Colors the chip for emphasis as if it were selected. + */ + @Input() + get highlighted(): boolean { return this._highlighted; } + set highlighted(value: boolean) { + this._highlighted = coerceBooleanProperty(value); + } + protected _highlighted: boolean = false; + + /** Emitted when the user interacts with the trailing icon. */ + @Output() trailingIconInteraction = new EventEmitter(); + + /** Emitted when the user interacts with the chip. */ + @Output() interaction = new EventEmitter(); + + /** Emitted when the chip is destroyed. */ + @Output() readonly destroyed: EventEmitter = new EventEmitter(); + + /** Emitted when a chip is to be removed. */ + @Output() readonly removed: EventEmitter = new EventEmitter(); + + /** The MDC foundation containing business logic for MDC chip. */ + _chipFoundation: MDCChipFoundation; + + /** The unstyled chip selector for this component. */ + protected basicChipAttrName = 'mat-basic-chip'; + + /** Subject that emits when the component has been destroyed. */ + private _destroyed = new Subject(); + + /** The ripple renderer for this chip. */ + private _rippleRenderer: RippleRenderer; + + /** + * Implemented as part of RippleTarget. Configures ripple animation to match MDC Ripple. + * @docs-private + */ + rippleConfig: RippleConfig = { + animation: { + enterDuration: 225 /* MDCRippleFoundation.numbers.DEACTIVATION_TIMEOUT_MS */, + exitDuration: 150 /* MDCRippleFoundation.numbers.FG_DEACTIVATION_MS */, + } + }; + + /** + * Implemented as part of RippleTarget. Whether ripples are disabled on interaction. + * @docs-private + */ + get rippleDisabled(): boolean { + return this.disabled || this.disableRipple || this._isBasicChip(); + } + + /** The chip's leading icon. */ + @ContentChild(MatChipAvatar, {static: false}) leadingIcon: MatChipAvatar; + + /** The chip's trailing icon. */ + @ContentChild(MatChipTrailingIcon, {static: false}) trailingIcon: MatChipTrailingIcon; + + /** + * Implementation of the MDC chip adapter interface. + * These methods are called by the chip foundation. + */ + protected _chipAdapter: MDCChipAdapter = { + addClass: (className) => this._setMdcClass(className, true), + removeClass: (className) => this._setMdcClass(className, false), + hasClass: (className) => this._elementRef.nativeElement.classList.contains(className), + addClassToLeadingIcon: (className) => this.leadingIcon.setClass(className, true), + removeClassFromLeadingIcon: (className) => this.leadingIcon.setClass(className, false), + eventTargetHasClass: (target: EventTarget | null, className: string) => { + return target ? (target as Element).classList.contains(className) : false; + }, + notifyInteraction: () => this.interaction.emit(this.id), + notifySelection: () => { + // No-op. We call dispatchSelectionEvent ourselves in MatChipOption, because we want to + // specify whether selection occurred via user input. + }, + notifyTrailingIconInteraction: () => this.trailingIconInteraction.emit(this.id), + notifyRemoval: () => this.removed.emit({chip: this}), + getComputedStyleValue: (propertyName) => { + return window.getComputedStyle(this._elementRef.nativeElement).getPropertyValue(propertyName); + }, + setStyleProperty: (propertyName: string, value: string) => { + this._elementRef.nativeElement.style.setProperty(propertyName, value); + }, + hasLeadingIcon: () => { return !!this.leadingIcon; }, + // The 2 functions below are used by the MDC ripple, which we aren't using, + // so they will never be called + getRootBoundingClientRect: () => this._elementRef.nativeElement.getBoundingClientRect(), + getCheckmarkBoundingClientRect: () => { return null; }, + }; + + constructor( + public _changeDetectorRef: ChangeDetectorRef, + readonly _elementRef: ElementRef, + private _platform: Platform, + private _ngZone: NgZone) { + super(_elementRef); + this._chipFoundation = new MDCChipFoundation(this._chipAdapter); + } + + ngAfterContentInit() { + this._initTrailingIcon(); + } + + ngAfterViewInit() { + this._initRipple(); + this._chipFoundation.init(); + } + + ngOnDestroy() { + this.destroyed.emit({chip: this}); + this._destroyed.next(); + this._destroyed.complete(); + this._rippleRenderer._removeTriggerEvents(); + this._chipFoundation.destroy(); + } + + /** Allows for programmatic focusing of the chip. */ + focus(): void { + if (this.disabled) { + return; + } + + if (!this._hasFocus) { + this._elementRef.nativeElement.focus(); + this._onFocus.next({chip: this}); + } + this._hasFocus = true; + } + + /** Resets the state of the chip when it loses focus. */ + _blur(): void { + this._hasFocus = false; + this._onBlur.next({chip: this}); + } + + /** Handles click events on the chip. */ + _handleClick(event: MouseEvent) { + if (this.disabled) { + event.preventDefault(); + } else { + this._handleInteraction(event); + event.stopPropagation(); + } + } + + /** Registers this chip with the trailing icon, and subscribes to trailing icon events. */ + _initTrailingIcon() { + if (this.trailingIcon) { + this.trailingIcon.parentChip = this; + this._chipFoundation.setShouldRemoveOnTrailingIconClick(this.trailingIcon.shouldRemove); + this._listenToTrailingIconInteraction(); + } + } + + /** Handles interaction with the trailing icon. */ + _listenToTrailingIconInteraction() { + this.trailingIcon.interaction + .pipe(takeUntil(this._destroyed)) + .subscribe((event) => { + if (!this.disabled) { + this._chipFoundation.handleTrailingIconInteraction(event); + } + }); + } + + /** + * Allows for programmatic removal of the chip. Called when the DELETE or BACKSPACE + * keys are pressed. + * + * Informs any listeners of the removal request. Does not remove the chip from the DOM. + */ + remove(): void { + if (this.removable) { + this._chipFoundation.beginExit(); + } + } + + /** Whether this chip is a basic (unstyled) chip. */ + _isBasicChip() { + const element = this._elementRef.nativeElement as HTMLElement; + return element.hasAttribute(this.basicChipAttrName) || + element.tagName.toLowerCase() === this.basicChipAttrName; + } + + /** Sets whether the given CSS class should be applied to the MDC chip. */ + private _setMdcClass(cssClass: string, active: boolean) { + const classes = this._elementRef.nativeElement.classList; + active ? classes.add(cssClass) : classes.remove(cssClass); + this._changeDetectorRef.markForCheck(); + } + + /** Initializes the ripple renderer. */ + private _initRipple() { + this._rippleRenderer = + new RippleRenderer(this, this._ngZone, this._elementRef, this._platform); + this._rippleRenderer.setupTriggerEvents(this._elementRef.nativeElement); + } + + /** Forwards interaction events to the MDC chip foundation. */ + _handleInteraction(event: MouseEvent | KeyboardEvent) { + if (!this.disabled) { + this._chipFoundation.handleInteraction(event); + } + } +} diff --git a/src/material-experimental/mdc-chips/chips.scss b/src/material-experimental/mdc-chips/chips.scss index f61450904c9a..ddb53690e650 100644 --- a/src/material-experimental/mdc-chips/chips.scss +++ b/src/material-experimental/mdc-chips/chips.scss @@ -1 +1,51 @@ -// TODO: MDC core styles here. +@import '@material/chips/mixins'; +@import '../mdc-helpers/mdc-helpers'; + +@include mdc-chip-without-ripple($query: $mat-base-styles-query); +@include mdc-chip-set-core-styles($query: $mat-base-styles-query); + + +// The MDC chip styles related to hover and focus states are intertwined with the MDC ripple styles. +// We currently don't use the MDC ripple due to size concerns, therefore we need to add some +// additional styles to restore these states. +.mat-mdc-chip:not(.mat-mdc-chip-disabled) { + &:hover, &:focus { + &::after { + content: ''; + position: absolute; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.2); + top: 0; + left:0; + pointer-events: none; + } + } +} + +// Angular Material supports disabled chips, which MDC does not. +// Dim the disabled chips and stop MDC from changing the icon color on click. +.mat-mdc-chip-disabled.mat-mdc-chip { + opacity: 0.4; + + .mat-mdc-chip-trailing-icon { + pointer-events: none; + } +} + +// Angular Material supports vertically-stacked chips, which MDC does not. +.mat-mdc-chip-set-stacked { + flex-direction: column; + align-items: flex-start; + + .mat-mdc-standard-chip { + width: 100%; + } +} + +// Add styles for the matChipInputFor input element. +$mat-chip-input-width: 150px; + +input.mat-chip-input { + flex: 1 0 $mat-chip-input-width; +} diff --git a/src/material-experimental/mdc-chips/module.ts b/src/material-experimental/mdc-chips/module.ts index d556ee664fe1..2bc2eaaa8791 100644 --- a/src/material-experimental/mdc-chips/module.ts +++ b/src/material-experimental/mdc-chips/module.ts @@ -6,16 +6,55 @@ * found in the LICENSE file at https://angular.io/license */ +import {ENTER} from '@angular/cdk/keycodes'; import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; -import {MatCommonModule} from '@angular/material/core'; -import {MatChipCell} from './chip-cell'; +import {ErrorStateMatcher, MatCommonModule} from '@angular/material/core'; +import {MatChip, MatChipAvatar, MatChipCssInternalOnly, MatChipTrailingIcon} from './chip'; +import {MatChipRow} from './chip-row'; +import {MatChipOption} from './chip-option'; +import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './chip-default-options'; +import {MatChipInput} from './chip-input'; +import {MatChipSet} from './chip-set'; import {MatChipGrid} from './chip-grid'; +import {MatChipListbox} from './chip-listbox'; @NgModule({ imports: [MatCommonModule, CommonModule], - exports: [MatChipCell, MatChipGrid, MatCommonModule], - declarations: [MatChipCell, MatChipGrid], + exports: [ + MatChip, + MatChipAvatar, + MatChipCssInternalOnly, + MatChipGrid, + MatChipInput, + MatChipListbox, + MatChipOption, + MatChipRow, + MatChipSet, + MatChipTrailingIcon, + MatCommonModule + ], + declarations: [ + MatChip, + MatChipAvatar, + MatChipCssInternalOnly, + MatChipGrid, + MatChipInput, + MatChipListbox, + MatChipOption, + MatChipRow, + MatChipSet, + MatChipTrailingIcon + ], + providers: [ + ErrorStateMatcher, + { + provide: MAT_CHIPS_DEFAULT_OPTIONS, + useValue: { + separatorKeyCodes: [ENTER] + } as MatChipsDefaultOptions + } + ] }) export class MatChipsModule { } diff --git a/src/material-experimental/mdc-chips/public-api.ts b/src/material-experimental/mdc-chips/public-api.ts index 52144a2ff370..dab8fb850ee8 100644 --- a/src/material-experimental/mdc-chips/public-api.ts +++ b/src/material-experimental/mdc-chips/public-api.ts @@ -6,6 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './chip-cell'; +export * from './chip'; +export * from './chip-option'; +export * from './chip-row'; +export * from './chip-set'; +export * from './chip-listbox'; export * from './chip-grid'; export * from './module'; +export * from './chip-input'; +export * from './chip-default-options'; diff --git a/yarn.lock b/yarn.lock index 681964797bad..19049761f651 100644 --- a/yarn.lock +++ b/yarn.lock @@ -522,10 +522,10 @@ dependencies: tslib "^1.9.3" -"@material/auto-init@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@material/auto-init/-/auto-init-1.0.0.tgz#0431b03f1bd533f57c4532657e236fe47711f3ab" - integrity sha512-xlWEjjyQquXMi9xz3RH7HW/5dsLGkVYQnm1WCYw2a6JIpIlipkkLEzBNc//agiQongIBbLndL07gO7WQeSohNA== +"@material/auto-init@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/auto-init/-/auto-init-2.3.0.tgz#3d32507675939ab0d7c9f05b075805ed24d22095" + integrity sha512-WFlX3xdZjp/gWZiY0+poFjO12fl8t8C9M82kkQLb6WaUkoQ1YHeV2xQSDLY4IvwcKXb70IpcU3Tqgh+aoiHC0w== dependencies: "@material/base" "^1.0.0" tslib "^1.9.3" @@ -537,76 +537,77 @@ dependencies: tslib "^1.9.3" -"@material/button@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/button/-/button-1.1.0.tgz#dbb46c953040d3a161346e1d3cd057159b9a3c34" - integrity sha512-P1oZyyC1ELRe26vdnmax+fO3BWNmftDqHDDlQbJ+gfYMDQsNQtZNJU16ZbnVHsnzEXOpFj729imbmuLfnz8Nbg== +"@material/button@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/button/-/button-2.3.0.tgz#eff1349babf3f929bd99f86742df988185247d43" + integrity sha512-xFyZjm0pmisfSU6OHO3Wk2zZX51N9D0DohmaPoXnZt2unfD8bCwRmlmD7TTUQCLnuBoIaqrYShpcRTGp4K5dMw== dependencies: "@material/elevation" "^1.1.0" "@material/feature-targeting" "^0.44.1" - "@material/ripple" "^1.1.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" - "@material/shape" "^1.0.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" -"@material/card@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/card/-/card-1.1.0.tgz#d6e884079f6ae256884e06e3e1b5716c2ae14ab2" - integrity sha512-ZuzMwnFZx0qMTiQK/QHoEClsOQ8TbfIpYm1N5gVvnGgbLF6xxoLD2zZFS9/l3eJxrDOxsrqqgxxfjGlJB5dFRg== +"@material/card@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/card/-/card-2.3.0.tgz#f59ac3b16d21d21af5747a530624f14622286c29" + integrity sha512-udVIanyz3KhZ8IfZjX2Yg+YJxU+oYdP9RbCM3hl7l1aqvNr8WSAy/YZ60Ue54fuN+0CpXiNpz1J8GyDktUZKKg== dependencies: "@material/elevation" "^1.1.0" "@material/feature-targeting" "^0.44.1" - "@material/ripple" "^1.1.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" - "@material/shape" "^1.0.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" -"@material/checkbox@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/checkbox/-/checkbox-1.1.0.tgz#f01c4f70e028ac26aa2bbe4ee6de7d7a1384f701" - integrity sha512-jCrkG8VkN63uH+YRhu7RWIsrYGw2Gu89OEPe+sqGYWsr+BbOuzBBqxrM09rUkXykCyi8gAbxpWZPVGCmcDHtJQ== +"@material/checkbox@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/checkbox/-/checkbox-2.3.0.tgz#3fd2d8ad4d602b8486f841fc561e01107f06b4a3" + integrity sha512-ejDn0CyXITF8mKcBZHCLa0fc0a/agej9o4viMLOXXkUIfOY3NJuY11MyZ07MxAgin11NK9HpUX7cIxu3upV+6w== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/dom" "^1.1.0" "@material/feature-targeting" "^0.44.1" - "@material/ripple" "^1.1.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" "@material/theme" "^1.1.0" tslib "^1.9.3" -"@material/chips@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/chips/-/chips-1.1.0.tgz#e4b698dfaece9c21bf4205eb65909b17388147e8" - integrity sha512-+cIscrfKBFhXMaOHgNWx46U7zctXX6toMMG9lxY+uqH9ALo/NeScdZp/kc0C3z0shvRzXjZfvl5rM28rKjnizw== +"@material/chips@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/chips/-/chips-2.3.0.tgz#c778dfd61ab072a9771e69edb8b00917a6faf558" + integrity sha512-jIyThwx3Ax8mFQSmNtfMsI66zJINPis1zZ5LBFTEDN2+iEf7KUgnke8UmQzQac4fPkdZQpsXuiUrMNe4PGf8iA== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" - "@material/checkbox" "^1.1.0" + "@material/checkbox" "^2.3.0" "@material/elevation" "^1.1.0" - "@material/ripple" "^1.1.0" - "@material/shape" "^1.0.0" + "@material/feature-targeting" "^0.44.1" + "@material/ripple" "^2.3.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" tslib "^1.9.3" -"@material/dialog@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/dialog/-/dialog-1.1.0.tgz#62f6bf31a7329ff4948609fb0e377ea336654972" - integrity sha512-XbFTTdzzRhjClLMDFxz8SR/mwwCBpQL6N7Z7UzX7SgD51UJmd19SsXPItFquRKk3mkNuoFBBcR4KJ6julAW+Fg== +"@material/dialog@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/dialog/-/dialog-2.3.0.tgz#4525daf31e1d65e50c67f1cc944e69184134734a" + integrity sha512-T0xEKUW4uY5ZK7S92bdCVjFo9naasTRFNTXccIk+lBkbUlTsCOJMtix+dSJcj8ba8NOZjjq7phfAQqnNKUOhvg== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/dom" "^1.1.0" "@material/elevation" "^1.1.0" "@material/feature-targeting" "^0.44.1" - "@material/ripple" "^1.1.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" - "@material/shape" "^1.0.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" - focus-trap "^4.0.2" + "@material/typography" "^2.3.0" + focus-trap "^5.0.0" tslib "^1.9.3" "@material/dom@^1.1.0": @@ -616,21 +617,21 @@ dependencies: tslib "^1.9.3" -"@material/drawer@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/drawer/-/drawer-1.1.0.tgz#624cc794ac94f09a08551d6b0030163d1dc2b4fb" - integrity sha512-IKQejVv9oSf1sUoAZ7BdV8fhTVDfMfZXfAOQBpw6QyfQujGkNVZatOsvbRcOFPu3naNZFS7brbHRiBH2ArgMQQ== +"@material/drawer@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/drawer/-/drawer-2.3.0.tgz#be81ef24d21f77b2d716dbd8240c02c9dccce2c9" + integrity sha512-BIrRLkqUO2SLkSRPWtXNwF4ZFmP1NOYynNelPZOdHZOIa6HcgmXzCL5p7hInJ5MDar/xMTA84CSO6Vf1byQwHA== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/elevation" "^1.1.0" - "@material/list" "^1.1.0" - "@material/ripple" "^1.1.0" + "@material/list" "^2.3.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" - "@material/shape" "^1.0.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" - focus-trap "^4.0.2" + "@material/typography" "^2.3.0" + focus-trap "^5.0.0" tslib "^1.9.3" "@material/elevation@^1.1.0": @@ -642,179 +643,179 @@ "@material/feature-targeting" "^0.44.1" "@material/theme" "^1.1.0" -"@material/fab@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/fab/-/fab-1.1.0.tgz#37d25f1c48283a19ac5ebb0cdc8a51fc972cb1e0" - integrity sha512-oCvo/4TFri+agTkGvEd5lREuwowInYw6OBbwNmGxpWQs4QGlMGHRS9/h8sfMYNZxs2TrwNUCwf/DK0WZEvniLQ== +"@material/fab@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/fab/-/fab-2.3.0.tgz#d2302b6f21bd84c7fa6d4196079d39bf7abd3cc2" + integrity sha512-XsRTbUMsH4pE/E8CkpAahwNywzwiz73nyBC0YxhzAxfHVO4oGwjcUsVlnFMSITj7K8aEyD/uMyAYwgISuAH83g== dependencies: "@material/animation" "^1.0.0" "@material/elevation" "^1.1.0" "@material/feature-targeting" "^0.44.1" - "@material/ripple" "^1.1.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" - "@material/shape" "^1.0.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" "@material/feature-targeting@^0.44.1": version "0.44.1" resolved "https://registry.yarnpkg.com/@material/feature-targeting/-/feature-targeting-0.44.1.tgz#afafc80294e5efab94bee31a187273d43d34979a" integrity sha512-90cc7njn4aHbH9UxY8qgZth1W5JgOgcEdWdubH1t7sFkwqFxS5g3zgxSBt46TygFBVIXNZNq35Xmg80wgqO7Pg== -"@material/floating-label@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/floating-label/-/floating-label-1.1.0.tgz#505b9f8c7628d0498a18b165c387d4c34840ca60" - integrity sha512-7q7V+9o9XesgMnK11up9z+BcRFwtLIAIqVTCL3liKRARNHuzw9FGrGMKhTJUKvLZ3z0bM1+FmmVlA3q9FJWehQ== +"@material/floating-label@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/floating-label/-/floating-label-2.3.0.tgz#001aa73bc035b8bf913dabc2926361f8c1458f28" + integrity sha512-OYcNmf+mVzW+rphDgVkyWES+SbivA6Y2+0amVDD9E9X6hLjO+L1dtIP3rC7lp0Y2Ey9rkuRORsUoriFvN7xQQw== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/rtl" "^0.42.0" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" tslib "^1.9.3" -"@material/form-field@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/form-field/-/form-field-1.1.0.tgz#4def529d10c416840bdfe5988a320b8c491e7cc1" - integrity sha512-/m382G90rctenVAbPwTzAVtQGm9Bjnnvde03WPijFE6kirczi6pJWns+cFtN///e5nUc5fhBwNdpEnJmoUYvQg== +"@material/form-field@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/form-field/-/form-field-2.3.0.tgz#0f36b50ad8c96d6aa856fa74084e5e2128218724" + integrity sha512-f+RmxxoARS1UV5/yLhZJ9x2b7ueSKZJJCvaUfuppbv/oIkas0dYeW6lIkGo1ienNZyr2ZdW/j2KiL47LahYP/Q== dependencies: "@material/base" "^1.0.0" "@material/feature-targeting" "^0.44.1" - "@material/ripple" "^1.1.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" tslib "^1.9.3" -"@material/grid-list@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/grid-list/-/grid-list-1.1.0.tgz#f92dba86ebd26480bc6407d2c6851214b7d21bbc" - integrity sha512-YukGKO4EzJca5DktfcNKCcrE6B3j/0CznCS0yfAoYagGZxsmmxa1wY4BcObuocIP31/xksRMUCieRbWpKzBhBQ== +"@material/grid-list@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/grid-list/-/grid-list-2.3.0.tgz#159e58321407c49527bc5a02dfbfcb1a8445060a" + integrity sha512-7SrUnhXW2iA0voUUGrwYM0PVx+BK9fsgW8GPakIJFIsRO9ES0VafaOl9DQLsjB8q39jL1nYmwIf/CMfeAlN/dQ== dependencies: "@material/base" "^1.0.0" "@material/feature-targeting" "^0.44.1" "@material/rtl" "^0.42.0" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" tslib "^1.9.3" -"@material/icon-button@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/icon-button/-/icon-button-1.1.0.tgz#a7c649b3549e6e9ecd9c82a8ef124757bfcc98b8" - integrity sha512-3HYOfFhlgpY32HyNt1thQJfTycjy0Cfd4B4IZmt2irfaETdj6JuP0wKs9kdEx1VHTbfWFkJHENArz0lPW+6r2A== +"@material/icon-button@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/icon-button/-/icon-button-2.3.0.tgz#1160ecccd201161a1a38033732beed95a0624497" + integrity sha512-EZhdCn9a9livj9rGTUtDQ3UmF2zkAkaysWzgyDaGUih9rAbZThtGE68DhBWjBoe/+FDAOZ/vXnR23FdCsITmmw== dependencies: "@material/base" "^1.0.0" "@material/feature-targeting" "^0.44.1" - "@material/ripple" "^1.1.0" + "@material/ripple" "^2.3.0" "@material/theme" "^1.1.0" tslib "^1.9.3" -"@material/image-list@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/image-list/-/image-list-1.1.0.tgz#944f51d2ce1e4415407f7eeaae1405e0523b6c83" - integrity sha512-X4ZlrzMbtZ80kkZQQHTR6CJEgBbCuQ3bYcJiXeTYS5jvl7frtiqf7CPkkrAZ4ftaaS/qiJgWMZUz3U5YaaAAsQ== +"@material/image-list@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/image-list/-/image-list-2.3.0.tgz#05f1836f93decf77529f7ce42d73ee6e9d31117e" + integrity sha512-0hV+G743pUhZbkwppY6J7QujWS7TMmYJ6KGLMabArkBwk/BgFk5HTHR7Wn1n3ZCId18JpEBo9XzLcSOZc6fNAg== dependencies: "@material/feature-targeting" "^0.44.1" - "@material/shape" "^1.0.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" "@material/layout-grid@^0.41.0": version "0.41.0" resolved "https://registry.yarnpkg.com/@material/layout-grid/-/layout-grid-0.41.0.tgz#2e7d3be76313e0684d573b10c2c6a88b3230d251" integrity sha512-Sa5RNoTGgfIojqJ9E94p7/k11V6q/tGk7HwKi4AQNAPjxield0zcl3G/SbsSb8YSHoK+D+7OXDN+n11x6EqF7g== -"@material/line-ripple@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/line-ripple/-/line-ripple-1.1.0.tgz#0fef69c14ddf42877f10eed05c1a2489808f1cf4" - integrity sha512-XqCxDNfgkh9zq0IVlTEFVjmQV8hx8m4vxLFM5qwHDDqcKPlX/Lfc8M43fmm9uE1CaJBC6whMGPvOt/dIla+RUg== +"@material/line-ripple@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/line-ripple/-/line-ripple-2.3.0.tgz#e7d876f1dad0a7e418ccfefed875443fc1f303a0" + integrity sha512-gDjUlrM6P182ldY4CYiBMcdcUtch1DWd1osgp5STJXxth6ukRCNIdDigrKCHtyJKB8eXeNORwWs39xWZOGihbA== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/theme" "^1.1.0" tslib "^1.9.3" -"@material/linear-progress@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/linear-progress/-/linear-progress-1.1.0.tgz#559921d4274fce0131fdf146698582d292855d6a" - integrity sha512-MIK0cD/o1rTLREtAfTK3v60h7A0/wAu8/3v9dPTMlAPe+Y5gOarAizYWM3r/bp88sGO4gU8nPK0X9/daD28pNw== +"@material/linear-progress@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/linear-progress/-/linear-progress-2.3.0.tgz#07ab97443e04a8075b2ca8d38bc97a7d60a4bd4a" + integrity sha512-GouSTtxMRpU2lpjhKXcKFTJOePkZoPZTnXN/o4PxUJFeGp9TJcDQwyz0+WcaLPJ/MmCsXZWXJzd+oBWVOkca+A== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/theme" "^1.1.0" tslib "^1.9.3" -"@material/list@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/list/-/list-1.1.0.tgz#e4f0282ceb23c7c8704b9160feab666be5db2ab7" - integrity sha512-+NqVwqhRX08kjDatwZRO/LiiOw9gl7eq2Ogi8hrWkXqYn9ARfUq3K74MeTIit9f7BGPobs86dXBaQ/mjI5HSXg== +"@material/list@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/list/-/list-2.3.0.tgz#07f8d7194495f487d440bbe9fd09a2d3a68cee06" + integrity sha512-uuHWXpaXvPuOaQtQXwrgNc+WTTwBSwU/es65KJJcGrpc/o8Q3mYwMepotMN7E7/L75Wxz2w6uejnoM3zGZfvqg== dependencies: "@material/base" "^1.0.0" "@material/dom" "^1.1.0" "@material/feature-targeting" "^0.44.1" - "@material/ripple" "^1.1.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" - "@material/shape" "^1.0.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" tslib "^1.9.3" -"@material/menu-surface@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/menu-surface/-/menu-surface-1.1.0.tgz#76bf59fca940f53e4b1f41a3e24db8d6f9b73105" - integrity sha512-FjCA6TBgeY+mwNcjfeq5/3TCSyhtXxHrT/xQrz1LyrztuHI4Qu7bTb496pXlfPsZHQuwmo9PqW+pwP78noDf9A== +"@material/menu-surface@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/menu-surface/-/menu-surface-2.3.0.tgz#5d82b4ed7b7124000a2e1fe76034e94f0a433b83" + integrity sha512-jJ1MyeJnEJUO0Z7kNxvqN0xruWbTT2XKHCiApQcJHHkeibWfbWJdhXcx5aO4FMf/TVcy3ADSxDTdvc6AYrBX0g== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/elevation" "^1.1.0" "@material/feature-targeting" "^0.44.1" "@material/rtl" "^0.42.0" - "@material/shape" "^1.0.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" tslib "^1.9.3" -"@material/menu@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/menu/-/menu-1.1.0.tgz#708110143985a9e98d811277e00f8ad85b277738" - integrity sha512-oEubeu4h5EWeaOWbDwstmWkzBSq/2qVtu476CLmaAI5joUilXaPBKpryv4F+ZRIDlVQjs5Ey7uBotxDwDELPQA== +"@material/menu@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/menu/-/menu-2.3.0.tgz#29d7b3557efcf627796cfaf532754f5c6f2d120b" + integrity sha512-XPI6w4x5c9ACwKBdujcTskBlisWlEgrb09Sa+s0vjhqBJVZVAUJT1j0OpG8tArNUqQiFssXBa/JuJIe6sMAK1A== dependencies: "@material/base" "^1.0.0" "@material/feature-targeting" "^0.44.1" - "@material/list" "^1.1.0" - "@material/menu-surface" "^1.1.0" - "@material/ripple" "^1.1.0" + "@material/list" "^2.3.0" + "@material/menu-surface" "^2.3.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" tslib "^1.9.3" -"@material/notched-outline@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/notched-outline/-/notched-outline-1.1.0.tgz#de1326b762e7e0002a90e15a06e34561334d9234" - integrity sha512-J/MCe3an5lTxzYZPzHxblbVZu51OKeum4BU9PeV9WJW+IBGBAnQmT2uf53rqAzhEchIpv39MYmJLPMJghVxWAg== +"@material/notched-outline@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/notched-outline/-/notched-outline-2.3.0.tgz#707c7329a01fe80b269a1c4689769b812d825eb1" + integrity sha512-EDhpCGcMi1gW12qzXsdKHr6aYxa80s94tn9/G1r7eJu/BOVTYt20qjqoYQJE6i8XxcD1t5xcNdQ7StgGpk2MGA== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" - "@material/floating-label" "^1.1.0" + "@material/floating-label" "^2.3.0" "@material/rtl" "^0.42.0" - "@material/shape" "^1.0.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" tslib "^1.9.3" -"@material/radio@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/radio/-/radio-1.1.0.tgz#15154c6c50d39b2b21b079dc2025a822a699e2b7" - integrity sha512-ySocik+l1fInopaA8hDfByJahv1UywUNtUcG7+hLLOhxQx4XVpUJUIjU/dlvVDYJB0QZQatm2asKi2cYVPlIsQ== +"@material/radio@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/radio/-/radio-2.3.0.tgz#6d990edb88a3aea610895d11591d93814faac44a" + integrity sha512-l22cFvA/cZj/tsyDJVk1xayXGUTFFXp8yFe4SuGRW00/hNgGNF44rOSjN5H41iPU3oD1AreQL8r43fWW4eMH8w== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/feature-targeting" "^0.44.1" - "@material/ripple" "^1.1.0" + "@material/ripple" "^2.3.0" "@material/theme" "^1.1.0" tslib "^1.9.3" -"@material/ripple@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/ripple/-/ripple-1.1.0.tgz#236016fb30c8366faf143297df2c38166d84ffbc" - integrity sha512-mkfDBZAmxjpRG7V9TrfOmLxt1g/wvGHCXtYPgvH7W8ozjf53edqxLOFENEdvHbie27y9nyixzXn0gzU0HnxSeA== +"@material/ripple@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/ripple/-/ripple-2.3.0.tgz#4701b2cfecddc4c83ae62c777ae2bf9607988705" + integrity sha512-ejXR0nstERofFhssRyFlwOLgebwm2uGbarHtWZ2/+7QY2Th/Z1wOqNb2h/WRoShsJXK11RUsochb6BJrg30u7w== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" @@ -828,137 +829,137 @@ resolved "https://registry.yarnpkg.com/@material/rtl/-/rtl-0.42.0.tgz#1836e78186c2d8b996f6fbf97adab203535335bc" integrity sha512-VrnrKJzhmspsN8WXHuxxBZ69yM5IwhCUqWr1t1eNfw3ZEvEj7i1g3P31HGowKThIN1dc1Wh4LE14rCISWCtv5w== -"@material/select@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/select/-/select-1.1.0.tgz#07589c1f8b6c9ac2dc1341a85cb7197e4f489474" - integrity sha512-sl5dt+sepqfo4HC82085q9sSCaXoK1swmeQ54Yp8KWlit9CtfV4/QZT83NwF9eEbQF3NmtCdh9T2ueaX//uihg== +"@material/select@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@material/select/-/select-2.3.1.tgz#25d23a554a6d04f3873cf857e37ea509c6b7f33b" + integrity sha512-0NLXp0G0eEFcXpO8VaqfBPBu8yUcXcgiWQwsusT2/ek9bA8eT/rzkwvcK++K9i9V3wTPcWPLz5tnZbPASHfGOw== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" - "@material/floating-label" "^1.1.0" - "@material/line-ripple" "^1.1.0" - "@material/menu" "^1.1.0" - "@material/menu-surface" "^1.1.0" - "@material/notched-outline" "^1.1.0" - "@material/ripple" "^1.1.0" + "@material/floating-label" "^2.3.0" + "@material/line-ripple" "^2.3.0" + "@material/menu" "^2.3.0" + "@material/menu-surface" "^2.3.0" + "@material/notched-outline" "^2.3.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" - "@material/shape" "^1.0.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" tslib "^1.9.3" -"@material/shape@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@material/shape/-/shape-1.0.0.tgz#bef17de1f282e5c71138338a34078d8402308f65" - integrity sha512-zfXEacPQZmH+ujVtaFyfAsYiF46j1QCcFzJeZVouG4pznrbA7XD6614Ywg0wbyWX5iB6hD52ld/IN+R/6oxKqA== +"@material/shape@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@material/shape/-/shape-1.1.1.tgz#7a5368694bc3555e69ea547950904b46fa1024bf" + integrity sha512-Jge/h1XBLjdLlam4QMSzVgM99e/N8+elQROPkltqVP7eyLc17BwM3aP5cLVfZDgrJgvsjUxbgAP1H1j8sqmUyg== dependencies: "@material/feature-targeting" "^0.44.1" -"@material/slider@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/slider/-/slider-1.1.0.tgz#a4940b345d80e2b08d68ba284b9f724119a0a8e8" - integrity sha512-JV/Phpt38LUVb2sQCr2pBsf0faw0E5LI7ag3dNF3hDq1f3F9Vcu+a8oNADi2M+TNlyelltAMKnc9guARwhDLKw== +"@material/slider@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/slider/-/slider-2.3.0.tgz#abf8c17b0e51842c807f26eac0519728185f2c9d" + integrity sha512-TlSwQ0d8ciD2qsZjrNrj0Qmw6AWlqkNJ3CrXVyqeaaL4JzS/oKKk7Ib83fJbwsSSeK6ipH3zGM7138R8OSj94g== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/rtl" "^0.42.0" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" tslib "^1.9.3" -"@material/snackbar@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/snackbar/-/snackbar-1.1.0.tgz#1a7886f4d76903abbd0f0f16db3374c4a27e7d97" - integrity sha512-gPTvAg/Djzz2FHVruGNvdvOkNoeZPctc1hasksBmJPHhQ0nQtc3JsRPfNTLAy32k8aF2+JlrEa1YGMAnltkuPA== +"@material/snackbar@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/snackbar/-/snackbar-2.3.0.tgz#c5e3700834373808eb8845c020de38054008447d" + integrity sha512-/ac/i6MXhIVvQAwJ20SCBFos2TZ66YUJRUfLH8C7UpH+ukbrZNohLwFzoVFOZtbGMOfS5jLzlVTI8LBNezng6Q== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" - "@material/button" "^1.1.0" + "@material/button" "^2.3.0" "@material/dom" "^1.1.0" - "@material/icon-button" "^1.1.0" - "@material/ripple" "^1.1.0" + "@material/icon-button" "^2.3.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" - "@material/shape" "^1.0.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" tslib "^1.9.3" -"@material/switch@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/switch/-/switch-1.1.0.tgz#c64b19523c4a495de9df022940ce5a5d2c21cbb2" - integrity sha512-hwgPzMgZksmJB/hOHQqrT7SZ6TpYaBtkW6BDhCojluBzaMDfDZxeXbNAhAYCO+tJI9p+CndPF+InsocCQDAJXA== +"@material/switch@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/switch/-/switch-2.3.0.tgz#3ea7acc35d645d83a718e40d27e72ccca083e271" + integrity sha512-c21j5VJFUAoey1fPGZaQaRcFeXZcP6dPYewUgFdGURtHnbVHqVVf5GEAcnFsk2NuBN2mDqKLQ3AKbozIAi65hw== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/dom" "^1.1.0" "@material/elevation" "^1.1.0" "@material/feature-targeting" "^0.44.1" - "@material/ripple" "^1.1.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" "@material/theme" "^1.1.0" tslib "^1.9.3" -"@material/tab-bar@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/tab-bar/-/tab-bar-1.1.0.tgz#5bd278ee26d8a3d4bab151e187c694cf2810bc3d" - integrity sha512-uhtIRiyOvBVMZ5Wf6QctPfI/amXfWR9sXjHpYj+So6GPRNgmeQFkRJ1yyxU3KthBoyzfLvj57sgWEsBlSkiJzw== +"@material/tab-bar@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/tab-bar/-/tab-bar-2.3.0.tgz#f2d7b714cdec4ed766a4b1b7f3a871e19a4d47a1" + integrity sha512-p+uDZrhwolnwsaY42kehEc8mbe5ekzjlZEsoCAJCN9AL8VnbsRSS/qePFllhz7roUGrH6AYmYYoHNw1Qx2XT+Q== dependencies: "@material/base" "^1.0.0" "@material/elevation" "^1.1.0" - "@material/tab" "^1.1.0" - "@material/tab-scroller" "^1.1.0" + "@material/tab" "^2.3.0" + "@material/tab-scroller" "^2.3.0" tslib "^1.9.3" -"@material/tab-indicator@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/tab-indicator/-/tab-indicator-1.1.0.tgz#2c37e8fe146d84a99482509cbb785f67336f0463" - integrity sha512-wbZMFCfQo62einr/Ju9PDAeFpRR/Ori5oV2lcsF1uSQXeLxougarwcbk0egWnEtY4Wa6dPgM5P6wQQsYnIeUIQ== +"@material/tab-indicator@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/tab-indicator/-/tab-indicator-2.3.0.tgz#db2eb467303e4b7cd09bc92b23f1d575dd5aee37" + integrity sha512-E9jlESIfjTOoo2pZn7KoTeXEW2jI/2RVtu6IEbhfESLuxPGYmxgO+QxIodEW0VoJhogaa6hR/mFnYBVLbh3JZQ== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/theme" "^1.1.0" tslib "^1.9.3" -"@material/tab-scroller@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/tab-scroller/-/tab-scroller-1.1.0.tgz#081340c54cba7dbc1c299bd996cc223da3ab85e7" - integrity sha512-o5AuG8kwZaghuZ4kvYXIsp2tp3plHoGArF5wM7UgERk+8vMeMRmanagnpYvFwrtGgsta8EQa9sKVZ90dMIDZvg== +"@material/tab-scroller@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/tab-scroller/-/tab-scroller-2.3.0.tgz#226b73770bd5b4b03026124e662fd2e4ce15e425" + integrity sha512-WLAH6po+3SrMJSsnq056aBcLxNMg4TVGyST+TO/wdWOg6QYFSgrjaYpZQEJIkPO+W8Jt54xieI7GPoU5KYB7AA== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/dom" "^1.1.0" - "@material/tab" "^1.1.0" + "@material/tab" "^2.3.0" tslib "^1.9.3" -"@material/tab@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/tab/-/tab-1.1.0.tgz#05191b5db9d41a946c2ea639e1f244c242c2c40c" - integrity sha512-62uttUJURFM36AmXNSrH5ewxJJv632pryEXllpBB1WCHYv6QqRwgfQ6dTjE5xdgSa4k+1X2BJCgmD/0ghc233A== +"@material/tab@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/tab/-/tab-2.3.0.tgz#539763bb1d50b06f6c36ed19fc83bf76d6cb84a9" + integrity sha512-6AdTvTWndPnFC4R/cXo9yZa1M6AGUoeKKaDBaTi2/8cDT0Hmwf5dJTq/T2E0XPtXEW/cELmw7x7yYy3B7IpeQA== dependencies: "@material/base" "^1.0.0" - "@material/ripple" "^1.1.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" - "@material/tab-indicator" "^1.1.0" + "@material/tab-indicator" "^2.3.0" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" tslib "^1.9.3" -"@material/textfield@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/textfield/-/textfield-1.1.0.tgz#ad26b3463f42be86540a7305577b9ecf03bb3b01" - integrity sha512-3+vFrWGVrGxvP4ICsniS8Sn9HHRaBz+/EZgpPnsYRcE0LcE2ABjUlwZQzeHV1h1qc0XmulmLUon8WFGvTxISkA== +"@material/textfield@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@material/textfield/-/textfield-2.3.1.tgz#893183f86b3c6f5913247f56f8516dcbf83a1cd8" + integrity sha512-4w/HyJjNUTnYwuLpOaYEDN45bdFkBcZLC3QfCn3I0B3qrbM1zcpmDUxG0DYyGj1oXCM/sDJXMk07u6CcXpJU5w== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/dom" "^1.1.0" - "@material/floating-label" "^1.1.0" - "@material/line-ripple" "^1.1.0" - "@material/notched-outline" "^1.1.0" - "@material/ripple" "^1.1.0" + "@material/floating-label" "^2.3.0" + "@material/line-ripple" "^2.3.0" + "@material/notched-outline" "^2.3.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" - "@material/shape" "^1.0.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" tslib "^1.9.3" "@material/theme@^1.1.0": @@ -968,38 +969,38 @@ dependencies: "@material/feature-targeting" "^0.44.1" -"@material/toolbar@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/toolbar/-/toolbar-1.1.0.tgz#6304845dc175fe3ebbaaae5245f0f0d12ece7f0a" - integrity sha512-NrO3Z1YH6AGbLjS91BT4BTaUL1ghXHB0Wrre+rT1mmQ7VXtr+AaTSshu9xc7z3wcVuPotR41OOdZqSCDWjzBrg== +"@material/toolbar@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/toolbar/-/toolbar-2.3.0.tgz#6cc4635909606a1dff3c51619f269aff927ada48" + integrity sha512-lRMxWLjThMl8jI9iv0By1ABsbESIclIn3jTugvZUmYQQwPmJQ38YzdA6I27gJKLiljZxteaShIOd1TQHnjI4Fw== dependencies: "@material/base" "^1.0.0" "@material/elevation" "^1.1.0" - "@material/ripple" "^1.1.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" tslib "^1.9.3" -"@material/top-app-bar@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@material/top-app-bar/-/top-app-bar-1.1.0.tgz#2b3412dd5eeba8b24a46ab184e17e68fa0693581" - integrity sha512-fxVKFolNzPG4+LqH8GUxDKfjRZtQNDYX8sGD6c2pKbk1tigyiAGBO/Py31RQQkbAdCKPdmnanGJK2gewaUdw4g== +"@material/top-app-bar@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/top-app-bar/-/top-app-bar-2.3.0.tgz#59805864b3693cd54e2d96c6f2981221e51f9e0e" + integrity sha512-Y62htWToLGABuxHbFYZxHrR99uVAQa7kdlix7tB7h4J5C/SIIKe9plRMh5e0mqhGf006okrAS0h8J+6KM4hh0Q== dependencies: "@material/animation" "^1.0.0" "@material/base" "^1.0.0" "@material/elevation" "^1.1.0" - "@material/ripple" "^1.1.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" - "@material/shape" "^1.0.0" + "@material/shape" "^1.1.1" "@material/theme" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/typography" "^2.3.0" tslib "^1.9.3" -"@material/typography@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@material/typography/-/typography-1.0.0.tgz#327ecfcac5ee3af8a3a102f3f125a761202f4189" - integrity sha512-Oeqbjci1cC7jTE8/n3dwnkqKe9ZeWiaE+rgMtRYtRFw1HvAw14SpGA5EEAS/Li2Hu2KZ50FYCe3HYqShfxtChA== +"@material/typography@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@material/typography/-/typography-2.3.0.tgz#fe2180c697172227f0745cda684ecafdaba3f8dd" + integrity sha512-NtWVVvwG9Te6/kuIl4fEwDcXGCS7mfPgo5CKPyxcK6y0hJHv6yRHpipJT9D4ZlXw0sQx9B33doOK7iYJtwBBZw== dependencies: "@material/feature-targeting" "^0.44.1" @@ -4711,12 +4712,12 @@ flatted@^2.0.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916" integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg== -focus-trap@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-4.0.2.tgz#4ee2b96547c9ea0e4252a2d4b2cca68944194663" - integrity sha512-HtLjfAK7Hp2qbBtLS6wEznID1mPT+48ZnP2nkHzgjpL4kroYHg0CdqJ5cTXk+UO5znAxF5fRUkhdyfgrhh8Lzw== +focus-trap@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-5.0.1.tgz#285f9df2cd9f5ef82dd1abb5d8a70e66cd4f99e3" + integrity sha512-vU7zEdL3y+kfkuwBbT9456JH8QfyemdcdZ2gKMfmgLyAs9NQAkSVQBSZmb9nlb1cVMo+iCsddqeGJog00pd2EQ== dependencies: - tabbable "^3.1.2" + tabbable "^4.0.0" xtend "^4.0.1" follow-redirects@^1.0.0, follow-redirects@^1.2.5, follow-redirects@^1.3.0: @@ -7641,53 +7642,53 @@ matchdep@^2.0.0: resolve "^1.4.0" stack-trace "0.0.10" -material-components-web@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/material-components-web/-/material-components-web-1.1.0.tgz#47446115e474c3dda1f21bffa8361903760cc3ed" - integrity sha512-bTivBr0oZUfw9N32G/PVZ4RRzH8ge7V8CXlGblVB4WE1HJQU5FVgp7cN1z5vYQhEGOCtyT0VlHjzWvOWKMDxFg== +material-components-web@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/material-components-web/-/material-components-web-2.3.1.tgz#04595079e6e9784bfab58519d866ef9df35cb246" + integrity sha512-jDf4T0h8XiSadvrodpkGBtybRI5bAqtfz6MJyq4Kw/PzUPT6jQ5T8EunJyhEXOkjdfWy5VLRfltICTioCtbq6Q== dependencies: "@material/animation" "^1.0.0" - "@material/auto-init" "^1.0.0" + "@material/auto-init" "^2.3.0" "@material/base" "^1.0.0" - "@material/button" "^1.1.0" - "@material/card" "^1.1.0" - "@material/checkbox" "^1.1.0" - "@material/chips" "^1.1.0" - "@material/dialog" "^1.1.0" + "@material/button" "^2.3.0" + "@material/card" "^2.3.0" + "@material/checkbox" "^2.3.0" + "@material/chips" "^2.3.0" + "@material/dialog" "^2.3.0" "@material/dom" "^1.1.0" - "@material/drawer" "^1.1.0" + "@material/drawer" "^2.3.0" "@material/elevation" "^1.1.0" - "@material/fab" "^1.1.0" + "@material/fab" "^2.3.0" "@material/feature-targeting" "^0.44.1" - "@material/floating-label" "^1.1.0" - "@material/form-field" "^1.1.0" - "@material/grid-list" "^1.1.0" - "@material/icon-button" "^1.1.0" - "@material/image-list" "^1.1.0" + "@material/floating-label" "^2.3.0" + "@material/form-field" "^2.3.0" + "@material/grid-list" "^2.3.0" + "@material/icon-button" "^2.3.0" + "@material/image-list" "^2.3.0" "@material/layout-grid" "^0.41.0" - "@material/line-ripple" "^1.1.0" - "@material/linear-progress" "^1.1.0" - "@material/list" "^1.1.0" - "@material/menu" "^1.1.0" - "@material/menu-surface" "^1.1.0" - "@material/notched-outline" "^1.1.0" - "@material/radio" "^1.1.0" - "@material/ripple" "^1.1.0" + "@material/line-ripple" "^2.3.0" + "@material/linear-progress" "^2.3.0" + "@material/list" "^2.3.0" + "@material/menu" "^2.3.0" + "@material/menu-surface" "^2.3.0" + "@material/notched-outline" "^2.3.0" + "@material/radio" "^2.3.0" + "@material/ripple" "^2.3.0" "@material/rtl" "^0.42.0" - "@material/select" "^1.1.0" - "@material/shape" "^1.0.0" - "@material/slider" "^1.1.0" - "@material/snackbar" "^1.1.0" - "@material/switch" "^1.1.0" - "@material/tab" "^1.1.0" - "@material/tab-bar" "^1.1.0" - "@material/tab-indicator" "^1.1.0" - "@material/tab-scroller" "^1.1.0" - "@material/textfield" "^1.1.0" + "@material/select" "^2.3.1" + "@material/shape" "^1.1.1" + "@material/slider" "^2.3.0" + "@material/snackbar" "^2.3.0" + "@material/switch" "^2.3.0" + "@material/tab" "^2.3.0" + "@material/tab-bar" "^2.3.0" + "@material/tab-indicator" "^2.3.0" + "@material/tab-scroller" "^2.3.0" + "@material/textfield" "^2.3.1" "@material/theme" "^1.1.0" - "@material/toolbar" "^1.1.0" - "@material/top-app-bar" "^1.1.0" - "@material/typography" "^1.0.0" + "@material/toolbar" "^2.3.0" + "@material/top-app-bar" "^2.3.0" + "@material/typography" "^2.3.0" math-random@^1.0.1: version "1.0.1" @@ -10994,10 +10995,10 @@ systemjs@0.19.43: dependencies: when "^3.7.5" -tabbable@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-3.1.2.tgz#f2d16cccd01f400e38635c7181adfe0ad965a4a2" - integrity sha512-wjB6puVXTYO0BSFtCmWQubA/KIn7Xvajw0x0l6eJUudMG/EAiJvIUnyNX6xO4NpGrJ16lbD0eUseB9WxW0vlpQ== +tabbable@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261" + integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ== table@^5.0.0: version "5.1.1"