diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 28ab11670608..70fdb7e15027 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,7 +7,7 @@ /src/material/button/** @andrewseguin /src/material/legacy-card/** @andrewseguin /src/material/legacy-checkbox/** @andrewseguin @devversion -/src/material/chips/** @andrewseguin +/src/material/legacy-chips/** @andrewseguin /src/material/datepicker/** @mmalerba @crisbeto @zarend /src/material/legacy-dialog/** @andrewseguin @crisbeto /src/material/divider/** @andrewseguin @crisbeto @@ -114,7 +114,7 @@ /src/material-experimental/mdc-button/** @andrewseguin /src/material/card/** @mmalerba /src/material/checkbox/** @mmalerba -/src/material-experimental/mdc-chips/** @mmalerba +/src/material/chips/** @mmalerba @crisbeto /src/material-experimental/mdc-core/** @crisbeto /src/material/dialog/** @devversion /src/material/form-field/** @devversion @mmalerba diff --git a/.ng-dev/commit-message.mts b/.ng-dev/commit-message.mts index da158c44527b..77908cd46f93 100644 --- a/.ng-dev/commit-message.mts +++ b/.ng-dev/commit-message.mts @@ -43,7 +43,6 @@ export const commitMessage: CommitMessageConfig = { 'material-experimental/mdc-button', 'material/card', 'material/checkbox', - 'material-experimental/mdc-chips', 'material-experimental/mdc-core', 'material/dialog', 'material/form-field', @@ -76,6 +75,7 @@ export const commitMessage: CommitMessageConfig = { 'material/legacy-checkbox', 'material/legacy-checkbox', 'material/chips', + 'material/legacy-chips', 'material/core', 'material/legacy-core', 'material/datepicker', diff --git a/integration/size-test/material-experimental/mdc-chips/BUILD.bazel b/integration/size-test/material-experimental/mdc-chips/BUILD.bazel index e0da7773f1a9..4a2dc13e56c0 100644 --- a/integration/size-test/material-experimental/mdc-chips/BUILD.bazel +++ b/integration/size-test/material-experimental/mdc-chips/BUILD.bazel @@ -3,5 +3,5 @@ load("//integration/size-test:index.bzl", "size_test") size_test( name = "basic", file = "basic.ts", - deps = ["//src/material-experimental/mdc-chips"], + deps = ["//src/material/chips"], ) diff --git a/integration/size-test/material-experimental/mdc-chips/basic.ts b/integration/size-test/material-experimental/mdc-chips/basic.ts index 53c77cbb5d60..9b3af793f233 100644 --- a/integration/size-test/material-experimental/mdc-chips/basic.ts +++ b/integration/size-test/material-experimental/mdc-chips/basic.ts @@ -1,5 +1,5 @@ import {Component, NgModule} from '@angular/core'; -import {MatChipsModule} from '@angular/material-experimental/mdc-chips'; +import {MatChipsModule} from '@angular/material/chips'; /** * Basic component using `MatChipSet` and `MatChip`. Other supported parts of the diff --git a/integration/size-test/material/chips/basic.ts b/integration/size-test/material/chips/basic.ts index 8dc2673f5968..be2d05758e58 100644 --- a/integration/size-test/material/chips/basic.ts +++ b/integration/size-test/material/chips/basic.ts @@ -1,5 +1,5 @@ import {Component, NgModule} from '@angular/core'; -import {MatChipsModule} from '@angular/material/chips'; +import {MatLegacyChipsModule} from '@angular/material/legacy-chips'; /** * Basic component using `MatChipList` and `MatChip`. Other supported parts of the @@ -15,7 +15,7 @@ import {MatChipsModule} from '@angular/material/chips'; export class TestComponent {} @NgModule({ - imports: [MatChipsModule], + imports: [MatLegacyChipsModule], declarations: [TestComponent], bootstrap: [TestComponent], }) diff --git a/src/components-examples/material/chips/BUILD.bazel b/src/components-examples/material/chips/BUILD.bazel index 36f26f2a69c5..539f88e94a84 100644 --- a/src/components-examples/material/chips/BUILD.bazel +++ b/src/components-examples/material/chips/BUILD.bazel @@ -17,10 +17,10 @@ ng_module( "//src/cdk/testing", "//src/cdk/testing/testbed", "//src/material/button", - "//src/material/chips", - "//src/material/chips/testing", "//src/material/icon", "//src/material/legacy-autocomplete", + "//src/material/legacy-chips", + "//src/material/legacy-chips/testing", "//src/material/legacy-form-field", "@npm//@angular/forms", "@npm//@angular/platform-browser", @@ -45,8 +45,8 @@ ng_test_library( ":chips", "//src/cdk/testing", "//src/cdk/testing/testbed", - "//src/material/chips", - "//src/material/chips/testing", + "//src/material/legacy-chips", + "//src/material/legacy-chips/testing", "@npm//@angular/platform-browser", "@npm//@angular/platform-browser-dynamic", ], diff --git a/src/components-examples/material/chips/chips-autocomplete/chips-autocomplete-example.ts b/src/components-examples/material/chips/chips-autocomplete/chips-autocomplete-example.ts index 1e6523d6a536..9b891978d970 100644 --- a/src/components-examples/material/chips/chips-autocomplete/chips-autocomplete-example.ts +++ b/src/components-examples/material/chips/chips-autocomplete/chips-autocomplete-example.ts @@ -2,7 +2,7 @@ import {COMMA, ENTER} from '@angular/cdk/keycodes'; import {Component, ElementRef, ViewChild} from '@angular/core'; import {FormControl} from '@angular/forms'; import {MatAutocompleteSelectedEvent} from '@angular/material/legacy-autocomplete'; -import {MatChipInputEvent} from '@angular/material/chips'; +import {MatLegacyChipInputEvent} from '@angular/material/legacy-chips'; import {Observable} from 'rxjs'; import {map, startWith} from 'rxjs/operators'; @@ -30,7 +30,7 @@ export class ChipsAutocompleteExample { ); } - add(event: MatChipInputEvent): void { + add(event: MatLegacyChipInputEvent): void { const value = (event.value || '').trim(); // Add our fruit diff --git a/src/components-examples/material/chips/chips-form-control/chips-form-control-example.ts b/src/components-examples/material/chips/chips-form-control/chips-form-control-example.ts index 87ce6b95c45c..73a98a163c49 100644 --- a/src/components-examples/material/chips/chips-form-control/chips-form-control-example.ts +++ b/src/components-examples/material/chips/chips-form-control/chips-form-control-example.ts @@ -1,6 +1,6 @@ import {Component} from '@angular/core'; import {FormControl} from '@angular/forms'; -import {MatChipInputEvent} from '@angular/material/chips'; +import {MatLegacyChipInputEvent} from '@angular/material/legacy-chips'; /** * @title Chips with form control @@ -14,7 +14,7 @@ export class ChipsFormControlExample { keywords = new Set(['angular', 'how-to', 'tutorial']); formControl = new FormControl(['angular']); - addKeywordFromInput(event: MatChipInputEvent) { + addKeywordFromInput(event: MatLegacyChipInputEvent) { if (event.value) { this.keywords.add(event.value); event.chipInput!.clear(); diff --git a/src/components-examples/material/chips/chips-harness/chips-harness-example.spec.ts b/src/components-examples/material/chips/chips-harness/chips-harness-example.spec.ts index b114d002c0fa..4a5360d538d9 100644 --- a/src/components-examples/material/chips/chips-harness/chips-harness-example.spec.ts +++ b/src/components-examples/material/chips/chips-harness/chips-harness-example.spec.ts @@ -1,10 +1,13 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; -import {MatChipHarness, MatChipListboxHarness} from '@angular/material/chips/testing'; +import { + MatLegacyChipHarness, + MatLegacyChipListboxHarness, +} from '@angular/material/legacy-chips/testing'; import {HarnessLoader, parallel} from '@angular/cdk/testing'; import {ChipsHarnessExample} from './chips-harness-example'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {MatChipsModule} from '@angular/material/chips'; +import {MatLegacyChipsModule} from '@angular/material/legacy-chips'; describe('ChipsHarnessExample', () => { let fixture: ComponentFixture; @@ -12,7 +15,7 @@ describe('ChipsHarnessExample', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MatChipsModule, NoopAnimationsModule], + imports: [MatLegacyChipsModule, NoopAnimationsModule], declarations: [ChipsHarnessExample], }).compileComponents(); fixture = TestBed.createComponent(ChipsHarnessExample); @@ -21,7 +24,7 @@ describe('ChipsHarnessExample', () => { }); it('should get whether a chip list is disabled', async () => { - const chipList = await loader.getHarness(MatChipListboxHarness); + const chipList = await loader.getHarness(MatLegacyChipListboxHarness); expect(await chipList.isDisabled()).toBeFalse(); @@ -32,13 +35,13 @@ describe('ChipsHarnessExample', () => { }); it('should get the orientation of a chip list', async () => { - const chipList = await loader.getHarness(MatChipListboxHarness); + const chipList = await loader.getHarness(MatLegacyChipListboxHarness); expect(await chipList.getOrientation()).toEqual('horizontal'); }); it('should be able to get the selected chips in a list', async () => { - const chipList = await loader.getHarness(MatChipListboxHarness); + const chipList = await loader.getHarness(MatLegacyChipListboxHarness); const chips = await chipList.getChips(); expect((await chipList.getChips({selected: true})).length).toBe(0); @@ -49,7 +52,7 @@ describe('ChipsHarnessExample', () => { }); it('should be able to trigger chip removal', async () => { - const chip = await loader.getHarness(MatChipHarness); + const chip = await loader.getHarness(MatLegacyChipHarness); expect(fixture.componentInstance.remove).not.toHaveBeenCalled(); await chip.remove(); expect(fixture.componentInstance.remove).toHaveBeenCalled(); diff --git a/src/components-examples/material/chips/chips-input/chips-input-example.ts b/src/components-examples/material/chips/chips-input/chips-input-example.ts index e098f55a8b97..7f8f618930bd 100644 --- a/src/components-examples/material/chips/chips-input/chips-input-example.ts +++ b/src/components-examples/material/chips/chips-input/chips-input-example.ts @@ -1,6 +1,6 @@ import {COMMA, ENTER} from '@angular/cdk/keycodes'; import {Component} from '@angular/core'; -import {MatChipInputEvent} from '@angular/material/chips'; +import {MatLegacyChipInputEvent} from '@angular/material/legacy-chips'; export interface Fruit { name: string; @@ -19,7 +19,7 @@ export class ChipsInputExample { readonly separatorKeysCodes = [ENTER, COMMA] as const; fruits: Fruit[] = [{name: 'Lemon'}, {name: 'Lime'}, {name: 'Apple'}]; - add(event: MatChipInputEvent): void { + add(event: MatLegacyChipInputEvent): void { const value = (event.value || '').trim(); // Add our fruit diff --git a/src/components-examples/material/chips/index.ts b/src/components-examples/material/chips/index.ts index 535101bf92d7..1ef35dc08f98 100644 --- a/src/components-examples/material/chips/index.ts +++ b/src/components-examples/material/chips/index.ts @@ -3,7 +3,7 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; import {ReactiveFormsModule} from '@angular/forms'; import {MatLegacyAutocompleteModule} from '@angular/material/legacy-autocomplete'; -import {MatChipsModule} from '@angular/material/chips'; +import {MatLegacyChipsModule} from '@angular/material/legacy-chips'; import {MatLegacyFormFieldModule} from '@angular/material/legacy-form-field'; import {MatIconModule} from '@angular/material/icon'; import {ChipsAutocompleteExample} from './chips-autocomplete/chips-autocomplete-example'; @@ -44,7 +44,7 @@ const EXAMPLES = [ DragDropModule, MatLegacyAutocompleteModule, MatButtonModule, - MatChipsModule, + MatLegacyChipsModule, MatIconModule, MatLegacyFormFieldModule, ReactiveFormsModule, diff --git a/src/dev-app/chips/BUILD.bazel b/src/dev-app/chips/BUILD.bazel index 76cf698001e8..dfe2fbc3a7b2 100644 --- a/src/dev-app/chips/BUILD.bazel +++ b/src/dev-app/chips/BUILD.bazel @@ -11,10 +11,10 @@ ng_module( ], deps = [ "//src/material/button", - "//src/material/chips", "//src/material/icon", "//src/material/legacy-card", "//src/material/legacy-checkbox", + "//src/material/legacy-chips", "//src/material/legacy-form-field", "//src/material/toolbar", ], diff --git a/src/dev-app/chips/chips-demo.ts b/src/dev-app/chips/chips-demo.ts index 410f72ec2d2c..b4dda715989a 100644 --- a/src/dev-app/chips/chips-demo.ts +++ b/src/dev-app/chips/chips-demo.ts @@ -13,7 +13,7 @@ import {FormsModule} from '@angular/forms'; import {MatButtonModule} from '@angular/material/button'; import {MatLegacyCardModule} from '@angular/material/legacy-card'; import {MatLegacyCheckboxModule} from '@angular/material/legacy-checkbox'; -import {MatChipInputEvent, MatChipsModule} from '@angular/material/chips'; +import {MatLegacyChipInputEvent, MatLegacyChipsModule} from '@angular/material/legacy-chips'; import {ThemePalette} from '@angular/material/core'; import {MatLegacyFormFieldModule} from '@angular/material/legacy-form-field'; import {MatIconModule} from '@angular/material/icon'; @@ -39,7 +39,7 @@ export interface DemoColor { MatButtonModule, MatLegacyCardModule, MatLegacyCheckboxModule, - MatChipsModule, + MatLegacyChipsModule, MatLegacyFormFieldModule, MatIconModule, MatToolbarModule, @@ -79,7 +79,7 @@ export class ChipsDemo { this.message = message; } - add(event: MatChipInputEvent): void { + add(event: MatLegacyChipInputEvent): void { const value = (event.value || '').trim(); // Add our person diff --git a/src/dev-app/mdc-chips/BUILD.bazel b/src/dev-app/mdc-chips/BUILD.bazel index de2d33ece95b..d33cafd52803 100644 --- a/src/dev-app/mdc-chips/BUILD.bazel +++ b/src/dev-app/mdc-chips/BUILD.bazel @@ -11,9 +11,9 @@ ng_module( ], deps = [ "//src/material-experimental/mdc-button", - "//src/material-experimental/mdc-chips", "//src/material/card", "//src/material/checkbox", + "//src/material/chips", "//src/material/core", "//src/material/form-field", "//src/material/icon", diff --git a/src/dev-app/mdc-chips/mdc-chips-demo.ts b/src/dev-app/mdc-chips/mdc-chips-demo.ts index d80cd7ed5737..6a9948f7f3ed 100644 --- a/src/dev-app/mdc-chips/mdc-chips-demo.ts +++ b/src/dev-app/mdc-chips/mdc-chips-demo.ts @@ -10,11 +10,7 @@ import {Component} from '@angular/core'; import {COMMA, ENTER} from '@angular/cdk/keycodes'; import {CommonModule} from '@angular/common'; import {ThemePalette} from '@angular/material/core'; -import { - MatChipInputEvent, - MatChipEditedEvent, - MatChipsModule, -} from '@angular/material-experimental/mdc-chips'; +import {MatChipInputEvent, MatChipEditedEvent, MatChipsModule} from '@angular/material/chips'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {MatButtonModule} from '@angular/material-experimental/mdc-button'; import {MatCardModule} from '@angular/material/card'; diff --git a/src/e2e-app/BUILD.bazel b/src/e2e-app/BUILD.bazel index 659845b0b933..0fe872e74bed 100644 --- a/src/e2e-app/BUILD.bazel +++ b/src/e2e-app/BUILD.bazel @@ -43,7 +43,6 @@ ng_module( "//src/components-examples/material-experimental/mdc-card", "//src/components-examples/private", "//src/material-experimental/mdc-button", - "//src/material-experimental/mdc-chips", "//src/material-experimental/mdc-menu", "//src/material-experimental/mdc-progress-spinner", "//src/material-experimental/mdc-radio", @@ -54,6 +53,7 @@ ng_module( "//src/material/button", "//src/material/card", "//src/material/checkbox", + "//src/material/chips", "//src/material/core", "//src/material/dialog", "//src/material/grid-list", diff --git a/src/e2e-app/mdc-chips/mdc-chips-e2e-module.ts b/src/e2e-app/mdc-chips/mdc-chips-e2e-module.ts index 6894ba16441a..aca625027ccc 100644 --- a/src/e2e-app/mdc-chips/mdc-chips-e2e-module.ts +++ b/src/e2e-app/mdc-chips/mdc-chips-e2e-module.ts @@ -7,7 +7,7 @@ */ import {NgModule} from '@angular/core'; -import {MatChipsModule} from '@angular/material-experimental/mdc-chips'; +import {MatChipsModule} from '@angular/material/chips'; import {MdcChipsE2e} from './mdc-chips-e2e'; @NgModule({ diff --git a/src/material-experimental/_index.scss b/src/material-experimental/_index.scss index ee96a2212f46..e1a73627801e 100644 --- a/src/material-experimental/_index.scss +++ b/src/material-experimental/_index.scss @@ -23,8 +23,6 @@ mdc-fab-density, mdc-fab-theme; @forward './mdc-button/icon-button-theme' as mdc-icon-button-* show mdc-icon-button-color, mdc-icon-button-typography, mdc-icon-button-density, mdc-icon-button-theme; -@forward './mdc-chips/chips-theme' as mdc-chips-* show mdc-chips-color, mdc-chips-typography, - mdc-chips-density, mdc-chips-theme; @forward './mdc-list/list-theme' as mdc-list-* show mdc-list-color, mdc-list-typography, mdc-list-density, mdc-list-theme; @forward './mdc-menu/menu-theme' as mdc-menu-* show mdc-menu-color, mdc-menu-typography, diff --git a/src/material-experimental/config.bzl b/src/material-experimental/config.bzl index e758cb3cee22..2f77bb3ef170 100644 --- a/src/material-experimental/config.bzl +++ b/src/material-experimental/config.bzl @@ -2,8 +2,6 @@ entryPoints = [ "column-resize", "mdc-button", "mdc-button/testing", - "mdc-chips", - "mdc-chips/testing", "mdc-core", "mdc-list", "mdc-list/testing", diff --git a/src/material-experimental/mdc-chips/README.md b/src/material-experimental/mdc-chips/README.md deleted file mode 100644 index 21b8c69d8422..000000000000 --- a/src/material-experimental/mdc-chips/README.md +++ /dev/null @@ -1,114 +0,0 @@ -This is a prototype of an alternate version of `MatChip` built on top of -[MDC Web](https://github.com/material-components/material-components-web). This component is experimental and should not be used in production. - -## How to use -Assuming your application is already up and running using Angular Material, you can add this component by following these steps: - -1. Install `@angular/material-experimental` and MDC Web: - - ```bash - npm i material-components-web @angular/material-experimental - ``` - -2. In your `angular.json`, make sure `node_modules/` is listed as a Sass include path. This is - needed for the Sass compiler to be able to find the MDC Web Sass files. - - ```json - ... - "styles": [ - "src/styles.scss" - ], - "stylePreprocessorOptions": { - "includePaths": [ - "node_modules/" - ] - }, - ... - ``` - -3. Import the experimental `MatChipsModule` and add it to the module that declares your component: - - ```ts - import {MatChipsModule} from '@angular/material-experimental/mdc-chips'; - - @NgModule({ - declarations: [MyComponent], - imports: [MatChipsModule], - }) - export class MyModule {} - ``` - -4. Use the chips in your component's template: - - ```html - - Chip 1 - Chip 2 - - ``` - -5. Add the theme mixins to your Sass: - - ```scss - @use '@angular/material' as mat; - @use '@angular/material-experimental' as mat-experimental; - - $candy-app-primary: mat.define-palette(mat.$indigo-palette); - $candy-app-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); - $candy-app-theme: mat.define-light-theme(( - color: ( - primary: $candy-app-primary, - accent: $candy-app-accent, - ) - )); - - - @include mat-experimental.mdc-chips-theme($candy-app-theme); - ``` - -## API differences - -The API for the MDC-based chips are mostly the same as the current chips implementation, but -one notable difference is that the names of the container and chips have been changed to clarify -when and how they should be used. - -The most basic container that you can use for the chips is with the `` containing a -list of ``: - -```html - - John - Paul - James - -``` - -To use chips as a selection list, use the `` with ``: - -```html - - Extra Small - Small - Medium - Large - -``` - -To use chips with an input, use the `` with ``: - -```html - - - - {{person.name}} - - - - - -``` diff --git a/src/material-experimental/mdc-chips/_chips-theme.import.scss b/src/material-experimental/mdc-chips/_chips-theme.import.scss deleted file mode 100644 index b6e007aef655..000000000000 --- a/src/material-experimental/mdc-chips/_chips-theme.import.scss +++ /dev/null @@ -1,4 +0,0 @@ -@forward 'chips-theme' hide color, density, theme, typography; -@forward 'chips-theme' as mat-mdc-chips-* hide $mat-mdc-chips-mdc-chips-fill-color-default, -$mat-mdc-chips-mdc-chips-icon-color, $mat-mdc-chips-mdc-chips-ink-color-default, -mat-mdc-chips-selected-color; diff --git a/src/material-experimental/mdc-chips/_chips-theme.scss b/src/material-experimental/mdc-chips/_chips-theme.scss deleted file mode 100644 index f0ec74c1d3eb..000000000000 --- a/src/material-experimental/mdc-chips/_chips-theme.scss +++ /dev/null @@ -1,115 +0,0 @@ -@use '@angular/material' as mat; -@use '@material/chips/chip-theme' as mdc-chip-theme; -@use '@material/chips/chip-set' as mdc-chip-set; -@use '@material/theme/theme-color' as mdc-theme-color; -@use '@material/theme/color-palette' as mdc-color-palette; -@use '@material/typography' as mdc-typography; -@use 'sass:color'; -@use 'sass:map'; - -// Customizes the appearance of a chip. Note that ideally we would be doing this using the -// `theme-styles` mixin, however it has the following problems: -// 1. Some of MDC's base styles have **very** high specificity. E.g. setting the background of a -// non-selected, enabled chip uses a selector like `.chip:not(.selected):not(.disabled)` instead of -// just `.chip`. This specificity increase has a ripple effect over all other components that are -// built on top of ours, making overrides extremely difficult and brittle. -// 2. Including the individual mixins allows us to avoid a lot of unnecessary CSS (~35kb in the -// dev app theme). -@mixin _chip-variant($background, $foreground) { - @include mdc-chip-theme.container-color($background); - @include mdc-chip-theme.icon-color($foreground); - @include mdc-chip-theme.trailing-action-color($foreground); - @include mdc-chip-theme.checkmark-color($foreground); - @include mdc-chip-theme.text-label-color($foreground); - - // Technically the avatar is only supposed to have an image, but we also allow for icons. - // Set the color so the icons inherit the correct color. - .mat-mdc-chip-avatar { - color: $foreground; - } -} - -@mixin _colored-chip($palette) { - $background: mat.get-color-from-palette($palette); - $foreground: mat.get-color-from-palette($palette, default-contrast); - - &.mat-mdc-chip-selected, - &.mat-mdc-chip-highlighted { - @include _chip-variant($background, $foreground); - } -} - -@mixin color($config-or-theme) { - $config: mat.get-color-config($config-or-theme); - $primary: map.get($config, primary); - $accent: map.get($config, accent); - $warn: map.get($config, warn); - $foreground: map.get($config, foreground); - $is-dark: map.get($config, is-dark); - - @include mat.private-using-mdc-theme($config) { - .mat-mdc-standard-chip { - @include _chip-variant( - color.mix(mdc-theme-color.prop-value(on-surface), mdc-theme-color.prop-value(surface), 12%), - if($is-dark, mdc-color-palette.$grey-50, mdc-color-palette.$grey-900) - ); - - &.mat-primary { - @include _colored-chip($primary); - } - - &.mat-accent { - @include _colored-chip($accent); - } - - &.mat-warn { - @include _colored-chip($warn); - } - } - } - - .mat-mdc-chip-focus-overlay { - background: map.get($foreground, base); - } -} - -@mixin typography($config-or-theme) { - $config: mat.private-typography-to-2018-config( - mat.get-typography-config($config-or-theme)); - @include mdc-chip-set.core-styles($query: mat.$private-mdc-typography-styles-query); - @include mat.private-using-mdc-typography($config) { - // Note that we don't go through MDC's typography mixin, because it assigns the styles to - // an inner element which makes it difficult for clients to customize. Instead we apply the - // same styles ourselves to the root. - .mat-mdc-standard-chip { - @include mdc-typography.typography(body2, $query: mat.$private-mdc-typography-styles-query); - } - } -} - -@mixin density($config-or-theme) { - $density-scale: mat.get-density-config($config-or-theme); - .mat-mdc-chip { - @include mdc-chip-theme.density($density-scale, $query: mat.$private-mdc-base-styles-query); - } -} - -@mixin theme($theme-or-color-config) { - $theme: mat.private-legacy-get-theme($theme-or-color-config); - @include mat.private-check-duplicate-theme-styles($theme, 'mat-mdc-chips') { - $color: mat.get-color-config($theme); - $density: mat.get-density-config($theme); - $typography: mat.get-typography-config($theme); - - @if $color != null { - @include color($color); - } - @if $density != null { - @include density($density); - } - @if $typography != null { - @include typography($typography); - } - } -} - diff --git a/src/material-experimental/mdc-chips/chip-remove.spec.ts b/src/material-experimental/mdc-chips/chip-remove.spec.ts deleted file mode 100644 index b747e7d0c879..000000000000 --- a/src/material-experimental/mdc-chips/chip-remove.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import {Component} from '@angular/core'; -import {waitForAsync, ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; -import {dispatchKeyboardEvent, dispatchMouseEvent} from '@angular/cdk/testing/private'; -import {By} from '@angular/platform-browser'; -import {SPACE, ENTER} from '@angular/cdk/keycodes'; -import {MatChip, MatChipsModule} from './index'; - -describe('MDC-based Chip Remove', () => { - let fixture: ComponentFixture; - let testChip: TestChip; - let chipNativeElement: HTMLElement; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [MatChipsModule], - declarations: [TestChip], - }); - - TestBed.compileComponents(); - })); - - beforeEach(waitForAsync(() => { - fixture = TestBed.createComponent(TestChip); - testChip = fixture.debugElement.componentInstance; - fixture.detectChanges(); - - const chipDebugElement = fixture.debugElement.query(By.directive(MatChip))!; - chipNativeElement = chipDebugElement.nativeElement; - })); - - describe('basic behavior', () => { - it('should apply a CSS class to the remove icon', fakeAsync(() => { - const buttonElement = chipNativeElement.querySelector('.mdc-evolution-chip__icon--trailing')!; - expect(buttonElement.classList).toContain('mat-mdc-chip-remove'); - })); - - it('should ensure that the button cannot submit its parent form', fakeAsync(() => { - const buttonElement = chipNativeElement.querySelector('button')!; - expect(buttonElement.getAttribute('type')).toBe('button'); - })); - - it('should not set the `type` attribute on non-button elements', fakeAsync(() => { - const buttonElement = chipNativeElement.querySelector('span.mat-mdc-chip-remove')!; - expect(buttonElement.hasAttribute('type')).toBe(false); - })); - - it('should emit (removed) event when exit animation is complete', fakeAsync(() => { - testChip.removable = true; - fixture.detectChanges(); - - chipNativeElement.querySelector('button')!.click(); - fixture.detectChanges(); - flush(); - - expect(testChip.didRemove).toHaveBeenCalled(); - })); - - it('should not make the element aria-hidden when it is focusable', fakeAsync(() => { - const buttonElement = chipNativeElement.querySelector('button')!; - - expect(buttonElement.getAttribute('tabindex')).toBe('-1'); - expect(buttonElement.hasAttribute('aria-hidden')).toBe(false); - })); - - it('should prevent the default SPACE action', fakeAsync(() => { - const buttonElement = chipNativeElement.querySelector('button')!; - - testChip.removable = true; - fixture.detectChanges(); - - const event = dispatchKeyboardEvent(buttonElement, 'keydown', SPACE); - fixture.detectChanges(); - flush(); - - expect(event.defaultPrevented).toBe(true); - })); - - it('should prevent the default ENTER action', fakeAsync(() => { - const buttonElement = chipNativeElement.querySelector('button')!; - - testChip.removable = true; - fixture.detectChanges(); - - const event = dispatchKeyboardEvent(buttonElement, 'keydown', ENTER); - fixture.detectChanges(); - flush(); - - expect(event.defaultPrevented).toBe(true); - })); - - it('should have a focus indicator', fakeAsync(() => { - const buttonElement = chipNativeElement.querySelector('.mdc-evolution-chip__icon--trailing')!; - expect(buttonElement.classList.contains('mat-mdc-focus-indicator')).toBe(true); - })); - - it('should prevent the default click action', fakeAsync(() => { - const buttonElement = chipNativeElement.querySelector('button')!; - const event = dispatchMouseEvent(buttonElement, 'click'); - fixture.detectChanges(); - flush(); - - expect(event.defaultPrevented).toBe(true); - })); - }); -}); - -@Component({ - template: ` - - - - - - - `, -}) -class TestChip { - removable: boolean; - disabled = false; - didRemove = jasmine.createSpy('didRemove spy'); -} diff --git a/src/material-experimental/mdc-chips/chip.spec.ts b/src/material-experimental/mdc-chips/chip.spec.ts deleted file mode 100644 index 37b2127708f1..000000000000 --- a/src/material-experimental/mdc-chips/chip.spec.ts +++ /dev/null @@ -1,233 +0,0 @@ -import {Directionality} from '@angular/cdk/bidi'; -import {Component, DebugElement, ViewChild} from '@angular/core'; -import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; -import {MatRipple} from '@angular/material/core'; -import {By} from '@angular/platform-browser'; -import {Subject} from 'rxjs'; -import {MatChip, MatChipEvent, MatChipSet, MatChipsModule} from './index'; - -describe('MDC-based MatChip', () => { - let fixture: ComponentFixture; - let chipDebugElement: DebugElement; - let chipNativeElement: HTMLElement; - let chipInstance: MatChip; - let chipRippleDebugElement: DebugElement; - let chipRippleInstance: MatRipple; - - let dir = 'ltr'; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [MatChipsModule], - declarations: [ - BasicChip, - SingleChip, - BasicChipWithStaticTabindex, - BasicChipWithBoundTabindex, - ], - providers: [ - { - provide: Directionality, - useFactory: () => ({ - value: dir, - change: new Subject(), - }), - }, - ], - }); - - TestBed.compileComponents(); - })); - - describe('MatBasicChip', () => { - it('adds a class to indicate that it is a basic chip', () => { - fixture = TestBed.createComponent(BasicChip); - fixture.detectChanges(); - - const chip = fixture.nativeElement.querySelector('mat-basic-chip'); - expect(chip.classList).toContain('mat-mdc-basic-chip'); - }); - - it('should be able to set a static tabindex', () => { - fixture = TestBed.createComponent(BasicChipWithStaticTabindex); - fixture.detectChanges(); - - const chip = fixture.nativeElement.querySelector('mat-basic-chip'); - expect(chip.getAttribute('tabindex')).toBe('3'); - }); - - it('should be able to set a dynamic tabindex', () => { - fixture = TestBed.createComponent(BasicChipWithBoundTabindex); - fixture.detectChanges(); - - const chip = fixture.nativeElement.querySelector('mat-basic-chip'); - - expect(chip.getAttribute('tabindex')).toBe('12'); - - fixture.componentInstance.tabindex = 15; - fixture.detectChanges(); - - expect(chip.getAttribute('tabindex')).toBe('15'); - }); - - it('should have its ripple disabled', () => { - fixture = TestBed.createComponent(BasicChip); - fixture.detectChanges(); - chipDebugElement = fixture.debugElement.query(By.directive(MatChip))!; - chipRippleDebugElement = chipDebugElement.query(By.directive(MatRipple))!; - chipRippleInstance = chipRippleDebugElement.injector.get(MatRipple); - expect(chipRippleInstance.disabled) - .withContext('Expected basic chip ripples to be disabled.') - .toBe(true); - }); - }); - - describe('MatChip', () => { - let testComponent: SingleChip; - let primaryAction: HTMLElement; - - beforeEach(() => { - fixture = TestBed.createComponent(SingleChip); - fixture.detectChanges(); - - chipDebugElement = fixture.debugElement.query(By.directive(MatChip))!; - chipNativeElement = chipDebugElement.nativeElement; - chipInstance = chipDebugElement.injector.get(MatChip); - chipRippleDebugElement = chipDebugElement.query(By.directive(MatRipple))!; - chipRippleInstance = chipRippleDebugElement.injector.get(MatRipple); - testComponent = fixture.debugElement.componentInstance; - primaryAction = chipNativeElement.querySelector('.mdc-evolution-chip__action--primary')!; - }); - - it('adds the `mat-chip` class', () => { - expect(chipNativeElement.classList).toContain('mat-mdc-chip'); - }); - - it('does not add the `mat-basic-chip` class', () => { - expect(chipNativeElement.classList).not.toContain('mat-mdc-basic-chip'); - }); - - it('emits destroy on destruction', () => { - spyOn(testComponent, 'chipDestroy').and.callThrough(); - - // Force a destroy callback - testComponent.shouldShow = false; - fixture.detectChanges(); - - expect(testComponent.chipDestroy).toHaveBeenCalledTimes(1); - }); - - it('allows color customization', () => { - expect(chipNativeElement.classList).toContain('mat-primary'); - - testComponent.color = 'warn'; - fixture.detectChanges(); - - expect(chipNativeElement.classList).not.toContain('mat-primary'); - expect(chipNativeElement.classList).toContain('mat-warn'); - }); - - it('allows removal', () => { - spyOn(testComponent, 'chipRemove'); - - chipInstance.remove(); - fixture.detectChanges(); - - expect(testComponent.chipRemove).toHaveBeenCalledWith({chip: chipInstance}); - }); - - it('should be able to disable ripples with the `[rippleDisabled]` input', () => { - expect(chipRippleInstance.disabled) - .withContext('Expected chip ripples to be enabled.') - .toBe(false); - - testComponent.rippleDisabled = true; - fixture.detectChanges(); - - expect(chipRippleInstance.disabled) - .withContext('Expected chip ripples to be disabled.') - .toBe(true); - }); - - it('should disable ripples when the chip is disabled', () => { - expect(chipRippleInstance.disabled) - .withContext('Expected chip ripples to be enabled.') - .toBe(false); - - testComponent.disabled = true; - fixture.detectChanges(); - - expect(chipRippleInstance.disabled) - .withContext('Expected chip ripples to be disabled.') - .toBe(true); - }); - - it('should make disabled chips non-focusable', () => { - testComponent.disabled = true; - fixture.detectChanges(); - expect(primaryAction.hasAttribute('tabindex')).toBe(false); - }); - - it('should return the chip text if value is undefined', () => { - expect(chipInstance.value.trim()).toBe(fixture.componentInstance.name); - }); - - it('should return the chip value if defined', () => { - fixture.componentInstance.value = 123; - fixture.detectChanges(); - - expect(chipInstance.value).toBe(123); - }); - - it('should return the chip value if set to null', () => { - fixture.componentInstance.value = null; - fixture.detectChanges(); - - expect(chipInstance.value).toBeNull(); - }); - }); -}); - -@Component({ - template: ` - -
- - {{name}} - -
-
`, -}) -class SingleChip { - @ViewChild(MatChipSet) chipList: MatChipSet; - disabled: boolean = false; - name: string = 'Test'; - color: string = 'primary'; - removable: boolean = true; - shouldShow: boolean = true; - value: any; - rippleDisabled: boolean = false; - - chipDestroy: (event?: MatChipEvent) => void = () => {}; - chipRemove: (event?: MatChipEvent) => void = () => {}; -} - -@Component({ - template: `Hello`, -}) -class BasicChip {} - -@Component({ - template: `Hello`, -}) -class BasicChipWithStaticTabindex {} - -@Component({ - template: `Hello`, -}) -class BasicChipWithBoundTabindex { - tabindex = 12; -} diff --git a/src/material-experimental/mdc-chips/chip.ts b/src/material-experimental/mdc-chips/chip.ts deleted file mode 100644 index e8b6f8c4233c..000000000000 --- a/src/material-experimental/mdc-chips/chip.ts +++ /dev/null @@ -1,359 +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 {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; -import { - AfterViewInit, - Component, - ChangeDetectionStrategy, - ChangeDetectorRef, - ContentChild, - ElementRef, - EventEmitter, - Inject, - Input, - NgZone, - OnDestroy, - Optional, - Output, - ViewEncapsulation, - ViewChild, - Attribute, -} from '@angular/core'; -import {DOCUMENT} from '@angular/common'; -import { - CanColor, - CanDisable, - CanDisableRipple, - HasTabIndex, - MatRipple, - MAT_RIPPLE_GLOBAL_OPTIONS, - mixinColor, - mixinDisableRipple, - mixinTabIndex, - mixinDisabled, - RippleGlobalOptions, -} from '@angular/material/core'; -import {FocusMonitor} from '@angular/cdk/a11y'; -import {Subject} from 'rxjs'; -import {take} from 'rxjs/operators'; -import {MatChipAvatar, MatChipTrailingIcon, MatChipRemove} from './chip-icons'; -import {MatChipAction} from './chip-action'; -import {BACKSPACE, DELETE} from '@angular/cdk/keycodes'; -import {MAT_CHIP, MAT_CHIP_AVATAR, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens'; - -let uid = 0; - -/** Represents an event fired on an individual `mat-chip`. */ -export interface MatChipEvent { - /** The chip the event was fired on. */ - chip: MatChip; -} - -/** - * Boilerplate for applying mixins to MatChip. - * @docs-private - */ -const _MatChipMixinBase = mixinTabIndex( - mixinColor( - mixinDisableRipple( - mixinDisabled( - class { - constructor(public _elementRef: ElementRef) {} - }, - ), - ), - 'primary', - ), - -1, -); - -/** - * Material design styled Chip base component. Used inside the MatChipSet component. - * - * Extended by MatChipOption and MatChipRow for different interaction patterns. - */ -@Component({ - selector: 'mat-basic-chip, mat-chip', - inputs: ['color', 'disabled', 'disableRipple', 'tabIndex'], - exportAs: 'matChip', - templateUrl: 'chip.html', - styleUrls: ['chip.css'], - host: { - 'class': 'mat-mdc-chip', - '[class.mdc-evolution-chip]': '!_isBasicChip', - '[class.mdc-evolution-chip--disabled]': 'disabled', - '[class.mdc-evolution-chip--with-trailing-action]': '_hasTrailingIcon()', - '[class.mdc-evolution-chip--with-primary-graphic]': 'leadingIcon', - '[class.mdc-evolution-chip--with-primary-icon]': 'leadingIcon', - '[class.mdc-evolution-chip--with-avatar]': 'leadingIcon', - '[class.mat-mdc-chip-with-avatar]': 'leadingIcon', - '[class.mat-mdc-chip-highlighted]': 'highlighted', - '[class.mat-mdc-chip-disabled]': 'disabled', - '[class.mat-mdc-basic-chip]': '_isBasicChip', - '[class.mat-mdc-standard-chip]': '!_isBasicChip', - '[class.mat-mdc-chip-with-trailing-icon]': '_hasTrailingIcon()', - '[class._mat-animation-noopable]': '_animationsDisabled', - '[id]': 'id', - '[attr.role]': 'role', - '[attr.tabindex]': 'role ? tabIndex : null', - '[attr.aria-label]': 'ariaLabel', - '(keydown)': '_handleKeydown($event)', - }, - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [{provide: MAT_CHIP, useExisting: MatChip}], -}) -export class MatChip - extends _MatChipMixinBase - implements AfterViewInit, CanColor, CanDisableRipple, CanDisable, HasTabIndex, OnDestroy -{ - protected _document: Document; - - /** Whether the ripple is centered on the chip. */ - readonly _isRippleCentered = false; - - /** Emits when the chip is focused. */ - readonly _onFocus = new Subject(); - - /** Emits when the chip is blurred. */ - readonly _onBlur = new Subject(); - - /** Whether this chip is a basic (unstyled) chip. */ - readonly _isBasicChip: boolean; - - /** Role for the root of the chip. */ - @Input() role: string | null = null; - - /** Whether the chip has focus. */ - private _hasFocusInternal = false; - - /** Whether moving focus into the chip is pending. */ - private _pendingFocus: boolean; - - /** Whether animations for the chip are enabled. */ - _animationsDisabled: boolean; - - _hasFocus() { - return this._hasFocusInternal; - } - - /** A unique id for the chip. If none is supplied, it will be auto-generated. */ - @Input() id: string = `mat-mdc-chip-${uid++}`; - - /** ARIA label for the content of the chip. */ - @Input('aria-label') ariaLabel: string | null = null; - - private _textElement!: HTMLElement; - - /** - * The value of the chip. Defaults to the content inside - * the `mat-mdc-chip-action-label` element. - */ - @Input() - get value(): any { - return this._value !== undefined ? this._value : this._textElement.textContent!.trim(); - } - 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: BooleanInput) { - 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: BooleanInput) { - this._highlighted = coerceBooleanProperty(value); - } - protected _highlighted: boolean = false; - - /** Emitted when a chip is to be removed. */ - @Output() readonly removed: EventEmitter = new EventEmitter(); - - /** Emitted when the chip is destroyed. */ - @Output() readonly destroyed: EventEmitter = new EventEmitter(); - - /** The unstyled chip selector for this component. */ - protected basicChipAttrName = 'mat-basic-chip'; - - /** The chip's leading icon. */ - @ContentChild(MAT_CHIP_AVATAR) leadingIcon: MatChipAvatar; - - /** The chip's trailing icon. */ - @ContentChild(MAT_CHIP_TRAILING_ICON) trailingIcon: MatChipTrailingIcon; - - /** The chip's trailing remove icon. */ - @ContentChild(MAT_CHIP_REMOVE) removeIcon: MatChipRemove; - - /** Reference to the MatRipple instance of the chip. */ - @ViewChild(MatRipple) ripple: MatRipple; - - /** Action receiving the primary set of user interactions. */ - @ViewChild(MatChipAction) primaryAction: MatChipAction; - - constructor( - public _changeDetectorRef: ChangeDetectorRef, - elementRef: ElementRef, - protected _ngZone: NgZone, - private _focusMonitor: FocusMonitor, - @Inject(DOCUMENT) _document: any, - @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string, - @Optional() - @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) - private _globalRippleOptions?: RippleGlobalOptions, - @Attribute('tabindex') tabIndex?: string, - ) { - super(elementRef); - const element = elementRef.nativeElement; - this._document = _document; - this._animationsDisabled = animationMode === 'NoopAnimations'; - this._isBasicChip = - element.hasAttribute(this.basicChipAttrName) || - element.tagName.toLowerCase() === this.basicChipAttrName; - if (tabIndex != null) { - this.tabIndex = parseInt(tabIndex) ?? this.defaultTabIndex; - } - this._monitorFocus(); - } - - ngAfterViewInit() { - this._textElement = this._elementRef.nativeElement.querySelector('.mat-mdc-chip-action-label')!; - - if (this._pendingFocus) { - this._pendingFocus = false; - this.focus(); - } - } - - ngOnDestroy() { - this._focusMonitor.stopMonitoring(this._elementRef); - this.destroyed.emit({chip: this}); - this.destroyed.complete(); - } - - /** - * Allows for programmatic removal of the chip. - * - * Informs any listeners of the removal request. Does not remove the chip from the DOM. - */ - remove(): void { - if (this.removable) { - this.removed.emit({chip: this}); - } - } - - /** Whether or not the ripple should be disabled. */ - _isRippleDisabled(): boolean { - return ( - this.disabled || - this.disableRipple || - this._animationsDisabled || - this._isBasicChip || - !!this._globalRippleOptions?.disabled - ); - } - - /** Returns whether the chip has a trailing icon. */ - _hasTrailingIcon() { - return !!(this.trailingIcon || this.removeIcon); - } - - /** Handles keyboard events on the chip. */ - _handleKeydown(event: KeyboardEvent) { - if (event.keyCode === BACKSPACE || event.keyCode === DELETE) { - event.preventDefault(); - this.remove(); - } - } - - /** Allows for programmatic focusing of the chip. */ - focus(): void { - if (!this.disabled) { - // If `focus` is called before `ngAfterViewInit`, we won't have access to the primary action. - // This can happen if the consumer tries to focus a chip immediately after it is added. - // Queue the method to be called again on init. - if (this.primaryAction) { - this.primaryAction.focus(); - } else { - this._pendingFocus = true; - } - } - } - - /** Gets the action that contains a specific target node. */ - _getSourceAction(target: Node): MatChipAction | undefined { - return this._getActions().find(action => { - const element = action._elementRef.nativeElement; - return element === target || element.contains(target); - }); - } - - /** Gets all of the actions within the chip. */ - _getActions(): MatChipAction[] { - const result: MatChipAction[] = []; - - if (this.primaryAction) { - result.push(this.primaryAction); - } - - if (this.removeIcon) { - result.push(this.removeIcon); - } - - if (this.trailingIcon) { - result.push(this.trailingIcon); - } - - return result; - } - - /** Handles interactions with the primary action of the chip. */ - _handlePrimaryActionInteraction() { - // Empty here, but is overwritten in child classes. - } - - /** Starts the focus monitoring process on the chip. */ - private _monitorFocus() { - this._focusMonitor.monitor(this._elementRef, true).subscribe(origin => { - const hasFocus = origin !== null; - - if (hasFocus !== this._hasFocusInternal) { - this._hasFocusInternal = hasFocus; - - if (hasFocus) { - this._onFocus.next({chip: this}); - } else { - // When animations are enabled, Angular may end up removing the chip from the DOM a little - // earlier than usual, causing it to be blurred and throwing off the logic in the chip list - // that moves focus not the next item. To work around the issue, we defer marking the chip - // as not focused until the next time the zone stabilizes. - this._ngZone.onStable - .pipe(take(1)) - .subscribe(() => this._ngZone.run(() => this._onBlur.next({chip: this}))); - } - } - }); - } -} diff --git a/src/material-experimental/mdc-chips/public-api.ts b/src/material-experimental/mdc-chips/public-api.ts deleted file mode 100644 index 4971595984dc..000000000000 --- a/src/material-experimental/mdc-chips/public-api.ts +++ /dev/null @@ -1,20 +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 - */ - -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 './tokens'; -export * from './chip-icons'; -export * from './chip-text-control'; -export * from './chip-edit-input'; diff --git a/src/material-experimental/mdc-chips/testing/chip-harness.ts b/src/material-experimental/mdc-chips/testing/chip-harness.ts deleted file mode 100644 index 982ac0003909..000000000000 --- a/src/material-experimental/mdc-chips/testing/chip-harness.ts +++ /dev/null @@ -1,76 +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 { - ComponentHarnessConstructor, - ContentContainerComponentHarness, - HarnessPredicate, - TestKey, -} from '@angular/cdk/testing'; -import {MatChipAvatarHarness} from './chip-avatar-harness'; -import { - ChipAvatarHarnessFilters, - ChipHarnessFilters, - ChipRemoveHarnessFilters, -} from './chip-harness-filters'; -import {MatChipRemoveHarness} from './chip-remove-harness'; - -/** Harness for interacting with a mat-chip in tests. */ -export class MatChipHarness extends ContentContainerComponentHarness { - protected _primaryAction = this.locatorFor('.mdc-evolution-chip__action--primary'); - - static hostSelector = '.mat-mdc-basic-chip, .mat-mdc-chip'; - - /** - * Gets a `HarnessPredicate` that can be used to search for a chip with specific attributes. - * @param options Options for narrowing the search. - * @return a `HarnessPredicate` configured with the given options. - */ - static with( - this: ComponentHarnessConstructor, - options: ChipHarnessFilters = {}, - ): HarnessPredicate { - return new HarnessPredicate(this, options).addOption('text', options.text, (harness, label) => { - return HarnessPredicate.stringMatches(harness.getText(), label); - }); - } - - /** Gets a promise for the text content the option. */ - async getText(): Promise { - return (await this.host()).text({ - exclude: '.mat-mdc-chip-avatar, .mat-mdc-chip-trailing-icon, .mat-icon', - }); - } - - /** Whether the chip is disabled. */ - async isDisabled(): Promise { - return (await this.host()).hasClass('mat-mdc-chip-disabled'); - } - - /** Delete a chip from the set. */ - async remove(): Promise { - const hostEl = await this.host(); - await hostEl.sendKeys(TestKey.DELETE); - } - - /** - * Gets the remove button inside of a chip. - * @param filter Optionally filters which chips are included. - */ - async getRemoveButton(filter: ChipRemoveHarnessFilters = {}): Promise { - return this.locatorFor(MatChipRemoveHarness.with(filter))(); - } - - /** - * Gets the avatar inside a chip. - * @param filter Optionally filters which avatars are included. - */ - async getAvatar(filter: ChipAvatarHarnessFilters = {}): Promise { - return this.locatorForOptional(MatChipAvatarHarness.with(filter))(); - } -} diff --git a/src/material-experimental/mdc-chips/testing/chip-listbox-harness.ts b/src/material-experimental/mdc-chips/testing/chip-listbox-harness.ts deleted file mode 100644 index 01ce7f66a15d..000000000000 --- a/src/material-experimental/mdc-chips/testing/chip-listbox-harness.ts +++ /dev/null @@ -1,76 +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 { - ComponentHarness, - ComponentHarnessConstructor, - HarnessPredicate, - parallel, -} from '@angular/cdk/testing'; -import {ChipListboxHarnessFilters, ChipOptionHarnessFilters} from './chip-harness-filters'; -import {MatChipOptionHarness} from './chip-option-harness'; - -/** Harness for interacting with a mat-chip-listbox in tests. */ -export class MatChipListboxHarness extends ComponentHarness { - static hostSelector = '.mat-mdc-chip-listbox'; - - /** - * Gets a `HarnessPredicate` that can be used to search for a chip listbox with specific - * attributes. - * @param options Options for narrowing the search. - * @return a `HarnessPredicate` configured with the given options. - */ - static with( - this: ComponentHarnessConstructor, - options: ChipListboxHarnessFilters = {}, - ): HarnessPredicate { - return new HarnessPredicate(this, options); - } - - /** Gets whether the chip listbox is disabled. */ - async isDisabled(): Promise { - return (await (await this.host()).getAttribute('aria-disabled')) === 'true'; - } - - /** Gets whether the chip listbox is required. */ - async isRequired(): Promise { - return (await (await this.host()).getAttribute('aria-required')) === 'true'; - } - - /** Gets whether the chip listbox is in multi selection mode. */ - async isMultiple(): Promise { - return (await (await this.host()).getAttribute('aria-multiselectable')) === 'true'; - } - - /** Gets whether the orientation of the chip list. */ - async getOrientation(): Promise<'horizontal' | 'vertical'> { - const orientation = await (await this.host()).getAttribute('aria-orientation'); - return orientation === 'vertical' ? 'vertical' : 'horizontal'; - } - - /** - * Gets the list of chips inside the chip list. - * @param filter Optionally filters which chips are included. - */ - async getChips(filter: ChipOptionHarnessFilters = {}): Promise { - return this.locatorForAll(MatChipOptionHarness.with(filter))(); - } - - /** - * Selects a chip inside the chip list. - * @param filter An optional filter to apply to the child chips. - * All the chips matching the filter will be selected. - */ - async selectChips(filter: ChipOptionHarnessFilters = {}): Promise { - const chips = await this.getChips(filter); - if (!chips.length) { - throw Error(`Cannot find chip matching filter ${JSON.stringify(filter)}`); - } - await parallel(() => chips.map(chip => chip.select())); - } -} diff --git a/src/material-experimental/mdc-core/color/_all-color.import.scss b/src/material-experimental/mdc-core/color/_all-color.import.scss index 647957115d39..75e8678b5850 100644 --- a/src/material-experimental/mdc-core/color/_all-color.import.scss +++ b/src/material-experimental/mdc-core/color/_all-color.import.scss @@ -7,10 +7,6 @@ mat-mdc-color, mat-mdc-density, mat-mdc-theme, mat-mdc-typography; $mat-mdc-button-mat-button-state-target; @forward '../../mdc-button/fab-theme' as mat-mdc-fab-*; @forward '../../mdc-button/icon-button-theme' as mat-mdc-icon-button-*; -@forward '../../mdc-chips/chips-theme' hide color, density, theme, typography; -@forward '../../mdc-chips/chips-theme' as mat-mdc-chips-* hide -$mat-mdc-chips-mdc-chips-fill-color-default, $mat-mdc-chips-mdc-chips-icon-color, -$mat-mdc-chips-mdc-chips-ink-color-default; @forward '../../mdc-radio/radio-theme' hide color, density, theme, typography; @forward '../../mdc-radio/radio-theme' as mat-mdc-radio-* hide $mat-mdc-radio-mdc-radio-baseline-theme-color, $mat-mdc-radio-mdc-radio-disabled-circle-color, diff --git a/src/material-experimental/mdc-core/density/_all-density.import.scss b/src/material-experimental/mdc-core/density/_all-density.import.scss index a86bc25012f5..5da8a9a5ad14 100644 --- a/src/material-experimental/mdc-core/density/_all-density.import.scss +++ b/src/material-experimental/mdc-core/density/_all-density.import.scss @@ -7,10 +7,6 @@ mat-mdc-color, mat-mdc-density, mat-mdc-theme, mat-mdc-typography; $mat-mdc-button-mat-button-state-target; @forward '../../mdc-button/fab-theme' as mat-mdc-fab-*; @forward '../../mdc-button/icon-button-theme' as mat-mdc-icon-button-*; -@forward '../../mdc-chips/chips-theme' hide color, density, theme, typography; -@forward '../../mdc-chips/chips-theme' as mat-mdc-chips-* hide -$mat-mdc-chips-mdc-chips-fill-color-default, $mat-mdc-chips-mdc-chips-icon-color, -$mat-mdc-chips-mdc-chips-ink-color-default; @forward '../../mdc-radio/radio-theme' hide color, density, theme, typography; @forward '../../mdc-radio/radio-theme' as mat-mdc-radio-* hide $mat-mdc-radio-mdc-radio-baseline-theme-color, $mat-mdc-radio-mdc-radio-disabled-circle-color, diff --git a/src/material-experimental/mdc-core/theming/BUILD.bazel b/src/material-experimental/mdc-core/theming/BUILD.bazel index 0421dd8440ad..64334900a6f1 100644 --- a/src/material-experimental/mdc-core/theming/BUILD.bazel +++ b/src/material-experimental/mdc-core/theming/BUILD.bazel @@ -21,7 +21,6 @@ sass_library( deps = [ "//src/material:sass_lib", "//src/material-experimental/mdc-button:mdc_button_scss_lib", - "//src/material-experimental/mdc-chips:mdc_chips_scss_lib", "//src/material-experimental/mdc-core:mdc_core_scss_lib", "//src/material-experimental/mdc-list:mdc_list_scss_lib", "//src/material-experimental/mdc-menu:mdc_menu_scss_lib", @@ -36,6 +35,7 @@ sass_library( "//src/material/autocomplete:autocomplete_scss_lib", "//src/material/card:card_scss_lib", "//src/material/checkbox:checkbox_scss_lib", + "//src/material/chips:chips_scss_lib", "//src/material/dialog:dialog_scss_lib", "//src/material/form-field:form_field_scss_lib", "//src/material/input:input_scss_lib", diff --git a/src/material-experimental/mdc-core/theming/_all-theme.import.scss b/src/material-experimental/mdc-core/theming/_all-theme.import.scss index b1d96e5a99aa..856ecd893b5c 100644 --- a/src/material-experimental/mdc-core/theming/_all-theme.import.scss +++ b/src/material-experimental/mdc-core/theming/_all-theme.import.scss @@ -7,10 +7,6 @@ mat-mdc-color, mat-mdc-density, mat-mdc-theme, mat-mdc-typography; $mat-mdc-button-mat-button-state-target; @forward '../../mdc-button/fab-theme' as mat-mdc-fab-*; @forward '../../mdc-button/icon-button-theme' as mat-mdc-icon-button-*; -@forward '../../mdc-chips/chips-theme' hide color, density, theme, typography; -@forward '../../mdc-chips/chips-theme' as mat-mdc-chips-* hide -$mat-mdc-chips-mdc-chips-fill-color-default, $mat-mdc-chips-mdc-chips-icon-color, -$mat-mdc-chips-mdc-chips-ink-color-default; @forward '../../mdc-radio/radio-theme' hide color, density, theme, typography; @forward '../../mdc-radio/radio-theme' as mat-mdc-radio-* hide $mat-mdc-radio-mdc-radio-baseline-theme-color, $mat-mdc-radio-mdc-radio-disabled-circle-color, @@ -48,7 +44,6 @@ $mat-mdc-table-mdc-data-table-stroke-color, $mat-mdc-table-mdc-data-table-table- @import '../core-theme'; @import '../../mdc-button/button-theme'; -@import '../../mdc-chips/chips-theme'; @import '../../mdc-list/list-theme'; @import '../../mdc-menu/menu-theme'; @import '../../mdc-radio/radio-theme'; diff --git a/src/material-experimental/mdc-core/theming/_all-theme.scss b/src/material-experimental/mdc-core/theming/_all-theme.scss index 8dd61c36560b..f56413c08d8a 100644 --- a/src/material-experimental/mdc-core/theming/_all-theme.scss +++ b/src/material-experimental/mdc-core/theming/_all-theme.scss @@ -4,7 +4,6 @@ @use '../../mdc-button/button-theme'; @use '../../mdc-button/fab-theme'; @use '../../mdc-button/icon-button-theme'; -@use '../../mdc-chips/chips-theme'; @use '../../mdc-list/list-theme'; @use '../../mdc-menu/menu-theme'; @use '../../mdc-radio/radio-theme'; @@ -27,7 +26,7 @@ @include icon-button-theme.theme($theme-or-color-config); @include mat.card-theme($theme-or-color-config); @include mat.checkbox-theme($theme-or-color-config); - @include chips-theme.theme($theme-or-color-config); + @include mat.chips-theme($theme-or-color-config); @include list-theme.theme($theme-or-color-config); @include menu-theme.theme($theme-or-color-config); @include paginator-theme.theme($theme-or-color-config); diff --git a/src/material-experimental/mdc-core/typography/_all-typography.import.scss b/src/material-experimental/mdc-core/typography/_all-typography.import.scss index de47bfd36ce9..0625b478d0bc 100644 --- a/src/material-experimental/mdc-core/typography/_all-typography.import.scss +++ b/src/material-experimental/mdc-core/typography/_all-typography.import.scss @@ -7,10 +7,6 @@ mat-mdc-color, mat-mdc-density, mat-mdc-theme, mat-mdc-typography; $mat-mdc-button-mat-button-state-target; @forward '../../mdc-button/fab-theme' as mat-mdc-fab-*; @forward '../../mdc-button/icon-button-theme' as mat-mdc-icon-button-*; -@forward '../../mdc-chips/chips-theme' hide color, density, theme, typography; -@forward '../../mdc-chips/chips-theme' as mat-mdc-chips-* hide -$mat-mdc-chips-mdc-chips-fill-color-default, $mat-mdc-chips-mdc-chips-icon-color, -$mat-mdc-chips-mdc-chips-ink-color-default; @forward '../../mdc-radio/radio-theme' hide color, density, theme, typography; @forward '../../mdc-radio/radio-theme' as mat-mdc-radio-* hide $mat-mdc-radio-mdc-radio-baseline-theme-color, $mat-mdc-radio-mdc-radio-disabled-circle-color, diff --git a/src/material/_index.scss b/src/material/_index.scss index 1cc1f66557e3..2887210352ec 100644 --- a/src/material/_index.scss +++ b/src/material/_index.scss @@ -20,7 +20,7 @@ private-all-legacy-component-densities; @forward './core/theming/theming' show private-check-duplicate-theme-styles, private-legacy-get-theme, private-is-theme-object; - @forward './core/style/layout-common' as private-* show private-fill; +@forward './core/style/layout-common' as private-* show private-fill; @forward './core/style/private' show private-theme-elevation, private-animation-noop; @forward './core/style/vendor-prefixes' as private-* show private-user-select, private-position-sticky, private-color-adjust, private-clip-path; @@ -91,6 +91,8 @@ checkbox-legacy-theme, checkbox-legacy-color, checkbox-legacy-typography; @forward './checkbox/checkbox-theme' as checkbox-* show checkbox-theme, checkbox-color, checkbox-typography; +@forward './legacy-chips/chips-theme' as legacy-chips-* show legacy-chips-theme, + legacy-chips-color, legacy-chips-typography; @forward './chips/chips-theme' as chips-* show chips-theme, chips-color, chips-typography; @forward './datepicker/datepicker-theme' as datepicker-* show datepicker-theme, datepicker-color, datepicker-typography, datepicker-date-range-colors; diff --git a/src/material/_theming.scss b/src/material/_theming.scss index 90f6ce94d6fa..ad2d2306eb90 100644 --- a/src/material/_theming.scss +++ b/src/material/_theming.scss @@ -14,7 +14,7 @@ @forward './button/button-legacy-index'; @forward './legacy-card/card-legacy-index'; @forward './legacy-checkbox/checkbox-legacy-index'; -@forward './chips/chips-legacy-index'; +@forward './legacy-chips/chips-legacy-index'; @forward './datepicker/datepicker-legacy-index'; @forward './legacy-dialog/dialog-legacy-index'; @forward './divider/divider-legacy-index'; diff --git a/src/material/chips/BUILD.bazel b/src/material/chips/BUILD.bazel index e68bc0966952..9848a57d2243 100644 --- a/src/material/chips/BUILD.bazel +++ b/src/material/chips/BUILD.bazel @@ -14,41 +14,55 @@ ng_module( name = "chips", srcs = glob( ["**/*.ts"], - exclude = ["**/*.spec.ts"], + exclude = [ + "**/*.spec.ts", + ], ), - assets = [":chips.css"] + glob(["**/*.html"]), + assets = [ + ":chip_scss", + ":chip_set_scss", + ] + glob(["**/*.html"]), deps = [ - "//src/cdk/a11y", - "//src/cdk/bidi", - "//src/cdk/coercion", - "//src/cdk/collections", - "//src/cdk/keycodes", - "//src/cdk/platform", + "//src:dev_mode_types", "//src/material/core", - "//src/material/legacy-form-field", + "//src/material/form-field", + "@npm//@angular/animations", + "@npm//@angular/common", "@npm//@angular/core", "@npm//@angular/forms", - "@npm//rxjs", ], ) sass_library( name = "chips_scss_lib", srcs = glob(["**/_*.scss"]), - deps = ["//src/material/core:core_scss_lib"], + deps = [ + "//:mdc_sass_lib", + "//src/material/core:core_scss_lib", + ], ) sass_binary( - name = "chips_scss", - src = "chips.scss", + name = "chip_scss", + src = "chip.scss", deps = [ + "//:mdc_sass_lib", "//src/cdk:sass_lib", "//src/material/core:core_scss_lib", ], ) +sass_binary( + name = "chip_set_scss", + src = "chip-set.scss", + deps = [ + "//:mdc_sass_lib", + "//src/material/core:core_scss_lib", + ], +) + ng_test_library( - name = "unit_test_sources", + name = "chips_tests_lib", srcs = glob( ["**/*.spec.ts"], exclude = ["**/*.e2e.spec.ts"], @@ -62,9 +76,10 @@ ng_test_library( "//src/cdk/testing", "//src/cdk/testing/private", "//src/material/core", - "//src/material/legacy-form-field", - "//src/material/legacy-input", + "//src/material/form-field", + "//src/material/input", "@npm//@angular/animations", + "@npm//@angular/common", "@npm//@angular/forms", "@npm//@angular/platform-browser", "@npm//rxjs", @@ -73,7 +88,9 @@ ng_test_library( ng_web_test_suite( name = "unit_tests", - deps = [":unit_test_sources"], + deps = [ + ":chips_tests_lib", + ], ) markdown_to_html( diff --git a/src/material/chips/README.md b/src/material/chips/README.md index d7a05de16e1d..a2c8db6cb8a9 100644 --- a/src/material/chips/README.md +++ b/src/material/chips/README.md @@ -1 +1 @@ -Please see the official documentation at https://material.angular.io/components/component/chips \ No newline at end of file +Please see the official documentation at https://material.angular.io/components/component/chips diff --git a/src/material/chips/_chips-theme.import.scss b/src/material/chips/_chips-theme.import.scss index a03aa8abd053..b6e007aef655 100644 --- a/src/material/chips/_chips-theme.import.scss +++ b/src/material/chips/_chips-theme.import.scss @@ -1,14 +1,4 @@ -@forward '../core/style/private.import'; -@forward '../core/theming/theming.import'; -@forward '../core/typography/typography-utils.import'; -@forward 'chips-theme' hide $chip-remove-font-size, color, theme, typography; -@forward 'chips-theme' as mat-* hide mat-chip-element-color, mat-chip-theme-color, mat-color, -mat-density, mat-ripple-background, mat-theme, mat-typography; -@forward 'chips-theme' as mat-chips-* hide $mat-chips-chip-remove-font-size, -mat-chips-chip-element-color, mat-chips-chip-theme-color, mat-chips-density, -mat-chips-ripple-background; - -@import '../core/style/private'; -@import '../core/theming/palette'; -@import '../core/theming/theming'; -@import '../core/typography/typography-utils'; +@forward 'chips-theme' hide color, density, theme, typography; +@forward 'chips-theme' as mat-mdc-chips-* hide $mat-mdc-chips-mdc-chips-fill-color-default, +$mat-mdc-chips-mdc-chips-icon-color, $mat-mdc-chips-mdc-chips-ink-color-default, +mat-mdc-chips-selected-color; diff --git a/src/material/chips/_chips-theme.scss b/src/material/chips/_chips-theme.scss index 8dc300fa43ee..23530f9d9769 100644 --- a/src/material/chips/_chips-theme.scss +++ b/src/material/chips/_chips-theme.scss @@ -1,109 +1,100 @@ +@use 'sass:color'; @use 'sass:map'; -@use 'sass:meta'; -@use '../core/style/private'; +@use '@material/chips/chip-theme' as mdc-chip-theme; +@use '@material/chips/chip-set' as mdc-chip-set; +@use '@material/theme/theme-color' as mdc-theme-color; +@use '@material/theme/color-palette' as mdc-color-palette; +@use '@material/typography' as mdc-typography; +@use '../core/mdc-helpers/mdc-helpers'; @use '../core/theming/theming'; @use '../core/typography/typography'; -@use '../core/typography/typography-utils'; -$chip-remove-font-size: 18px; - -@mixin _element-color($foreground, $background) { - background-color: $background; - color: $foreground; - - .mat-chip-remove { +// Customizes the appearance of a chip. Note that ideally we would be doing this using the +// `theme-styles` mixin, however it has the following problems: +// 1. Some of MDC's base styles have **very** high specificity. E.g. setting the background of a +// non-selected, enabled chip uses a selector like `.chip:not(.selected):not(.disabled)` instead of +// just `.chip`. This specificity increase has a ripple effect over all other components that are +// built on top of ours, making overrides extremely difficult and brittle. +// 2. Including the individual mixins allows us to avoid a lot of unnecessary CSS (~35kb in the +// dev app theme). +@mixin _chip-variant($background, $foreground) { + @include mdc-chip-theme.container-color($background); + @include mdc-chip-theme.icon-color($foreground); + @include mdc-chip-theme.trailing-action-color($foreground); + @include mdc-chip-theme.checkmark-color($foreground); + @include mdc-chip-theme.text-label-color($foreground); + + // Technically the avatar is only supposed to have an image, but we also allow for icons. + // Set the color so the icons inherit the correct color. + .mat-mdc-chip-avatar { color: $foreground; - opacity: 0.4; - } -} - - -// Applies the background color for a ripple element. -// If the color value provided is not a Sass color, -// we assume that we've been given a CSS variable. -// Since we can't perform alpha-blending on a CSS variable, -// we instead add the opacity directly to the ripple element. -@mixin _ripple-background($palette, $default-contrast, $opacity) { - $background-color: theming.get-color-from-palette($palette, $default-contrast, $opacity); - background-color: $background-color; - @if (meta.type-of($background-color) != color) { - opacity: $opacity; } } -@mixin _palette-styles($palette) { - @include _element-color(theming.get-color-from-palette($palette, default-contrast), - theming.get-color-from-palette($palette)); +@mixin _colored-chip($palette) { + $background: theming.get-color-from-palette($palette); + $foreground: theming.get-color-from-palette($palette, default-contrast); - .mat-ripple-element { - @include _ripple-background($palette, default-contrast, 0.1); + &.mat-mdc-chip-selected, + &.mat-mdc-chip-highlighted { + @include _chip-variant($background, $foreground); } } @mixin color($config-or-theme) { $config: theming.get-color-config($config-or-theme); - $is-dark-theme: map.get($config, is-dark); $primary: map.get($config, primary); $accent: map.get($config, accent); $warn: map.get($config, warn); - $background: map.get($config, background); $foreground: map.get($config, foreground); + $is-dark: map.get($config, is-dark); - $unselected-background: theming.get-color-from-palette($background, unselected-chip); - $unselected-foreground: theming.get-color-from-palette($foreground, text); + @include mdc-helpers.using-mdc-theme($config) { + .mat-mdc-standard-chip { + @include _chip-variant( + color.mix(mdc-theme-color.prop-value(on-surface), mdc-theme-color.prop-value(surface), 12%), + if($is-dark, mdc-color-palette.$grey-50, mdc-color-palette.$grey-900) + ); - .mat-chip.mat-standard-chip { - @include _element-color($unselected-foreground, $unselected-background); - - &:not(.mat-chip-disabled) { - &:active { - @include private.private-theme-elevation(3, $config); + &.mat-primary { + @include _colored-chip($primary); } - .mat-chip-remove:hover { - opacity: 0.54; + &.mat-accent { + @include _colored-chip($accent); } - } - &.mat-chip-disabled { - opacity: 0.4; - } - - &::after { - background: map.get($foreground, base); + &.mat-warn { + @include _colored-chip($warn); + } } } - .mat-chip.mat-standard-chip.mat-chip-selected { - &.mat-primary { - @include _palette-styles($primary); - } - - &.mat-warn { - @include _palette-styles($warn); - } - - &.mat-accent { - @include _palette-styles($accent); - } + .mat-mdc-chip-focus-overlay { + background: map.get($foreground, base); } } @mixin typography($config-or-theme) { - $config: typography.private-typography-to-2014-config( + $config: typography.private-typography-to-2018-config( theming.get-typography-config($config-or-theme)); - .mat-chip { - font-size: typography-utils.font-size($config, body-2); - font-weight: typography-utils.font-weight($config, body-2); - - .mat-chip-trailing-icon.mat-icon, - .mat-chip-remove.mat-icon { - font-size: $chip-remove-font-size; + @include mdc-chip-set.core-styles($query: mdc-helpers.$mdc-typography-styles-query); + @include mdc-helpers.using-mdc-typography($config) { + // Note that we don't go through MDC's typography mixin, because it assigns the styles to + // an inner element which makes it difficult for clients to customize. Instead we apply the + // same styles ourselves to the root. + .mat-mdc-standard-chip { + @include mdc-typography.typography(body2, $query: mdc-helpers.$mdc-typography-styles-query); } } } -@mixin _density($config-or-theme) {} +@mixin density($config-or-theme) { + $density-scale: theming.get-density-config($config-or-theme); + .mat-mdc-chip { + @include mdc-chip-theme.density($density-scale, $query: mdc-helpers.$mdc-base-styles-query); + } +} @mixin theme($theme-or-color-config) { $theme: theming.private-legacy-get-theme($theme-or-color-config); @@ -116,12 +107,10 @@ $chip-remove-font-size: 18px; @include color($color); } @if $density != null { - @include _density($density); + @include density($density); } @if $typography != null { @include typography($typography); } } } - - diff --git a/src/material-experimental/mdc-chips/chip-action.ts b/src/material/chips/chip-action.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-action.ts rename to src/material/chips/chip-action.ts diff --git a/src/material-experimental/mdc-chips/chip-edit-input.spec.ts b/src/material/chips/chip-edit-input.spec.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-edit-input.spec.ts rename to src/material/chips/chip-edit-input.spec.ts diff --git a/src/material-experimental/mdc-chips/chip-edit-input.ts b/src/material/chips/chip-edit-input.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-edit-input.ts rename to src/material/chips/chip-edit-input.ts diff --git a/src/material-experimental/mdc-chips/chip-grid.spec.ts b/src/material/chips/chip-grid.spec.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-grid.spec.ts rename to src/material/chips/chip-grid.spec.ts diff --git a/src/material-experimental/mdc-chips/chip-grid.ts b/src/material/chips/chip-grid.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-grid.ts rename to src/material/chips/chip-grid.ts diff --git a/src/material-experimental/mdc-chips/chip-icons.ts b/src/material/chips/chip-icons.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-icons.ts rename to src/material/chips/chip-icons.ts diff --git a/src/material/chips/chip-input.spec.ts b/src/material/chips/chip-input.spec.ts index 8a71a471da6e..58f1f91e6aec 100644 --- a/src/material/chips/chip-input.spec.ts +++ b/src/material/chips/chip-input.spec.ts @@ -3,27 +3,31 @@ import {COMMA, ENTER, TAB} from '@angular/cdk/keycodes'; import {PlatformModule} from '@angular/cdk/platform'; import {dispatchKeyboardEvent} from '../../cdk/testing/private'; import {Component, DebugElement, ViewChild} from '@angular/core'; -import {waitForAsync, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; -import {MatLegacyFormFieldModule} from '@angular/material/legacy-form-field'; +import {waitForAsync, ComponentFixture, fakeAsync, TestBed, flush} from '@angular/core/testing'; +import {MatFormFieldModule} from '@angular/material/form-field'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {Subject} from 'rxjs'; -import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './chip-default-options'; -import {MatChipInput, MatChipInputEvent} from './chip-input'; -import {MatChipList} from './chip-list'; -import {MatChipsModule} from './index'; - -describe('MatChipInput', () => { +import { + MAT_CHIPS_DEFAULT_OPTIONS, + MatChipGrid, + MatChipInput, + MatChipInputEvent, + MatChipsDefaultOptions, + MatChipsModule, +} from './index'; + +describe('MDC-based MatChipInput', () => { let fixture: ComponentFixture; let testChipInput: TestChipInput; let inputDebugElement: DebugElement; let inputNativeElement: HTMLElement; let chipInputDirective: MatChipInput; - const dir = 'ltr'; + let dir = 'ltr'; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [PlatformModule, MatChipsModule, MatLegacyFormFieldModule, NoopAnimationsModule], + imports: [PlatformModule, MatChipsModule, MatFormFieldModule, NoopAnimationsModule], declarations: [TestChipInput], providers: [ { @@ -72,84 +76,60 @@ describe('MatChipInput', () => { expect(inputNativeElement.getAttribute('placeholder')).toBe('bound placeholder'); }); - it('should propagate the dynamic `placeholder` value to the form field', () => { - fixture.componentInstance.placeholder = 'add a chip'; - fixture.detectChanges(); - - const label: HTMLElement = fixture.nativeElement.querySelector('.mat-form-field-label'); - - expect(label).toBeTruthy(); - expect(label.textContent).toContain('add a chip'); - - fixture.componentInstance.placeholder = "or don't"; - fixture.detectChanges(); - - expect(label.textContent).toContain("or don't"); - }); - it('should become disabled if the list is disabled', () => { expect(inputNativeElement.hasAttribute('disabled')).toBe(false); expect(chipInputDirective.disabled).toBe(false); - fixture.componentInstance.chipListInstance.disabled = true; + fixture.componentInstance.chipGridInstance.disabled = true; fixture.detectChanges(); expect(inputNativeElement.getAttribute('disabled')).toBe('true'); expect(chipInputDirective.disabled).toBe(true); }); - it('should allow focus to escape when tabbing forwards', fakeAsync(() => { - const listElement: HTMLElement = fixture.nativeElement.querySelector('.mat-chip-list'); - - expect(listElement.getAttribute('tabindex')).toBe('0'); + it('should be aria-required if the list is required', () => { + expect(inputNativeElement.hasAttribute('aria-required')).toBe(false); - dispatchKeyboardEvent(inputNativeElement, 'keydown', TAB); + fixture.componentInstance.required = true; fixture.detectChanges(); - expect(listElement.getAttribute('tabindex')) - .withContext('Expected tabIndex to be set to -1 temporarily.') - .toBe('-1'); + expect(inputNativeElement.getAttribute('aria-required')).toBe('true'); + }); + + it('should be required if the list is required', () => { + expect(inputNativeElement.hasAttribute('required')).toBe(false); - tick(); + fixture.componentInstance.required = true; fixture.detectChanges(); - expect(listElement.getAttribute('tabindex')) - .withContext('Expected tabIndex to be reset back to 0') - .toBe('0'); - })); + expect(inputNativeElement.getAttribute('required')).toBe('true'); + }); - it('should not allow focus to escape when tabbing backwards', fakeAsync(() => { - const listElement: HTMLElement = fixture.nativeElement.querySelector('.mat-chip-list'); + it('should allow focus to escape when tabbing forwards', fakeAsync(() => { + const gridElement: HTMLElement = fixture.nativeElement.querySelector('mat-chip-grid'); - expect(listElement.getAttribute('tabindex')).toBe('0'); + expect(gridElement.getAttribute('tabindex')).toBe('0'); - dispatchKeyboardEvent(inputNativeElement, 'keydown', TAB, undefined, {shift: true}); + dispatchKeyboardEvent(gridElement, 'keydown', TAB); fixture.detectChanges(); - expect(listElement.getAttribute('tabindex')) - .withContext('Expected tabindex to remain 0') - .toBe('0'); + expect(gridElement.getAttribute('tabindex')) + .withContext('Expected tabIndex to be set to -1 temporarily.') + .toBe('-1'); - tick(); + flush(); fixture.detectChanges(); - expect(listElement.getAttribute('tabindex')) - .withContext('Expected tabindex to remain 0') + expect(gridElement.getAttribute('tabindex')) + .withContext('Expected tabIndex to be reset back to 0') .toBe('0'); })); - it('should be aria-required if the list is required', () => { - expect(inputNativeElement.hasAttribute('aria-required')).toBe(false); - - fixture.componentInstance.required = true; - fixture.detectChanges(); - - expect(inputNativeElement.getAttribute('aria-required')).toBe('true'); - }); - it('should set input styling classes', () => { - expect(inputNativeElement.classList).toContain('mat-input-element'); - expect(inputNativeElement.classList).toContain('mat-chip-input'); + expect(inputNativeElement.classList).toContain('mat-mdc-input-element'); + expect(inputNativeElement.classList).toContain('mat-mdc-form-field-input-control'); + expect(inputNativeElement.classList).toContain('mat-mdc-chip-input'); + expect(inputNativeElement.classList).toContain('mdc-text-field__input'); }); }); @@ -211,7 +191,7 @@ describe('MatChipInput', () => { TestBed.resetTestingModule() .configureTestingModule({ - imports: [MatChipsModule, MatLegacyFormFieldModule, PlatformModule, NoopAnimationsModule], + imports: [MatChipsModule, MatFormFieldModule, PlatformModule, NoopAnimationsModule], declarations: [TestChipInput], providers: [ { @@ -246,27 +226,47 @@ describe('MatChipInput', () => { dispatchKeyboardEvent(inputNativeElement, 'keydown', ENTER, undefined, {shift: true}); expect(testChipInput.add).not.toHaveBeenCalled(); }); + + it('should set aria-describedby correctly when a non-empty list of ids is passed to setDescribedByIds', fakeAsync(() => { + const ids = ['a', 'b', 'c']; + + testChipInput.chipGridInstance.setDescribedByIds(ids); + flush(); + fixture.detectChanges(); + + expect(inputNativeElement.getAttribute('aria-describedby')).toEqual('a b c'); + })); + + it('should set aria-describedby correctly when an empty list of ids is passed to setDescribedByIds', fakeAsync(() => { + const ids: string[] = []; + + testChipInput.chipGridInstance.setDescribedByIds(ids); + flush(); + fixture.detectChanges(); + + expect(inputNativeElement.getAttribute('aria-describedby')).toBeNull(); + })); }); }); @Component({ template: ` - - Hello - + Hello + - +
`, }) class TestChipInput { - @ViewChild(MatChipList) chipListInstance: MatChipList; - addOnBlur = false; - required = false; + @ViewChild(MatChipGrid) chipGridInstance: MatChipGrid; + addOnBlur: boolean = false; placeholder = ''; + required = false; add(_: MatChipInputEvent) {} } diff --git a/src/material/chips/chip-input.ts b/src/material/chips/chip-input.ts index e90caab3ca84..776f7887d35f 100644 --- a/src/material/chips/chip-input.ts +++ b/src/material/chips/chip-input.ts @@ -7,7 +7,7 @@ */ import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {BACKSPACE, hasModifierKey, TAB} from '@angular/cdk/keycodes'; +import {BACKSPACE, hasModifierKey} from '@angular/cdk/keycodes'; import { AfterContentInit, Directive, @@ -17,10 +17,12 @@ import { Input, OnChanges, OnDestroy, + Optional, Output, } from '@angular/core'; -import {MatChipsDefaultOptions, MAT_CHIPS_DEFAULT_OPTIONS} from './chip-default-options'; -import {MatChipList} from './chip-list'; +import {MatFormField, MAT_FORM_FIELD} from '@angular/material/form-field'; +import {MatChipsDefaultOptions, MAT_CHIPS_DEFAULT_OPTIONS} from './tokens'; +import {MatChipGrid} from './chip-grid'; import {MatChipTextControl} from './chip-text-control'; /** Represents an input event on a `matChipInput`. */ @@ -44,13 +46,16 @@ let nextUniqueId = 0; /** * Directive that adds chip-specific behaviors to an input element inside ``. - * May be placed inside or outside of an ``. + * May be placed inside or outside of a ``. */ @Directive({ selector: 'input[matChipInputFor]', exportAs: 'matChipInput, matChipInputFor', host: { - 'class': 'mat-chip-input mat-input-element', + // TODO: eventually we should remove `mat-input-element` from here since it comes from the + // non-MDC version of the input. It's currently being kept for backwards compatibility, because + // the MDC chips were landed initially with it. + 'class': 'mat-mdc-chip-input mat-mdc-input-element mdc-text-field__input mat-input-element', '(keydown)': '_keydown($event)', '(keyup)': '_keyup($event)', '(blur)': '_blur()', @@ -59,24 +64,29 @@ let nextUniqueId = 0; '[id]': 'id', '[attr.disabled]': 'disabled || null', '[attr.placeholder]': 'placeholder || null', - '[attr.aria-invalid]': '_chipList && _chipList.ngControl ? _chipList.ngControl.invalid : null', - '[attr.aria-required]': '_chipList && _chipList.required || null', + '[attr.aria-invalid]': '_chipGrid && _chipGrid.ngControl ? _chipGrid.ngControl.invalid : null', + '[attr.aria-describedby]': '_ariaDescribedby || null', + '[attr.aria-required]': '_chipGrid && _chipGrid.required || null', + '[attr.required]': '_chipGrid && _chipGrid.required || null', }, }) -export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, AfterContentInit { +export class MatChipInput implements MatChipTextControl, AfterContentInit, OnChanges, OnDestroy { /** Used to prevent focus moving to chips while user is holding backspace */ private _focusLastChipOnBackspace: boolean; + /** Value for ariaDescribedby property */ + _ariaDescribedby?: string; + /** Whether the control is focused. */ focused: boolean = false; - _chipList: MatChipList; + _chipGrid: MatChipGrid; /** Register input for chip list */ @Input('matChipInputFor') - set chipList(value: MatChipList) { + set chipGrid(value: MatChipGrid) { if (value) { - this._chipList = value; - this._chipList.registerInput(this); + this._chipGrid = value; + this._chipGrid.registerInput(this); } } @@ -102,18 +112,19 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, A this._defaultOptions.separatorKeyCodes; /** Emitted when a chip is to be added. */ - @Output('matChipInputTokenEnd') readonly chipEnd = new EventEmitter(); + @Output('matChipInputTokenEnd') + readonly 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++}`; + @Input() id: string = `mat-mdc-chip-list-input-${nextUniqueId++}`; /** Whether the input is disabled. */ @Input() get disabled(): boolean { - return this._disabled || (this._chipList && this._chipList.disabled); + return this._disabled || (this._chipGrid && this._chipGrid.disabled); } set disabled(value: BooleanInput) { this._disabled = coerceBooleanProperty(value); @@ -131,12 +142,17 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, A constructor( protected _elementRef: ElementRef, @Inject(MAT_CHIPS_DEFAULT_OPTIONS) private _defaultOptions: MatChipsDefaultOptions, + @Optional() @Inject(MAT_FORM_FIELD) formField?: MatFormField, ) { this.inputElement = this._elementRef.nativeElement as HTMLInputElement; + + if (formField) { + this.inputElement.classList.add('mat-mdc-form-field-input-control'); + } } - ngOnChanges(): void { - this._chipList.stateChanges.next(); + ngOnChanges() { + this._chipGrid.stateChanges.next(); } ngOnDestroy(): void { @@ -150,17 +166,11 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, A /** Utility method to make host definition/tests more clear. */ _keydown(event?: KeyboardEvent) { if (event) { - // 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.keyCode === TAB && !hasModifierKey(event, 'shiftKey')) { - this._chipList._allowFocusEscape(); - } - // To prevent the user from accidentally deleting chips when pressing BACKSPACE continuously, // We focus the last chip on backspace only after the user has released the backspace button, - // and the input is empty (see behaviour in _keyup) + // And the input is empty (see behaviour in _keyup) if (event.keyCode === BACKSPACE && this._focusLastChipOnBackspace) { - this._chipList._keyManager.setLastItemActive(); + this._chipGrid._focusLastChip(); event.preventDefault(); return; } else { @@ -189,24 +199,20 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, A } this.focused = false; // Blur the chip list if it is not focused - if (!this._chipList.focused) { - this._chipList._blur(); + if (!this._chipGrid.focused) { + this._chipGrid._blur(); } - this._chipList.stateChanges.next(); + this._chipGrid.stateChanges.next(); } _focus() { this.focused = true; this._focusLastChipOnBackspace = this.empty; - this._chipList.stateChanges.next(); + this._chipGrid.stateChanges.next(); } /** Checks to see if the (chipEnd) event needs to be emitted. */ _emitChipEnd(event?: KeyboardEvent) { - if (!this.inputElement.value && !!event) { - this._chipList._keydown(event); - } - if (!event || this._isSeparatorKey(event)) { this.chipEnd.emit({ input: this.inputElement, @@ -220,12 +226,12 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, A _onInput() { // Let chip list know whenever the value changes. - this._chipList.stateChanges.next(); + this._chipGrid.stateChanges.next(); } /** Focuses the input. */ - focus(options?: FocusOptions): void { - this.inputElement.focus(options); + focus(): void { + this.inputElement.focus(); } /** Clears the input */ @@ -234,6 +240,10 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, A this._focusLastChipOnBackspace = true; } + setDescribedByIds(ids: string[]): void { + this._ariaDescribedby = ids.join(' '); + } + /** Checks whether a keycode is one of the configured separators. */ private _isSeparatorKey(event: KeyboardEvent) { return !hasModifierKey(event) && new Set(this.separatorKeyCodes).has(event.keyCode); diff --git a/src/material-experimental/mdc-chips/chip-listbox.spec.ts b/src/material/chips/chip-listbox.spec.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-listbox.spec.ts rename to src/material/chips/chip-listbox.spec.ts diff --git a/src/material-experimental/mdc-chips/chip-listbox.ts b/src/material/chips/chip-listbox.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-listbox.ts rename to src/material/chips/chip-listbox.ts diff --git a/src/material-experimental/mdc-chips/chip-option.html b/src/material/chips/chip-option.html similarity index 100% rename from src/material-experimental/mdc-chips/chip-option.html rename to src/material/chips/chip-option.html diff --git a/src/material-experimental/mdc-chips/chip-option.spec.ts b/src/material/chips/chip-option.spec.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-option.spec.ts rename to src/material/chips/chip-option.spec.ts diff --git a/src/material-experimental/mdc-chips/chip-option.ts b/src/material/chips/chip-option.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-option.ts rename to src/material/chips/chip-option.ts diff --git a/src/material/chips/chip-remove.spec.ts b/src/material/chips/chip-remove.spec.ts index 4632934cb376..b747e7d0c879 100644 --- a/src/material/chips/chip-remove.spec.ts +++ b/src/material/chips/chip-remove.spec.ts @@ -1,13 +1,13 @@ -import {Component, DebugElement} from '@angular/core'; +import {Component} from '@angular/core'; +import {waitForAsync, ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; +import {dispatchKeyboardEvent, dispatchMouseEvent} from '@angular/cdk/testing/private'; import {By} from '@angular/platform-browser'; -import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; +import {SPACE, ENTER} from '@angular/cdk/keycodes'; import {MatChip, MatChipsModule} from './index'; -import {dispatchMouseEvent} from '@angular/cdk/testing/private'; -describe('Chip Remove', () => { +describe('MDC-based Chip Remove', () => { let fixture: ComponentFixture; let testChip: TestChip; - let chipDebugElement: DebugElement; let chipNativeElement: HTMLElement; beforeEach(waitForAsync(() => { @@ -24,82 +24,101 @@ describe('Chip Remove', () => { testChip = fixture.debugElement.componentInstance; fixture.detectChanges(); - chipDebugElement = fixture.debugElement.query(By.directive(MatChip))!; + const chipDebugElement = fixture.debugElement.query(By.directive(MatChip))!; chipNativeElement = chipDebugElement.nativeElement; })); describe('basic behavior', () => { - it('should apply a CSS class to the remove icon', () => { + it('should apply a CSS class to the remove icon', fakeAsync(() => { + const buttonElement = chipNativeElement.querySelector('.mdc-evolution-chip__icon--trailing')!; + expect(buttonElement.classList).toContain('mat-mdc-chip-remove'); + })); + + it('should ensure that the button cannot submit its parent form', fakeAsync(() => { const buttonElement = chipNativeElement.querySelector('button')!; + expect(buttonElement.getAttribute('type')).toBe('button'); + })); - expect(buttonElement.classList).toContain('mat-chip-remove'); - }); + it('should not set the `type` attribute on non-button elements', fakeAsync(() => { + const buttonElement = chipNativeElement.querySelector('span.mat-mdc-chip-remove')!; + expect(buttonElement.hasAttribute('type')).toBe(false); + })); - it('should ensure that the button cannot submit its parent form', () => { - const buttonElement = chipNativeElement.querySelector('button')!; + it('should emit (removed) event when exit animation is complete', fakeAsync(() => { + testChip.removable = true; + fixture.detectChanges(); - expect(buttonElement.getAttribute('type')).toBe('button'); - }); + chipNativeElement.querySelector('button')!.click(); + fixture.detectChanges(); + flush(); - it('should not set the `type` attribute on non-button elements', () => { - const buttonElement = chipNativeElement.querySelector('span.mat-chip-remove')!; + expect(testChip.didRemove).toHaveBeenCalled(); + })); - expect(buttonElement.hasAttribute('type')).toBe(false); - }); + it('should not make the element aria-hidden when it is focusable', fakeAsync(() => { + const buttonElement = chipNativeElement.querySelector('button')!; + + expect(buttonElement.getAttribute('tabindex')).toBe('-1'); + expect(buttonElement.hasAttribute('aria-hidden')).toBe(false); + })); - it('should emit (removed) on click', () => { + it('should prevent the default SPACE action', fakeAsync(() => { const buttonElement = chipNativeElement.querySelector('button')!; testChip.removable = true; fixture.detectChanges(); - spyOn(testChip, 'didRemove'); - - buttonElement.click(); + const event = dispatchKeyboardEvent(buttonElement, 'keydown', SPACE); fixture.detectChanges(); + flush(); - expect(testChip.didRemove).toHaveBeenCalled(); - }); + expect(event.defaultPrevented).toBe(true); + })); - it('should not remove if parent chip is disabled', () => { + it('should prevent the default ENTER action', fakeAsync(() => { const buttonElement = chipNativeElement.querySelector('button')!; - testChip.disabled = true; testChip.removable = true; fixture.detectChanges(); - spyOn(testChip, 'didRemove'); - - buttonElement.click(); + const event = dispatchKeyboardEvent(buttonElement, 'keydown', ENTER); fixture.detectChanges(); + flush(); - expect(testChip.didRemove).not.toHaveBeenCalled(); - }); + expect(event.defaultPrevented).toBe(true); + })); - it('should prevent the default click action', () => { + it('should have a focus indicator', fakeAsync(() => { + const buttonElement = chipNativeElement.querySelector('.mdc-evolution-chip__icon--trailing')!; + expect(buttonElement.classList.contains('mat-mdc-focus-indicator')).toBe(true); + })); + + it('should prevent the default click action', fakeAsync(() => { const buttonElement = chipNativeElement.querySelector('button')!; const event = dispatchMouseEvent(buttonElement, 'click'); fixture.detectChanges(); + flush(); expect(event.defaultPrevented).toBe(true); - }); + })); }); }); @Component({ template: ` - - - - + + + + + + `, }) class TestChip { removable: boolean; disabled = false; - - didRemove() {} + didRemove = jasmine.createSpy('didRemove spy'); } diff --git a/src/material-experimental/mdc-chips/chip-row.html b/src/material/chips/chip-row.html similarity index 100% rename from src/material-experimental/mdc-chips/chip-row.html rename to src/material/chips/chip-row.html diff --git a/src/material-experimental/mdc-chips/chip-row.spec.ts b/src/material/chips/chip-row.spec.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-row.spec.ts rename to src/material/chips/chip-row.spec.ts diff --git a/src/material-experimental/mdc-chips/chip-row.ts b/src/material/chips/chip-row.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-row.ts rename to src/material/chips/chip-row.ts diff --git a/src/material-experimental/mdc-chips/chip-set.scss b/src/material/chips/chip-set.scss similarity index 86% rename from src/material-experimental/mdc-chips/chip-set.scss rename to src/material/chips/chip-set.scss index f3785b30676e..b2f9b3ff9a93 100644 --- a/src/material-experimental/mdc-chips/chip-set.scss +++ b/src/material/chips/chip-set.scss @@ -1,7 +1,7 @@ -@use '@angular/material' as mat; @use '@material/chips/chip-set' as mdc-chip-set; +@use '../core/mdc-helpers/mdc-helpers'; -@include mdc-chip-set.core-styles($query: mat.$private-mdc-base-styles-query); +@include mdc-chip-set.core-styles($query: mdc-helpers.$mdc-base-styles-query); // Ensures that the internal chip container spans the entire outer container width, if the // outer container width is customized. This is used by some wrapper components in g3. diff --git a/src/material-experimental/mdc-chips/chip-set.spec.ts b/src/material/chips/chip-set.spec.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-set.spec.ts rename to src/material/chips/chip-set.spec.ts diff --git a/src/material-experimental/mdc-chips/chip-set.ts b/src/material/chips/chip-set.ts similarity index 100% rename from src/material-experimental/mdc-chips/chip-set.ts rename to src/material/chips/chip-set.ts diff --git a/src/material/chips/chip-text-control.ts b/src/material/chips/chip-text-control.ts index 2ef6654336a1..422328517e23 100644 --- a/src/material/chips/chip-text-control.ts +++ b/src/material/chips/chip-text-control.ts @@ -21,5 +21,8 @@ export interface MatChipTextControl { empty: boolean; /** Focuses the text control. */ - focus(options?: FocusOptions): void; + focus(): void; + + /** Sets the list of ids the input is described by. */ + setDescribedByIds(ids: string[]): void; } diff --git a/src/material-experimental/mdc-chips/chip.html b/src/material/chips/chip.html similarity index 100% rename from src/material-experimental/mdc-chips/chip.html rename to src/material/chips/chip.html diff --git a/src/material-experimental/mdc-chips/chip.scss b/src/material/chips/chip.scss similarity index 94% rename from src/material-experimental/mdc-chips/chip.scss rename to src/material/chips/chip.scss index 13e0c6a73b47..758e47ecfa2f 100644 --- a/src/material-experimental/mdc-chips/chip.scss +++ b/src/material/chips/chip.scss @@ -1,9 +1,11 @@ @use '@angular/cdk'; -@use '@angular/material' as mat; @use '@material/chips/chip' as mdc-chip; @use '@material/chips/chip-theme' as mdc-chip-theme; +@use '../core/mdc-helpers/mdc-helpers'; +@use '../core/style/layout-common'; +@use '../core/focus-indicators/private' as focus-indicators-private; -@include mdc-chip.without-ripple-styles($query: mat.$private-mdc-base-styles-query); +@include mdc-chip.without-ripple-styles($query: mdc-helpers.$mdc-base-styles-query); .mat-mdc-standard-chip { @include mdc-chip-theme.theme-styles(( @@ -101,7 +103,7 @@ // MDC's focus and hover indication is handled through their ripple which we currently // don't use due to size concerns so we have to re-implement it ourselves. .mat-mdc-chip-focus-overlay { - @include mat.private-fill; + @include layout-common.fill; pointer-events: none; opacity: 0; border-radius: inherit; @@ -126,7 +128,7 @@ // The ripple container should match the bounds of the entire chip. .mat-mdc-chip-ripple { - @include mat.private-fill; + @include layout-common.fill; // Disable pointer events for the ripple container and state overlay because the container // will overlay the user content and we don't want to disable mouse events on the user content. @@ -177,7 +179,7 @@ // For the chip element, default inset/offset values are necessary to ensure that // the focus indicator is sufficiently contrastive and renders appropriately. .mat-mdc-focus-indicator::before { - $default-border-width: mat.$private-strong-focus-indicators-default-border-width; + $default-border-width: focus-indicators-private.$default-border-width; $border-width: var(--mat-mdc-focus-indicator-border-width, #{$default-border-width}); $offset: calc(#{$border-width} + 2px); margin: calc(#{$offset} * -1); @@ -194,7 +196,7 @@ } &::before { - $default-border-width: mat.$private-strong-focus-indicators-default-border-width; + $default-border-width: focus-indicators-private.$default-border-width; $offset: var(--mat-mdc-focus-indicator-border-width, #{$default-border-width}); margin: calc(#{$offset} * -1); diff --git a/src/material/chips/chip.spec.ts b/src/material/chips/chip.spec.ts index 800160fb21b9..37b2127708f1 100644 --- a/src/material/chips/chip.spec.ts +++ b/src/material/chips/chip.spec.ts @@ -1,23 +1,22 @@ import {Directionality} from '@angular/cdk/bidi'; -import {BACKSPACE, DELETE, SPACE} from '@angular/cdk/keycodes'; -import {createKeyboardEvent, dispatchFakeEvent} from '../../cdk/testing/private'; import {Component, DebugElement, ViewChild} from '@angular/core'; import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; -import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core'; +import {MatRipple} from '@angular/material/core'; import {By} from '@angular/platform-browser'; import {Subject} from 'rxjs'; -import {MatChip, MatChipEvent, MatChipSelectionChange, MatChipsModule, MatChipList} from './index'; +import {MatChip, MatChipEvent, MatChipSet, MatChipsModule} from './index'; -describe('MatChip', () => { +describe('MDC-based MatChip', () => { let fixture: ComponentFixture; let chipDebugElement: DebugElement; let chipNativeElement: HTMLElement; let chipInstance: MatChip; - let globalRippleOptions: RippleGlobalOptions; + let chipRippleDebugElement: DebugElement; + let chipRippleInstance: MatRipple; + let dir = 'ltr'; beforeEach(waitForAsync(() => { - globalRippleOptions = {}; TestBed.configureTestingModule({ imports: [MatChipsModule], declarations: [ @@ -27,7 +26,6 @@ describe('MatChip', () => { BasicChipWithBoundTabindex, ], providers: [ - {provide: MAT_RIPPLE_GLOBAL_OPTIONS, useFactory: () => globalRippleOptions}, { provide: Directionality, useFactory: () => ({ @@ -47,8 +45,7 @@ describe('MatChip', () => { fixture.detectChanges(); const chip = fixture.nativeElement.querySelector('mat-basic-chip'); - expect(chip.classList).toContain('mat-chip'); - expect(chip.classList).toContain('mat-basic-chip'); + expect(chip.classList).toContain('mat-mdc-basic-chip'); }); it('should be able to set a static tabindex', () => { @@ -64,6 +61,7 @@ describe('MatChip', () => { fixture.detectChanges(); const chip = fixture.nativeElement.querySelector('mat-basic-chip'); + expect(chip.getAttribute('tabindex')).toBe('12'); fixture.componentInstance.tabindex = 15; @@ -72,31 +70,21 @@ describe('MatChip', () => { expect(chip.getAttribute('tabindex')).toBe('15'); }); - it('should have the correct role', () => { + it('should have its ripple disabled', () => { fixture = TestBed.createComponent(BasicChip); fixture.detectChanges(); chipDebugElement = fixture.debugElement.query(By.directive(MatChip))!; - chipNativeElement = chipDebugElement.nativeElement; - - expect(chipNativeElement.getAttribute('role')).toBe('option'); - }); - - it('should be able to set a custom role', () => { - fixture = TestBed.createComponent(BasicChip); - fixture.detectChanges(); - chipDebugElement = fixture.debugElement.query(By.directive(MatChip))!; - chipInstance = chipDebugElement.injector.get(MatChip); - chipNativeElement = chipDebugElement.nativeElement; - - chipInstance.role = 'gridcell'; - fixture.detectChanges(); - - expect(chipNativeElement.getAttribute('role')).toBe('gridcell'); + chipRippleDebugElement = chipDebugElement.query(By.directive(MatRipple))!; + chipRippleInstance = chipRippleDebugElement.injector.get(MatRipple); + expect(chipRippleInstance.disabled) + .withContext('Expected basic chip ripples to be disabled.') + .toBe(true); }); }); describe('MatChip', () => { let testComponent: SingleChip; + let primaryAction: HTMLElement; beforeEach(() => { fixture = TestBed.createComponent(SingleChip); @@ -105,376 +93,125 @@ describe('MatChip', () => { chipDebugElement = fixture.debugElement.query(By.directive(MatChip))!; chipNativeElement = chipDebugElement.nativeElement; chipInstance = chipDebugElement.injector.get(MatChip); + chipRippleDebugElement = chipDebugElement.query(By.directive(MatRipple))!; + chipRippleInstance = chipRippleDebugElement.injector.get(MatRipple); testComponent = fixture.debugElement.componentInstance; + primaryAction = chipNativeElement.querySelector('.mdc-evolution-chip__action--primary')!; }); - describe('basic behaviors', () => { - it('adds the `mat-chip` class', () => { - expect(chipNativeElement.classList).toContain('mat-chip'); - }); - - it('does not add the `mat-basic-chip` class', () => { - expect(chipNativeElement.classList).not.toContain('mat-basic-chip'); - }); - - it('emits focus only once for multiple clicks', () => { - let counter = 0; - chipInstance._onFocus.subscribe(() => { - counter++; - }); - - chipNativeElement.focus(); - chipNativeElement.focus(); - fixture.detectChanges(); - - expect(counter).toBe(1); - }); - - it('emits destroy on destruction', () => { - spyOn(testComponent, 'chipDestroy').and.callThrough(); - - // Force a destroy callback - testComponent.shouldShow = false; - fixture.detectChanges(); - - expect(testComponent.chipDestroy).toHaveBeenCalledTimes(1); - }); - - it('allows color customization', () => { - expect(chipNativeElement.classList).toContain('mat-primary'); - - testComponent.color = 'warn'; - fixture.detectChanges(); - - expect(chipNativeElement.classList).not.toContain('mat-primary'); - expect(chipNativeElement.classList).toContain('mat-warn'); - }); - - it('allows selection', () => { - spyOn(testComponent, 'chipSelectionChange'); - expect(chipNativeElement.classList).not.toContain('mat-chip-selected'); - - testComponent.selected = true; - fixture.detectChanges(); - - expect(chipNativeElement.classList).toContain('mat-chip-selected'); - expect(testComponent.chipSelectionChange).toHaveBeenCalledWith({ - source: chipInstance, - isUserInput: false, - selected: true, - }); - }); - - it('allows removal', () => { - spyOn(testComponent, 'chipRemove'); - - chipInstance.remove(); - fixture.detectChanges(); - - expect(testComponent.chipRemove).toHaveBeenCalledWith({chip: chipInstance}); - }); - - it('should not prevent the default click action', () => { - const event = dispatchFakeEvent(chipNativeElement, 'click'); - fixture.detectChanges(); - - expect(event.defaultPrevented).toBe(false); - }); - - it('should prevent the default click action when the chip is disabled', () => { - chipInstance.disabled = true; - fixture.detectChanges(); - - const event = dispatchFakeEvent(chipNativeElement, 'click'); - fixture.detectChanges(); - - expect(event.defaultPrevented).toBe(true); - }); - - it('should not dispatch `selectionChange` event when deselecting a non-selected chip', () => { - chipInstance.deselect(); - - const spy = jasmine.createSpy('selectionChange spy'); - const subscription = chipInstance.selectionChange.subscribe(spy); - - chipInstance.deselect(); - - expect(spy).not.toHaveBeenCalled(); - subscription.unsubscribe(); - }); - - it('should not dispatch `selectionChange` event when selecting a selected chip', () => { - chipInstance.select(); - - const spy = jasmine.createSpy('selectionChange spy'); - const subscription = chipInstance.selectionChange.subscribe(spy); - - chipInstance.select(); - - expect(spy).not.toHaveBeenCalled(); - subscription.unsubscribe(); - }); - - it( - 'should not dispatch `selectionChange` event when selecting a selected chip via ' + - 'user interaction', - () => { - chipInstance.select(); - - const spy = jasmine.createSpy('selectionChange spy'); - const subscription = chipInstance.selectionChange.subscribe(spy); - - chipInstance.selectViaInteraction(); - - expect(spy).not.toHaveBeenCalled(); - subscription.unsubscribe(); - }, - ); - - it('should not dispatch `selectionChange` through setter if the value did not change', () => { - chipInstance.selected = false; - - const spy = jasmine.createSpy('selectionChange spy'); - const subscription = chipInstance.selectionChange.subscribe(spy); - - chipInstance.selected = false; - - expect(spy).not.toHaveBeenCalled(); - subscription.unsubscribe(); - }); - - it('should be able to disable ripples through ripple global options at runtime', () => { - expect(chipInstance.rippleDisabled) - .withContext('Expected chip ripples to be enabled.') - .toBe(false); - - globalRippleOptions.disabled = true; - - expect(chipInstance.rippleDisabled) - .withContext('Expected chip ripples to be disabled.') - .toBe(true); - }); - - it('should return the chip text if value is undefined', () => { - expect(chipInstance.value.trim()).toBe(fixture.componentInstance.name); - }); - - it('should return the chip value if defined', () => { - fixture.componentInstance.value = 123; - fixture.detectChanges(); - - expect(chipInstance.value).toBe(123); - }); - - it('should return the chip value if set to null', () => { - fixture.componentInstance.value = null; - fixture.detectChanges(); - - expect(chipInstance.value).toBeNull(); - }); + it('adds the `mat-chip` class', () => { + expect(chipNativeElement.classList).toContain('mat-mdc-chip'); }); - describe('keyboard behavior', () => { - describe('when selectable is true', () => { - beforeEach(() => { - testComponent.selectable = true; - fixture.detectChanges(); - }); - - it('should selects/deselects the currently focused chip on SPACE', () => { - const SPACE_EVENT = createKeyboardEvent('keydown', SPACE); - const CHIP_SELECTED_EVENT: MatChipSelectionChange = { - source: chipInstance, - isUserInput: true, - selected: true, - }; - - const CHIP_DESELECTED_EVENT: MatChipSelectionChange = { - source: chipInstance, - isUserInput: true, - selected: false, - }; - - spyOn(testComponent, 'chipSelectionChange'); - - // Use the spacebar to select the chip - chipInstance._handleKeydown(SPACE_EVENT); - fixture.detectChanges(); - - expect(chipInstance.selected).toBeTruthy(); - expect(testComponent.chipSelectionChange).toHaveBeenCalledTimes(1); - expect(testComponent.chipSelectionChange).toHaveBeenCalledWith(CHIP_SELECTED_EVENT); - - // Use the spacebar to deselect the chip - chipInstance._handleKeydown(SPACE_EVENT); - fixture.detectChanges(); - - expect(chipInstance.selected).toBeFalsy(); - expect(testComponent.chipSelectionChange).toHaveBeenCalledTimes(2); - expect(testComponent.chipSelectionChange).toHaveBeenCalledWith(CHIP_DESELECTED_EVENT); - }); - - it('should have correct aria-selected in single selection mode', () => { - expect(chipNativeElement.hasAttribute('aria-selected')).toBe(false); - - testComponent.selected = true; - fixture.detectChanges(); - - expect(chipNativeElement.getAttribute('aria-selected')).toBe('true'); - }); - - it('should have the correct aria-selected in multi-selection mode', () => { - testComponent.chipList.multiple = true; - fixture.detectChanges(); - - expect(chipNativeElement.getAttribute('aria-selected')).toBe('false'); - - testComponent.selected = true; - fixture.detectChanges(); - - expect(chipNativeElement.getAttribute('aria-selected')).toBe('true'); - }); - }); - - describe('when selectable is false', () => { - beforeEach(() => { - testComponent.selectable = false; - fixture.detectChanges(); - }); - - it('SPACE ignores selection', () => { - const SPACE_EVENT = createKeyboardEvent('keydown', SPACE); - - spyOn(testComponent, 'chipSelectionChange'); - - // Use the spacebar to attempt to select the chip - chipInstance._handleKeydown(SPACE_EVENT); - fixture.detectChanges(); - - expect(chipInstance.selected).toBeFalsy(); - expect(testComponent.chipSelectionChange).not.toHaveBeenCalled(); - }); - - it('should not have the aria-selected attribute', () => { - expect(chipNativeElement.hasAttribute('aria-selected')).toBe(false); - }); - }); - - describe('when removable is true', () => { - beforeEach(() => { - testComponent.removable = true; - fixture.detectChanges(); - }); - - it('DELETE emits the (removed) event', () => { - const DELETE_EVENT = createKeyboardEvent('keydown', DELETE); - - spyOn(testComponent, 'chipRemove'); - - // Use the delete to remove the chip - chipInstance._handleKeydown(DELETE_EVENT); - fixture.detectChanges(); - - expect(testComponent.chipRemove).toHaveBeenCalled(); - }); + it('does not add the `mat-basic-chip` class', () => { + expect(chipNativeElement.classList).not.toContain('mat-mdc-basic-chip'); + }); - it('BACKSPACE emits the (removed) event', () => { - const BACKSPACE_EVENT = createKeyboardEvent('keydown', BACKSPACE); + it('emits destroy on destruction', () => { + spyOn(testComponent, 'chipDestroy').and.callThrough(); - spyOn(testComponent, 'chipRemove'); + // Force a destroy callback + testComponent.shouldShow = false; + fixture.detectChanges(); - // Use the delete to remove the chip - chipInstance._handleKeydown(BACKSPACE_EVENT); - fixture.detectChanges(); + expect(testComponent.chipDestroy).toHaveBeenCalledTimes(1); + }); - expect(testComponent.chipRemove).toHaveBeenCalled(); - }); - }); + it('allows color customization', () => { + expect(chipNativeElement.classList).toContain('mat-primary'); - describe('when removable is false', () => { - beforeEach(() => { - testComponent.removable = false; - fixture.detectChanges(); - }); + testComponent.color = 'warn'; + fixture.detectChanges(); - it('DELETE does not emit the (removed) event', () => { - const DELETE_EVENT = createKeyboardEvent('keydown', DELETE); + expect(chipNativeElement.classList).not.toContain('mat-primary'); + expect(chipNativeElement.classList).toContain('mat-warn'); + }); - spyOn(testComponent, 'chipRemove'); + it('allows removal', () => { + spyOn(testComponent, 'chipRemove'); - // Use the delete to remove the chip - chipInstance._handleKeydown(DELETE_EVENT); - fixture.detectChanges(); + chipInstance.remove(); + fixture.detectChanges(); - expect(testComponent.chipRemove).not.toHaveBeenCalled(); - }); + expect(testComponent.chipRemove).toHaveBeenCalledWith({chip: chipInstance}); + }); - it('BACKSPACE does not emit the (removed) event', () => { - const BACKSPACE_EVENT = createKeyboardEvent('keydown', BACKSPACE); + it('should be able to disable ripples with the `[rippleDisabled]` input', () => { + expect(chipRippleInstance.disabled) + .withContext('Expected chip ripples to be enabled.') + .toBe(false); - spyOn(testComponent, 'chipRemove'); + testComponent.rippleDisabled = true; + fixture.detectChanges(); - // Use the delete to remove the chip - chipInstance._handleKeydown(BACKSPACE_EVENT); - fixture.detectChanges(); + expect(chipRippleInstance.disabled) + .withContext('Expected chip ripples to be disabled.') + .toBe(true); + }); - expect(testComponent.chipRemove).not.toHaveBeenCalled(); - }); - }); + it('should disable ripples when the chip is disabled', () => { + expect(chipRippleInstance.disabled) + .withContext('Expected chip ripples to be enabled.') + .toBe(false); - it('should update the aria-label for disabled chips', () => { - expect(chipNativeElement.getAttribute('aria-disabled')).toBe('false'); + testComponent.disabled = true; + fixture.detectChanges(); - testComponent.disabled = true; - fixture.detectChanges(); + expect(chipRippleInstance.disabled) + .withContext('Expected chip ripples to be disabled.') + .toBe(true); + }); - expect(chipNativeElement.getAttribute('aria-disabled')).toBe('true'); - }); + it('should make disabled chips non-focusable', () => { + testComponent.disabled = true; + fixture.detectChanges(); + expect(primaryAction.hasAttribute('tabindex')).toBe(false); + }); - it('should make disabled chips non-focusable', () => { - expect(chipNativeElement.getAttribute('tabindex')).toBe('-1'); + it('should return the chip text if value is undefined', () => { + expect(chipInstance.value.trim()).toBe(fixture.componentInstance.name); + }); - testComponent.disabled = true; - fixture.detectChanges(); + it('should return the chip value if defined', () => { + fixture.componentInstance.value = 123; + fixture.detectChanges(); - expect(chipNativeElement.getAttribute('tabindex')).toBeFalsy(); - }); + expect(chipInstance.value).toBe(123); }); - it('should have a focus indicator', () => { - expect(chipNativeElement.classList.contains('mat-focus-indicator')).toBe(true); + it('should return the chip value if set to null', () => { + fixture.componentInstance.value = null; + fixture.detectChanges(); + + expect(chipInstance.value).toBeNull(); }); }); }); @Component({ template: ` - +
- + {{name}}
-
`, +
`, }) class SingleChip { - @ViewChild(MatChipList) chipList: MatChipList; + @ViewChild(MatChipSet) chipList: MatChipSet; disabled: boolean = false; name: string = 'Test'; color: string = 'primary'; - selected: boolean = false; - selectable: boolean = true; removable: boolean = true; shouldShow: boolean = true; value: any; + rippleDisabled: boolean = false; - chipFocus: (event?: MatChipEvent) => void = () => {}; chipDestroy: (event?: MatChipEvent) => void = () => {}; - chipSelectionChange: (event?: MatChipSelectionChange) => void = () => {}; chipRemove: (event?: MatChipEvent) => void = () => {}; } @@ -484,12 +221,12 @@ class SingleChip { class BasicChip {} @Component({ - template: `Hello`, + template: `Hello`, }) class BasicChipWithStaticTabindex {} @Component({ - template: `Hello`, + template: `Hello`, }) class BasicChipWithBoundTabindex { tabindex = 12; diff --git a/src/material/chips/chip.ts b/src/material/chips/chip.ts index 449d170f38c2..e8b6f8c4233c 100644 --- a/src/material/chips/chip.ts +++ b/src/material/chips/chip.ts @@ -6,43 +6,49 @@ * found in the LICENSE file at https://angular.io/license */ -import {FocusableOption} from '@angular/cdk/a11y'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {BACKSPACE, DELETE, SPACE} from '@angular/cdk/keycodes'; -import {Platform} from '@angular/cdk/platform'; -import {DOCUMENT} from '@angular/common'; +import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; import { - Attribute, + AfterViewInit, + Component, + ChangeDetectionStrategy, ChangeDetectorRef, ContentChild, - Directive, ElementRef, EventEmitter, Inject, - InjectionToken, Input, NgZone, OnDestroy, Optional, Output, + ViewEncapsulation, + ViewChild, + Attribute, } from '@angular/core'; +import {DOCUMENT} from '@angular/common'; import { CanColor, CanDisable, CanDisableRipple, HasTabIndex, + MatRipple, MAT_RIPPLE_GLOBAL_OPTIONS, mixinColor, mixinDisableRipple, mixinTabIndex, - RippleConfig, + mixinDisabled, RippleGlobalOptions, - RippleRenderer, - RippleTarget, } from '@angular/material/core'; -import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; +import {FocusMonitor} from '@angular/cdk/a11y'; import {Subject} from 'rxjs'; import {take} from 'rxjs/operators'; +import {MatChipAvatar, MatChipTrailingIcon, MatChipRemove} from './chip-icons'; +import {MatChipAction} from './chip-action'; +import {BACKSPACE, DELETE} from '@angular/cdk/keycodes'; +import {MAT_CHIP, MAT_CHIP_AVATAR, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens'; + +let uid = 0; /** Represents an event fired on an individual `mat-chip`. */ export interface MatChipEvent { @@ -50,214 +56,115 @@ export interface MatChipEvent { chip: MatChip; } -/** Event object emitted by MatChip when selected or deselected. */ -export class MatChipSelectionChange { - constructor( - /** Reference to the chip that emitted the event. */ - public source: MatChip, - /** 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, - ) {} -} - /** - * Injection token that can be used to reference instances of `MatChipRemove`. It serves as - * alternative token to the actual `MatChipRemove` class which could cause unnecessary - * retention of the class and its directive metadata. - */ -export const MAT_CHIP_REMOVE = new InjectionToken('MatChipRemove'); - -/** - * Injection token that can be used to reference instances of `MatChipAvatar`. It serves as - * alternative token to the actual `MatChipAvatar` class which could cause unnecessary - * retention of the class and its directive metadata. - */ -export const MAT_CHIP_AVATAR = new InjectionToken('MatChipAvatar'); - -/** - * Injection token that can be used to reference instances of `MatChipTrailingIcon`. It serves as - * alternative token to the actual `MatChipTrailingIcon` class which could cause unnecessary - * retention of the class and its directive metadata. - */ -export const MAT_CHIP_TRAILING_ICON = new InjectionToken( - 'MatChipTrailingIcon', -); - -// Boilerplate for applying mixins to MatChip. -/** @docs-private */ -abstract class MatChipBase { - abstract disabled: boolean; - constructor(public _elementRef: ElementRef) {} -} - -const _MatChipMixinBase = mixinTabIndex(mixinColor(mixinDisableRipple(MatChipBase), 'primary'), -1); - -/** - * Dummy directive to add CSS class to chip avatar. + * Boilerplate for applying mixins to MatChip. * @docs-private */ -@Directive({ - selector: 'mat-chip-avatar, [matChipAvatar]', - host: {'class': 'mat-chip-avatar'}, - providers: [{provide: MAT_CHIP_AVATAR, useExisting: MatChipAvatar}], -}) -export class MatChipAvatar {} +const _MatChipMixinBase = mixinTabIndex( + mixinColor( + mixinDisableRipple( + mixinDisabled( + class { + constructor(public _elementRef: ElementRef) {} + }, + ), + ), + 'primary', + ), + -1, +); /** - * Dummy directive to add CSS class to chip trailing icon. - * @docs-private + * Material design styled Chip base component. Used inside the MatChipSet component. + * + * Extended by MatChipOption and MatChipRow for different interaction patterns. */ -@Directive({ - selector: 'mat-chip-trailing-icon, [matChipTrailingIcon]', - host: {'class': 'mat-chip-trailing-icon'}, - providers: [{provide: MAT_CHIP_TRAILING_ICON, useExisting: MatChipTrailingIcon}], -}) -export class MatChipTrailingIcon {} - -/** Material Design styled chip directive. Used inside the MatChipList component. */ -@Directive({ - selector: `mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]`, - inputs: ['color', 'disableRipple', 'tabIndex'], +@Component({ + selector: 'mat-basic-chip, mat-chip', + inputs: ['color', 'disabled', 'disableRipple', 'tabIndex'], exportAs: 'matChip', + templateUrl: 'chip.html', + styleUrls: ['chip.css'], host: { - 'class': 'mat-chip mat-focus-indicator', - '[attr.tabindex]': 'disabled ? null : tabIndex', - '[attr.role]': 'role', - '[class.mat-chip-selected]': 'selected', - '[class.mat-chip-with-avatar]': 'avatar', - '[class.mat-chip-with-trailing-icon]': 'trailingIcon || removeIcon', - '[class.mat-chip-disabled]': 'disabled', + 'class': 'mat-mdc-chip', + '[class.mdc-evolution-chip]': '!_isBasicChip', + '[class.mdc-evolution-chip--disabled]': 'disabled', + '[class.mdc-evolution-chip--with-trailing-action]': '_hasTrailingIcon()', + '[class.mdc-evolution-chip--with-primary-graphic]': 'leadingIcon', + '[class.mdc-evolution-chip--with-primary-icon]': 'leadingIcon', + '[class.mdc-evolution-chip--with-avatar]': 'leadingIcon', + '[class.mat-mdc-chip-with-avatar]': 'leadingIcon', + '[class.mat-mdc-chip-highlighted]': 'highlighted', + '[class.mat-mdc-chip-disabled]': 'disabled', + '[class.mat-mdc-basic-chip]': '_isBasicChip', + '[class.mat-mdc-standard-chip]': '!_isBasicChip', + '[class.mat-mdc-chip-with-trailing-icon]': '_hasTrailingIcon()', '[class._mat-animation-noopable]': '_animationsDisabled', - '[attr.disabled]': 'disabled || null', - '[attr.aria-disabled]': 'disabled.toString()', - '[attr.aria-selected]': 'ariaSelected', - '(click)': '_handleClick($event)', + '[id]': 'id', + '[attr.role]': 'role', + '[attr.tabindex]': 'role ? tabIndex : null', + '[attr.aria-label]': 'ariaLabel', '(keydown)': '_handleKeydown($event)', - '(focus)': 'focus()', - '(blur)': '_blur()', }, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [{provide: MAT_CHIP, useExisting: MatChip}], }) export class MatChip extends _MatChipMixinBase - implements - FocusableOption, - OnDestroy, - CanColor, - CanDisableRipple, - RippleTarget, - HasTabIndex, - CanDisable + implements AfterViewInit, CanColor, CanDisableRipple, CanDisable, HasTabIndex, OnDestroy { - /** Reference to the RippleRenderer for the chip. */ - private _chipRipple: RippleRenderer; + protected _document: Document; - /** - * Reference to the element that acts as the chip's ripple target. This element is - * dynamically added as a child node of the chip. The chip itself cannot be used as the - * ripple target because it must be the host of the focus indicator. - */ - private _chipRippleTarget: HTMLElement; - - /** - * Ripple configuration for ripples that are launched on pointer down. The ripple config - * is set to the global ripple options since we don't have any configurable options for - * the chip ripples. - * @docs-private - */ - rippleConfig: RippleConfig & RippleGlobalOptions; - - /** - * Whether ripples are disabled on interaction - * @docs-private - */ - get rippleDisabled(): boolean { - return ( - this.disabled || - this.disableRipple || - this._animationsDisabled || - !!this.rippleConfig.disabled - ); - } + /** Whether the ripple is centered on the chip. */ + readonly _isRippleCentered = false; - /** Whether the chip has focus. */ - _hasFocus: boolean = false; + /** Emits when the chip is focused. */ + readonly _onFocus = new Subject(); - /** Whether animations for the chip are enabled. */ - _animationsDisabled: boolean; + /** Emits when the chip is blurred. */ + readonly _onBlur = new Subject(); - /** Whether the chip list is selectable */ - chipListSelectable: boolean = true; + /** Whether this chip is a basic (unstyled) chip. */ + readonly _isBasicChip: boolean; - /** Whether the chip list is in multi-selection mode. */ - _chipListMultiple: boolean = false; + /** Role for the root of the chip. */ + @Input() role: string | null = null; - /** Whether the chip list as a whole is disabled. */ - _chipListDisabled: boolean = false; + /** Whether the chip has focus. */ + private _hasFocusInternal = false; - /** The chip avatar */ - @ContentChild(MAT_CHIP_AVATAR) avatar: MatChipAvatar; + /** Whether moving focus into the chip is pending. */ + private _pendingFocus: boolean; - /** The chip's trailing icon. */ - @ContentChild(MAT_CHIP_TRAILING_ICON) trailingIcon: MatChipTrailingIcon; + /** Whether animations for the chip are enabled. */ + _animationsDisabled: boolean; - /** The chip's remove toggler. */ - @ContentChild(MAT_CHIP_REMOVE) removeIcon: MatChipRemove; + _hasFocus() { + return this._hasFocusInternal; + } - /** ARIA role that should be applied to the chip. */ - @Input() role: string = 'option'; + /** A unique id for the chip. If none is supplied, it will be auto-generated. */ + @Input() id: string = `mat-mdc-chip-${uid++}`; - /** Whether the chip is selected. */ - @Input() - get selected(): boolean { - return this._selected; - } - set selected(value: BooleanInput) { - const coercedValue = coerceBooleanProperty(value); + /** ARIA label for the content of the chip. */ + @Input('aria-label') ariaLabel: string | null = null; - if (coercedValue !== this._selected) { - this._selected = coercedValue; - this._dispatchSelectionChange(); - } - } - protected _selected: boolean = false; + private _textElement!: HTMLElement; - /** The value of the chip. Defaults to the content inside `` tags. */ + /** + * The value of the chip. Defaults to the content inside + * the `mat-mdc-chip-action-label` element. + */ @Input() get value(): any { - return this._value !== undefined ? this._value : this._elementRef.nativeElement.textContent; + return this._value !== undefined ? this._value : this._textElement.textContent!.trim(); } set value(value: any) { this._value = value; } protected _value: any; - /** - * Whether or not the chip is selectable. When a chip is not selectable, - * changes to its selected state are always ignored. By default a 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: BooleanInput) { - this._selectable = coerceBooleanProperty(value); - } - protected _selectable: boolean = true; - - /** Whether the chip is disabled. */ - @Input() - get disabled(): boolean { - return this._chipListDisabled || this._disabled; - } - set disabled(value: BooleanInput) { - this._disabled = coerceBooleanProperty(value); - } - protected _disabled: boolean = false; - /** * Determines whether or not the chip displays the remove styling and emits (removed) events. */ @@ -270,128 +177,84 @@ export class MatChip } protected _removable: boolean = true; - /** Emits when the chip is focused. */ - readonly _onFocus = new Subject(); - - /** Emits when the chip is blurred. */ - readonly _onBlur = new Subject(); + /** + * Colors the chip for emphasis as if it were selected. + */ + @Input() + get highlighted(): boolean { + return this._highlighted; + } + set highlighted(value: BooleanInput) { + this._highlighted = coerceBooleanProperty(value); + } + protected _highlighted: boolean = false; - /** Emitted when the chip is selected or deselected. */ - @Output() readonly selectionChange: EventEmitter = - new EventEmitter(); + /** Emitted when a chip is to be removed. */ + @Output() readonly removed: EventEmitter = 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 unstyled chip selector for this component. */ + protected basicChipAttrName = 'mat-basic-chip'; - /** 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 chip's leading icon. */ + @ContentChild(MAT_CHIP_AVATAR) leadingIcon: MatChipAvatar; + + /** The chip's trailing icon. */ + @ContentChild(MAT_CHIP_TRAILING_ICON) trailingIcon: MatChipTrailingIcon; + + /** The chip's trailing remove icon. */ + @ContentChild(MAT_CHIP_REMOVE) removeIcon: MatChipRemove; + + /** Reference to the MatRipple instance of the chip. */ + @ViewChild(MatRipple) ripple: MatRipple; + + /** Action receiving the primary set of user interactions. */ + @ViewChild(MatChipAction) primaryAction: MatChipAction; constructor( + public _changeDetectorRef: ChangeDetectorRef, elementRef: ElementRef, - private _ngZone: NgZone, - platform: Platform, - @Optional() - @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) - globalRippleOptions: RippleGlobalOptions | null, - private _changeDetectorRef: ChangeDetectorRef, + protected _ngZone: NgZone, + private _focusMonitor: FocusMonitor, @Inject(DOCUMENT) _document: any, @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string, + @Optional() + @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) + private _globalRippleOptions?: RippleGlobalOptions, @Attribute('tabindex') tabIndex?: string, ) { super(elementRef); - - this._addHostClassName(); - - // Dynamically create the ripple target, append it within the chip, and use it as the - // chip's ripple target. Adding the class '.mat-chip-ripple' ensures that it will have - // the proper styles. - this._chipRippleTarget = _document.createElement('div'); - this._chipRippleTarget.classList.add('mat-chip-ripple'); - this._elementRef.nativeElement.appendChild(this._chipRippleTarget); - this._chipRipple = new RippleRenderer(this, _ngZone, this._chipRippleTarget, platform); - this._chipRipple.setupTriggerEvents(elementRef); - - this.rippleConfig = globalRippleOptions || {}; + const element = elementRef.nativeElement; + this._document = _document; this._animationsDisabled = animationMode === 'NoopAnimations'; - this.tabIndex = tabIndex != null ? parseInt(tabIndex) || -1 : -1; - } - - _addHostClassName() { - const basicChipAttrName = 'mat-basic-chip'; - const element = this._elementRef.nativeElement as HTMLElement; - - if ( - element.hasAttribute(basicChipAttrName) || - element.tagName.toLowerCase() === basicChipAttrName - ) { - element.classList.add(basicChipAttrName); - return; - } else { - element.classList.add('mat-standard-chip'); + this._isBasicChip = + element.hasAttribute(this.basicChipAttrName) || + element.tagName.toLowerCase() === this.basicChipAttrName; + if (tabIndex != null) { + this.tabIndex = parseInt(tabIndex) ?? this.defaultTabIndex; } + this._monitorFocus(); } - ngOnDestroy() { - this.destroyed.emit({chip: this}); - this._chipRipple._removeTriggerEvents(); - } + ngAfterViewInit() { + this._textElement = this._elementRef.nativeElement.querySelector('.mat-mdc-chip-action-label')!; - /** Selects the chip. */ - select(): void { - if (!this._selected) { - this._selected = true; - this._dispatchSelectionChange(); - this._changeDetectorRef.markForCheck(); + if (this._pendingFocus) { + this._pendingFocus = false; + this.focus(); } } - /** Deselects the chip. */ - deselect(): void { - if (this._selected) { - this._selected = false; - this._dispatchSelectionChange(); - this._changeDetectorRef.markForCheck(); - } - } - - /** Select this chip and emit selected event */ - selectViaInteraction(): void { - if (!this._selected) { - this._selected = true; - this._dispatchSelectionChange(true); - this._changeDetectorRef.markForCheck(); - } - } - - /** Toggles the current selected state of this chip. */ - toggleSelected(isUserInput: boolean = false): boolean { - this._selected = !this.selected; - this._dispatchSelectionChange(isUserInput); - this._changeDetectorRef.markForCheck(); - return this.selected; - } - - /** Allows for programmatic focusing of the chip. */ - focus(): void { - if (!this._hasFocus) { - this._elementRef.nativeElement.focus(); - this._onFocus.next({chip: this}); - } - this._hasFocus = true; + ngOnDestroy() { + this._focusMonitor.stopMonitoring(this._elementRef); + this.destroyed.emit({chip: this}); + this.destroyed.complete(); } /** - * Allows for programmatic removal of the chip. Called by the MatChipList when the DELETE or - * BACKSPACE keys are pressed. + * Allows for programmatic removal of the chip. * * Informs any listeners of the removal request. Does not remove the chip from the DOM. */ @@ -401,103 +264,96 @@ export class MatChip } } - /** Handles click events on the chip. */ - _handleClick(event: Event) { - if (this.disabled) { - event.preventDefault(); - } + /** Whether or not the ripple should be disabled. */ + _isRippleDisabled(): boolean { + return ( + this.disabled || + this.disableRipple || + this._animationsDisabled || + this._isBasicChip || + !!this._globalRippleOptions?.disabled + ); } - /** Handle custom key presses. */ - _handleKeydown(event: KeyboardEvent): void { - if (this.disabled) { - return; - } - - switch (event.keyCode) { - case DELETE: - case BACKSPACE: - // If we are removable, remove the focused chip - this.remove(); - // Always prevent so page navigation does not occur - event.preventDefault(); - break; - case SPACE: - // If we are selectable, toggle the focused chip - if (this.selectable) { - this.toggleSelected(true); - } + /** Returns whether the chip has a trailing icon. */ + _hasTrailingIcon() { + return !!(this.trailingIcon || this.removeIcon); + } - // Always prevent space from scrolling the page since the list has focus - event.preventDefault(); - break; + /** Handles keyboard events on the chip. */ + _handleKeydown(event: KeyboardEvent) { + if (event.keyCode === BACKSPACE || event.keyCode === DELETE) { + event.preventDefault(); + this.remove(); } } - _blur(): void { - // When animations are enabled, Angular may end up removing the chip from the DOM a little - // earlier than usual, causing it to be blurred and throwing off the logic in the chip list - // that moves focus not the next item. To work around the issue, we defer marking the chip - // as not focused until the next time the zone stabilizes. - this._ngZone.onStable.pipe(take(1)).subscribe(() => { - this._ngZone.run(() => { - this._hasFocus = false; - this._onBlur.next({chip: this}); - }); - }); + /** Allows for programmatic focusing of the chip. */ + focus(): void { + if (!this.disabled) { + // If `focus` is called before `ngAfterViewInit`, we won't have access to the primary action. + // This can happen if the consumer tries to focus a chip immediately after it is added. + // Queue the method to be called again on init. + if (this.primaryAction) { + this.primaryAction.focus(); + } else { + this._pendingFocus = true; + } + } } - private _dispatchSelectionChange(isUserInput = false) { - this.selectionChange.emit({ - source: this, - isUserInput, - selected: this._selected, + /** Gets the action that contains a specific target node. */ + _getSourceAction(target: Node): MatChipAction | undefined { + return this._getActions().find(action => { + const element = action._elementRef.nativeElement; + return element === target || element.contains(target); }); } -} -/** - * Applies proper (click) support and adds styling for use with the Material Design "cancel" icon - * available at https://material.io/icons/#ic_cancel. - * - * Example: - * - * ` - * cancel - * ` - * - * You *may* use a custom icon, but you may need to override the `mat-chip-remove` positioning - * styles to properly center the icon within the chip. - */ -@Directive({ - selector: '[matChipRemove]', - host: { - 'class': 'mat-chip-remove mat-chip-trailing-icon', - '(click)': '_handleClick($event)', - }, - providers: [{provide: MAT_CHIP_REMOVE, useExisting: MatChipRemove}], -}) -export class MatChipRemove { - constructor(protected _parentChip: MatChip, elementRef: ElementRef) { - if (elementRef.nativeElement.nodeName === 'BUTTON') { - elementRef.nativeElement.setAttribute('type', 'button'); + /** Gets all of the actions within the chip. */ + _getActions(): MatChipAction[] { + const result: MatChipAction[] = []; + + if (this.primaryAction) { + result.push(this.primaryAction); } - } - /** Calls the parent chip's public `remove()` method if applicable. */ - _handleClick(event: Event): void { - const parentChip = this._parentChip; + if (this.removeIcon) { + result.push(this.removeIcon); + } - if (parentChip.removable && !parentChip.disabled) { - parentChip.remove(); + if (this.trailingIcon) { + result.push(this.trailingIcon); } - // We need to stop event propagation because otherwise the event will bubble up to the - // form field and cause the `onContainerClick` method to be invoked. This method would then - // reset the focused chip that has been focused after chip removal. Usually the parent - // the parent click listener of the `MatChip` would prevent propagation, but it can happen - // that the chip is being removed before the event bubbles up. - event.stopPropagation(); - event.preventDefault(); + return result; + } + + /** Handles interactions with the primary action of the chip. */ + _handlePrimaryActionInteraction() { + // Empty here, but is overwritten in child classes. + } + + /** Starts the focus monitoring process on the chip. */ + private _monitorFocus() { + this._focusMonitor.monitor(this._elementRef, true).subscribe(origin => { + const hasFocus = origin !== null; + + if (hasFocus !== this._hasFocusInternal) { + this._hasFocusInternal = hasFocus; + + if (hasFocus) { + this._onFocus.next({chip: this}); + } else { + // When animations are enabled, Angular may end up removing the chip from the DOM a little + // earlier than usual, causing it to be blurred and throwing off the logic in the chip list + // that moves focus not the next item. To work around the issue, we defer marking the chip + // as not focused until the next time the zone stabilizes. + this._ngZone.onStable + .pipe(take(1)) + .subscribe(() => this._ngZone.run(() => this._onBlur.next({chip: this}))); + } + } + }); } } diff --git a/src/material/chips/chips.md b/src/material/chips/chips.md index 78c176948300..0f8455854503 100644 --- a/src/material/chips/chips.md +++ b/src/material/chips/chips.md @@ -1,82 +1 @@ -`` displays a list of values as individual, keyboard accessible, chips. - - - -### Unstyled chips -By default, `` has Material Design styles applied. For a chip with no styles applied, -use ``. You can then customize the chip appearance by adding your own CSS. - -_Hint: `` receives the `mat-basic-chip` CSS class in addition to the `mat-chip` class._ - -### Selection -Chips can be selected via the `selected` property. Selection can be disabled by setting -`selectable` to `false` on the ``. - -Whenever the selection state changes, a `ChipSelectionChange` event will be emitted via -`(selectionChange)`. - -### Disabled chips -Individual chips may be disabled by applying the `disabled` attribute to the chip. When disabled, -chips are neither selectable nor focusable. - -### Chip input -The `MatChipInput` directive can be used together with a chip-list to streamline the interaction -between the two components. This directive adds chip-specific behaviors to the input element -within `` for adding and removing chips. The `` with `MatChipInput` can -be placed inside or outside the chip-list element. - -An example of chip input placed inside the chip-list element. - - -An example of chip input placed outside the chip-list element. - -```html - - - Chip 1 - Chip 2 - - - -``` - -An example of chip input with an autocomplete placed inside the chip-list element. - - -### Keyboard interaction -Users can move through the chips using the arrow keys and select/deselect them with the space. Chips -also gain focus when clicked, ensuring keyboard navigation starts at the appropriate chip. - -### Orientation -If you want the chips in the list to be stacked vertically, instead of horizontally, you can apply -the `mat-chip-list-stacked` class, as well as the `aria-orientation="vertical"` attribute: - - - -### Specifying global configuration defaults -Default options for the chips module can be specified using the `MAT_CHIPS_DEFAULT_OPTIONS` -injection token. - -```ts -@NgModule({ - providers: [ - { - provide: MAT_CHIPS_DEFAULT_OPTIONS, - useValue: { - separatorKeyCodes: [ENTER, COMMA] - } - } - ] -}) -``` - -### Theming -The selected color of an `` can be changed by using the `color` property. By default, chips -use a neutral background color based on the current theme (light or dark). This can be changed to -`'primary'`, `'accent'`, or `'warn'`. - -### Accessibility -A chip-list behaves as a `role="listbox"`, with each chip being a `role="option"`. The chip input -should have a placeholder or be given a meaningful label via `aria-label` or `aria-labelledby`. + diff --git a/src/material-experimental/mdc-chips/module.ts b/src/material/chips/module.ts similarity index 100% rename from src/material-experimental/mdc-chips/module.ts rename to src/material/chips/module.ts diff --git a/src/material/chips/public-api.ts b/src/material/chips/public-api.ts index fd202e54df03..4971595984dc 100644 --- a/src/material/chips/public-api.ts +++ b/src/material/chips/public-api.ts @@ -6,8 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './chips-module'; -export * from './chip-list'; 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'; +export * from './tokens'; +export * from './chip-icons'; +export * from './chip-text-control'; +export * from './chip-edit-input'; diff --git a/src/material/chips/testing/BUILD.bazel b/src/material/chips/testing/BUILD.bazel index f7a1e6908752..e5c99a2f0de7 100644 --- a/src/material/chips/testing/BUILD.bazel +++ b/src/material/chips/testing/BUILD.bazel @@ -9,47 +9,33 @@ ts_library( exclude = ["**/*.spec.ts"], ), deps = [ + "//src/cdk/coercion", "//src/cdk/testing", ], ) -filegroup( - name = "source-files", - srcs = glob(["**/*.ts"]), -) - ng_test_library( - name = "harness_tests_lib", - srcs = ["shared.spec.ts"], + name = "unit_tests_lib", + srcs = glob(["**/*.spec.ts"]), deps = [ ":testing", "//src/cdk/testing", - "//src/cdk/testing/private", "//src/cdk/testing/testbed", "//src/material/chips", "//src/material/icon", "//src/material/icon/testing", - "//src/material/legacy-form-field", - "@npm//@angular/platform-browser", + "@npm//@angular/forms", ], ) -ng_test_library( - name = "unit_tests_lib", - srcs = glob( - ["**/*.spec.ts"], - exclude = ["shared.spec.ts"], - ), +ng_web_test_suite( + name = "unit_tests", deps = [ - ":harness_tests_lib", - ":testing", - "//src/material/chips", - "//src/material/icon", - "//src/material/icon/testing", + ":unit_tests_lib", ], ) -ng_web_test_suite( - name = "unit_tests", - deps = [":unit_tests_lib"], +filegroup( + name = "source-files", + srcs = glob(["**/*.ts"]), ) diff --git a/src/material/chips/testing/chip-avatar-harness.ts b/src/material/chips/testing/chip-avatar-harness.ts index 2e825392b78a..469036f7b4d1 100644 --- a/src/material/chips/testing/chip-avatar-harness.ts +++ b/src/material/chips/testing/chip-avatar-harness.ts @@ -6,20 +6,27 @@ * found in the LICENSE file at https://angular.io/license */ -import {HarnessPredicate, ComponentHarness} from '@angular/cdk/testing'; +import { + ComponentHarness, + ComponentHarnessConstructor, + HarnessPredicate, +} from '@angular/cdk/testing'; import {ChipAvatarHarnessFilters} from './chip-harness-filters'; /** Harness for interacting with a standard Material chip avatar in tests. */ export class MatChipAvatarHarness extends ComponentHarness { - static hostSelector = '.mat-chip-avatar'; + static hostSelector = '.mat-mdc-chip-avatar'; /** - * Gets a `HarnessPredicate` that can be used to search for a `MatChipAvatarHarness` that meets - * certain criteria. + * Gets a `HarnessPredicate` that can be used to search for a chip avatar with specific + * attributes. * @param options Options for filtering which input instances are considered a match. * @return a `HarnessPredicate` configured with the given options. */ - static with(options: ChipAvatarHarnessFilters = {}): HarnessPredicate { - return new HarnessPredicate(MatChipAvatarHarness, options); + static with( + this: ComponentHarnessConstructor, + options: ChipAvatarHarnessFilters = {}, + ): HarnessPredicate { + return new HarnessPredicate(this, options); } } diff --git a/src/material-experimental/mdc-chips/testing/chip-grid-harness.spec.ts b/src/material/chips/testing/chip-grid-harness.spec.ts similarity index 100% rename from src/material-experimental/mdc-chips/testing/chip-grid-harness.spec.ts rename to src/material/chips/testing/chip-grid-harness.spec.ts diff --git a/src/material-experimental/mdc-chips/testing/chip-grid-harness.ts b/src/material/chips/testing/chip-grid-harness.ts similarity index 100% rename from src/material-experimental/mdc-chips/testing/chip-grid-harness.ts rename to src/material/chips/testing/chip-grid-harness.ts diff --git a/src/material/chips/testing/chip-harness-filters.ts b/src/material/chips/testing/chip-harness-filters.ts index c3926451b4ad..ec82bf202a0c 100644 --- a/src/material/chips/testing/chip-harness-filters.ts +++ b/src/material/chips/testing/chip-harness-filters.ts @@ -5,42 +5,34 @@ * 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 {BaseHarnessFilters} from '@angular/cdk/testing'; -/** A set of criteria that can be used to filter a list of chip instances. */ export interface ChipHarnessFilters extends BaseHarnessFilters { /** Only find instances whose text matches the given value. */ text?: string | RegExp; - /** - * Only find chip instances whose selected state matches the given value. - * @deprecated Use `MatChipOptionHarness` together with `ChipOptionHarnessFilters`. - * @breaking-change 12.0.0 - */ - selected?: boolean; } -/** A set of criteria that can be used to filter a list of selectable chip instances. */ +export interface ChipInputHarnessFilters extends BaseHarnessFilters { + /** Filters based on the value of the input. */ + value?: string | RegExp; + /** Filters based on the placeholder text of the input. */ + placeholder?: string | RegExp; +} + +export interface ChipListboxHarnessFilters extends BaseHarnessFilters {} + export interface ChipOptionHarnessFilters extends ChipHarnessFilters { /** Only find chip instances whose selected state matches the given value. */ selected?: boolean; } -/** A set of criteria that can be used to filter chip list instances. */ -export interface ChipListHarnessFilters extends BaseHarnessFilters {} +export interface ChipGridHarnessFilters extends BaseHarnessFilters {} -/** A set of criteria that can be used to filter selectable chip list instances. */ -export interface ChipListboxHarnessFilters extends BaseHarnessFilters {} +export interface ChipRowHarnessFilters extends ChipHarnessFilters {} -/** A set of criteria that can be used to filter a list of `MatChipListInputHarness` instances. */ -export interface ChipInputHarnessFilters extends BaseHarnessFilters { - /** Filters based on the value of the input. */ - value?: string | RegExp; - /** Filters based on the placeholder text of the input. */ - placeholder?: string | RegExp; -} +export interface ChipSetHarnessFilters extends BaseHarnessFilters {} -/** A set of criteria that can be used to filter a list of `MatChipRemoveHarness` instances. */ export interface ChipRemoveHarnessFilters extends BaseHarnessFilters {} -/** A set of criteria that can be used to filter a list of `MatChipAvatarHarness` instances. */ export interface ChipAvatarHarnessFilters extends BaseHarnessFilters {} diff --git a/src/material-experimental/mdc-chips/testing/chip-harness.spec.ts b/src/material/chips/testing/chip-harness.spec.ts similarity index 100% rename from src/material-experimental/mdc-chips/testing/chip-harness.spec.ts rename to src/material/chips/testing/chip-harness.spec.ts diff --git a/src/material/chips/testing/chip-harness.ts b/src/material/chips/testing/chip-harness.ts index d2a64d60e2bf..982ac0003909 100644 --- a/src/material/chips/testing/chip-harness.ts +++ b/src/material/chips/testing/chip-harness.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {ContentContainerComponentHarness, HarnessPredicate, TestKey} from '@angular/cdk/testing'; +import { + ComponentHarnessConstructor, + ContentContainerComponentHarness, + HarnessPredicate, + TestKey, +} from '@angular/cdk/testing'; import {MatChipAvatarHarness} from './chip-avatar-harness'; import { ChipAvatarHarnessFilters, @@ -15,89 +20,47 @@ import { } from './chip-harness-filters'; import {MatChipRemoveHarness} from './chip-remove-harness'; -/** Harness for interacting with a standard selectable Angular Material chip in tests. */ +/** Harness for interacting with a mat-chip in tests. */ export class MatChipHarness extends ContentContainerComponentHarness { - /** The selector for the host element of a `MatChip` instance. */ - static hostSelector = '.mat-chip'; + protected _primaryAction = this.locatorFor('.mdc-evolution-chip__action--primary'); + + static hostSelector = '.mat-mdc-basic-chip, .mat-mdc-chip'; /** - * Gets a `HarnessPredicate` that can be used to search for a `MatChipHarness` that meets - * certain criteria. - * @param options Options for filtering which chip instances are considered a match. + * Gets a `HarnessPredicate` that can be used to search for a chip with specific attributes. + * @param options Options for narrowing the search. * @return a `HarnessPredicate` configured with the given options. */ - static with(options: ChipHarnessFilters = {}): HarnessPredicate { - return new HarnessPredicate(MatChipHarness, options) - .addOption('text', options.text, (harness, label) => - HarnessPredicate.stringMatches(harness.getText(), label), - ) - .addOption( - 'selected', - options.selected, - async (harness, selected) => (await harness.isSelected()) === selected, - ); + static with( + this: ComponentHarnessConstructor, + options: ChipHarnessFilters = {}, + ): HarnessPredicate { + return new HarnessPredicate(this, options).addOption('text', options.text, (harness, label) => { + return HarnessPredicate.stringMatches(harness.getText(), label); + }); } - /** Gets the text of the chip. */ + /** Gets a promise for the text content the option. */ async getText(): Promise { return (await this.host()).text({ - exclude: '.mat-chip-avatar, .mat-chip-trailing-icon, .mat-icon', + exclude: '.mat-mdc-chip-avatar, .mat-mdc-chip-trailing-icon, .mat-icon', }); } - /** - * Whether the chip is selected. - * @deprecated Use `MatChipOptionHarness.isSelected` instead. - * @breaking-change 12.0.0 - */ - async isSelected(): Promise { - return (await this.host()).hasClass('mat-chip-selected'); - } - /** Whether the chip is disabled. */ async isDisabled(): Promise { - return (await this.host()).hasClass('mat-chip-disabled'); - } - - /** - * Selects the given chip. Only applies if it's selectable. - * @deprecated Use `MatChipOptionHarness.select` instead. - * @breaking-change 12.0.0 - */ - async select(): Promise { - if (!(await this.isSelected())) { - await this.toggle(); - } - } - - /** - * Deselects the given chip. Only applies if it's selectable. - * @deprecated Use `MatChipOptionHarness.deselect` instead. - * @breaking-change 12.0.0 - */ - async deselect(): Promise { - if (await this.isSelected()) { - await this.toggle(); - } - } - - /** - * Toggles the selected state of the given chip. Only applies if it's selectable. - * @deprecated Use `MatChipOptionHarness.toggle` instead. - * @breaking-change 12.0.0 - */ - async toggle(): Promise { - return (await this.host()).sendKeys(' '); + return (await this.host()).hasClass('mat-mdc-chip-disabled'); } - /** Removes the given chip. Only applies if it's removable. */ + /** Delete a chip from the set. */ async remove(): Promise { - await (await this.host()).sendKeys(TestKey.DELETE); + const hostEl = await this.host(); + await hostEl.sendKeys(TestKey.DELETE); } /** * Gets the remove button inside of a chip. - * @param filter Optionally filters which remove buttons are included. + * @param filter Optionally filters which chips are included. */ async getRemoveButton(filter: ChipRemoveHarnessFilters = {}): Promise { return this.locatorFor(MatChipRemoveHarness.with(filter))(); diff --git a/src/material-experimental/mdc-chips/testing/chip-input-harness.spec.ts b/src/material/chips/testing/chip-input-harness.spec.ts similarity index 100% rename from src/material-experimental/mdc-chips/testing/chip-input-harness.spec.ts rename to src/material/chips/testing/chip-input-harness.spec.ts diff --git a/src/material/chips/testing/chip-input-harness.ts b/src/material/chips/testing/chip-input-harness.ts index 6172e938d9cf..157073d6eb09 100644 --- a/src/material/chips/testing/chip-input-harness.ts +++ b/src/material/chips/testing/chip-input-harness.ts @@ -6,21 +6,29 @@ * found in the LICENSE file at https://angular.io/license */ -import {HarnessPredicate, ComponentHarness, TestKey} from '@angular/cdk/testing'; +import { + ComponentHarness, + ComponentHarnessConstructor, + HarnessPredicate, + TestKey, +} from '@angular/cdk/testing'; import {ChipInputHarnessFilters} from './chip-harness-filters'; -/** Harness for interacting with a standard Material chip inputs in tests. */ +/** Harness for interacting with a grid's chip input in tests. */ export class MatChipInputHarness extends ComponentHarness { - static hostSelector = '.mat-chip-input'; + static hostSelector = '.mat-mdc-chip-input'; /** - * Gets a `HarnessPredicate` that can be used to search for a `MatChipInputHarness` that meets - * certain criteria. + * Gets a `HarnessPredicate` that can be used to search for a chip input with specific + * attributes. * @param options Options for filtering which input instances are considered a match. * @return a `HarnessPredicate` configured with the given options. */ - static with(options: ChipInputHarnessFilters = {}): HarnessPredicate { - return new HarnessPredicate(MatChipInputHarness, options) + static with( + this: ComponentHarnessConstructor, + options: ChipInputHarnessFilters = {}, + ): HarnessPredicate { + return new HarnessPredicate(this, options) .addOption('value', options.value, async (harness, value) => { return (await harness.getValue()) === value; }) @@ -31,23 +39,23 @@ export class MatChipInputHarness extends ComponentHarness { /** Whether the input is disabled. */ async isDisabled(): Promise { - return (await this.host()).getProperty('disabled')!; + return (await this.host()).getProperty('disabled'); } /** Whether the input is required. */ async isRequired(): Promise { - return (await this.host()).getProperty('required')!; + return (await this.host()).getProperty('required'); } /** Gets the value of the input. */ async getValue(): Promise { // The "value" property of the native input is never undefined. - return (await (await this.host()).getProperty('value'))!; + return await (await this.host()).getProperty('value'); } /** Gets the placeholder of the input. */ async getPlaceholder(): Promise { - return await (await this.host()).getProperty('placeholder'); + return await (await this.host()).getProperty('placeholder'); } /** diff --git a/src/material/chips/testing/chip-list-harness.spec.ts b/src/material/chips/testing/chip-list-harness.spec.ts deleted file mode 100644 index 2eeadf5000fd..000000000000 --- a/src/material/chips/testing/chip-list-harness.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {MatChipsModule} from '@angular/material/chips'; -import {runHarnessTests} from '@angular/material/chips/testing/shared.spec'; -import {MatIconModule} from '@angular/material/icon'; -import {MatIconHarness} from '@angular/material/icon/testing'; -import {MatChipListHarness} from './chip-list-harness'; -import {MatChipHarness} from './chip-harness'; -import {MatChipInputHarness} from './chip-input-harness'; -import {MatChipRemoveHarness} from './chip-remove-harness'; -import {MatChipListboxHarness} from './chip-listbox-harness'; -import {MatChipOptionHarness} from './chip-option-harness'; - -describe('Non-MDC-based MatChipListHarness', () => { - runHarnessTests( - MatChipsModule, - MatChipListHarness, - MatChipListboxHarness, - MatChipHarness, - MatChipOptionHarness, - MatChipInputHarness, - MatChipRemoveHarness, - MatIconModule, - MatIconHarness, - ); -}); diff --git a/src/material-experimental/mdc-chips/testing/chip-listbox-harness.spec.ts b/src/material/chips/testing/chip-listbox-harness.spec.ts similarity index 100% rename from src/material-experimental/mdc-chips/testing/chip-listbox-harness.spec.ts rename to src/material/chips/testing/chip-listbox-harness.spec.ts diff --git a/src/material/chips/testing/chip-listbox-harness.ts b/src/material/chips/testing/chip-listbox-harness.ts index b804e1f069dd..01ce7f66a15d 100644 --- a/src/material/chips/testing/chip-listbox-harness.ts +++ b/src/material/chips/testing/chip-listbox-harness.ts @@ -6,24 +6,51 @@ * found in the LICENSE file at https://angular.io/license */ -import {HarnessPredicate, parallel} from '@angular/cdk/testing'; -import {MatChipOptionHarness} from './chip-option-harness'; +import { + ComponentHarness, + ComponentHarnessConstructor, + HarnessPredicate, + parallel, +} from '@angular/cdk/testing'; import {ChipListboxHarnessFilters, ChipOptionHarnessFilters} from './chip-harness-filters'; -import {_MatChipListHarnessBase} from './chip-list-harness'; +import {MatChipOptionHarness} from './chip-option-harness'; -/** Harness for interacting with a standard selectable chip list in tests. */ -export class MatChipListboxHarness extends _MatChipListHarnessBase { - /** The selector for the host element of a `MatChipList` instance. */ - static hostSelector = '.mat-chip-list'; +/** Harness for interacting with a mat-chip-listbox in tests. */ +export class MatChipListboxHarness extends ComponentHarness { + static hostSelector = '.mat-mdc-chip-listbox'; /** - * Gets a `HarnessPredicate` that can be used to search for a `MatChipListHarness` that meets - * certain criteria. - * @param options Options for filtering which chip list instances are considered a match. + * Gets a `HarnessPredicate` that can be used to search for a chip listbox with specific + * attributes. + * @param options Options for narrowing the search. * @return a `HarnessPredicate` configured with the given options. */ - static with(options: ChipListboxHarnessFilters = {}): HarnessPredicate { - return new HarnessPredicate(MatChipListboxHarness, options); + static with( + this: ComponentHarnessConstructor, + options: ChipListboxHarnessFilters = {}, + ): HarnessPredicate { + return new HarnessPredicate(this, options); + } + + /** Gets whether the chip listbox is disabled. */ + async isDisabled(): Promise { + return (await (await this.host()).getAttribute('aria-disabled')) === 'true'; + } + + /** Gets whether the chip listbox is required. */ + async isRequired(): Promise { + return (await (await this.host()).getAttribute('aria-required')) === 'true'; + } + + /** Gets whether the chip listbox is in multi selection mode. */ + async isMultiple(): Promise { + return (await (await this.host()).getAttribute('aria-multiselectable')) === 'true'; + } + + /** Gets whether the orientation of the chip list. */ + async getOrientation(): Promise<'horizontal' | 'vertical'> { + const orientation = await (await this.host()).getAttribute('aria-orientation'); + return orientation === 'vertical' ? 'vertical' : 'horizontal'; } /** diff --git a/src/material-experimental/mdc-chips/testing/chip-option-harness.spec.ts b/src/material/chips/testing/chip-option-harness.spec.ts similarity index 100% rename from src/material-experimental/mdc-chips/testing/chip-option-harness.spec.ts rename to src/material/chips/testing/chip-option-harness.spec.ts diff --git a/src/material/chips/testing/chip-option-harness.ts b/src/material/chips/testing/chip-option-harness.ts index 65e5974d9a93..e4ca239a8307 100644 --- a/src/material/chips/testing/chip-option-harness.ts +++ b/src/material/chips/testing/chip-option-harness.ts @@ -6,23 +6,24 @@ * found in the LICENSE file at https://angular.io/license */ -import {HarnessPredicate} from '@angular/cdk/testing'; +import {ComponentHarnessConstructor, HarnessPredicate} from '@angular/cdk/testing'; import {MatChipHarness} from './chip-harness'; import {ChipOptionHarnessFilters} from './chip-harness-filters'; +/** Harness for interacting with a mat-chip-option in tests. */ export class MatChipOptionHarness extends MatChipHarness { - /** The selector for the host element of a selectable chip instance. */ - static override hostSelector = '.mat-chip'; + static override hostSelector = '.mat-mdc-chip-option'; /** - * Gets a `HarnessPredicate` that can be used to search for a `MatChipOptionHarness` - * that meets certain criteria. - * @param options Options for filtering which chip instances are considered a match. + * Gets a `HarnessPredicate` that can be used to search for a chip option with specific + * attributes. + * @param options Options for narrowing the search. * @return a `HarnessPredicate` configured with the given options. */ - static override with( + static override with( + this: ComponentHarnessConstructor, options: ChipOptionHarnessFilters = {}, - ): HarnessPredicate { + ): HarnessPredicate { return new HarnessPredicate(MatChipOptionHarness, options) .addOption('text', options.text, (harness, label) => HarnessPredicate.stringMatches(harness.getText(), label), @@ -31,30 +32,30 @@ export class MatChipOptionHarness extends MatChipHarness { 'selected', options.selected, async (harness, selected) => (await harness.isSelected()) === selected, - ); + ) as unknown as HarnessPredicate; } /** Whether the chip is selected. */ - override async isSelected(): Promise { - return (await this.host()).hasClass('mat-chip-selected'); + async isSelected(): Promise { + return (await this.host()).hasClass('mat-mdc-chip-selected'); } /** Selects the given chip. Only applies if it's selectable. */ - override async select(): Promise { + async select(): Promise { if (!(await this.isSelected())) { await this.toggle(); } } /** Deselects the given chip. Only applies if it's selectable. */ - override async deselect(): Promise { + async deselect(): Promise { if (await this.isSelected()) { await this.toggle(); } } /** Toggles the selected state of the given chip. */ - override async toggle(): Promise { - return (await this.host()).sendKeys(' '); + async toggle(): Promise { + return (await this._primaryAction()).click(); } } diff --git a/src/material/chips/testing/chip-remove-harness.ts b/src/material/chips/testing/chip-remove-harness.ts index 7a1ab89b71f5..e40633b8e652 100644 --- a/src/material/chips/testing/chip-remove-harness.ts +++ b/src/material/chips/testing/chip-remove-harness.ts @@ -6,21 +6,28 @@ * found in the LICENSE file at https://angular.io/license */ -import {HarnessPredicate, ComponentHarness} from '@angular/cdk/testing'; +import { + ComponentHarness, + ComponentHarnessConstructor, + HarnessPredicate, +} from '@angular/cdk/testing'; import {ChipRemoveHarnessFilters} from './chip-harness-filters'; /** Harness for interacting with a standard Material chip remove button in tests. */ export class MatChipRemoveHarness extends ComponentHarness { - static hostSelector = '.mat-chip-remove'; + static hostSelector = '.mat-mdc-chip-remove'; /** - * Gets a `HarnessPredicate` that can be used to search for a `MatChipRemoveHarness` that meets - * certain criteria. + * Gets a `HarnessPredicate` that can be used to search for a chip remove with specific + * attributes. * @param options Options for filtering which input instances are considered a match. * @return a `HarnessPredicate` configured with the given options. */ - static with(options: ChipRemoveHarnessFilters = {}): HarnessPredicate { - return new HarnessPredicate(MatChipRemoveHarness, options); + static with( + this: ComponentHarnessConstructor, + options: ChipRemoveHarnessFilters = {}, + ): HarnessPredicate { + return new HarnessPredicate(this, options); } /** Clicks the remove button. */ diff --git a/src/material-experimental/mdc-chips/testing/chip-row-harness.spec.ts b/src/material/chips/testing/chip-row-harness.spec.ts similarity index 100% rename from src/material-experimental/mdc-chips/testing/chip-row-harness.spec.ts rename to src/material/chips/testing/chip-row-harness.spec.ts diff --git a/src/material-experimental/mdc-chips/testing/chip-row-harness.ts b/src/material/chips/testing/chip-row-harness.ts similarity index 100% rename from src/material-experimental/mdc-chips/testing/chip-row-harness.ts rename to src/material/chips/testing/chip-row-harness.ts diff --git a/src/material-experimental/mdc-chips/testing/chip-set-harness.spec.ts b/src/material/chips/testing/chip-set-harness.spec.ts similarity index 100% rename from src/material-experimental/mdc-chips/testing/chip-set-harness.spec.ts rename to src/material/chips/testing/chip-set-harness.spec.ts diff --git a/src/material-experimental/mdc-chips/testing/chip-set-harness.ts b/src/material/chips/testing/chip-set-harness.ts similarity index 100% rename from src/material-experimental/mdc-chips/testing/chip-set-harness.ts rename to src/material/chips/testing/chip-set-harness.ts diff --git a/src/material/chips/testing/public-api.ts b/src/material/chips/testing/public-api.ts index a01fcbbed031..0689ddb6c35a 100644 --- a/src/material/chips/testing/public-api.ts +++ b/src/material/chips/testing/public-api.ts @@ -6,10 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ +export * from './chip-avatar-harness'; export * from './chip-harness'; export * from './chip-harness-filters'; -export {MatChipListHarness} from './chip-list-harness'; export * from './chip-input-harness'; export * from './chip-remove-harness'; export * from './chip-option-harness'; export * from './chip-listbox-harness'; +export * from './chip-grid-harness'; +export * from './chip-row-harness'; +export * from './chip-set-harness'; diff --git a/src/material-experimental/mdc-chips/tokens.ts b/src/material/chips/tokens.ts similarity index 100% rename from src/material-experimental/mdc-chips/tokens.ts rename to src/material/chips/tokens.ts diff --git a/src/material/config.bzl b/src/material/config.bzl index c75fd52f2a08..fbeab09e6c83 100644 --- a/src/material/config.bzl +++ b/src/material/config.bzl @@ -21,6 +21,8 @@ entryPoints = [ "legacy-checkbox/testing", "chips", "chips/testing", + "legacy-chips", + "legacy-chips/testing", "core", "core/testing", "legacy-core", diff --git a/src/material/core/density/private/_all-density.scss b/src/material/core/density/private/_all-density.scss index 9c393e507a3a..a1389d6a5d39 100644 --- a/src/material/core/density/private/_all-density.scss +++ b/src/material/core/density/private/_all-density.scss @@ -16,6 +16,7 @@ @use '../../../core/option/optgroup-theme'; @use '../../../select/select-theme'; @use '../../../dialog/dialog-theme'; +@use '../../../chips/chips-theme'; @mixin private-all-unmigrated-component-densities($config) { @include expansion-theme.density($config); @@ -53,6 +54,7 @@ @include checkbox-theme.density($config); @include autocomplete-theme.density($config); @include dialog-theme.density($config); + @include chips-theme.density($config); @include private-all-unmigrated-component-densities($config); } diff --git a/src/material/core/theming/_all-theme.scss b/src/material/core/theming/_all-theme.scss index 712a05392320..2ce73e19236a 100644 --- a/src/material/core/theming/_all-theme.scss +++ b/src/material/core/theming/_all-theme.scss @@ -42,7 +42,6 @@ @include button-theme.theme($theme-or-color-config); @include button-toggle-theme.theme($theme-or-color-config); @include checkbox-theme.theme($theme-or-color-config); - @include chips-theme.theme($theme-or-color-config); @include table-theme.theme($theme-or-color-config); @include datepicker-theme.theme($theme-or-color-config); @include divider-theme.theme($theme-or-color-config); @@ -78,6 +77,7 @@ @include select-theme.theme($theme-or-color-config); @include autocomplete-theme.theme($theme-or-color-config); @include dialog-theme.theme($theme-or-color-config); + @include chips-theme.theme($theme-or-color-config); @include private-all-unmigrated-component-themes($theme-or-color-config); } } diff --git a/src/material/core/theming/tests/test-css-variables-theme.scss b/src/material/core/theming/tests/test-css-variables-theme.scss index 75edc94c7156..db4d909fbe66 100644 --- a/src/material/core/theming/tests/test-css-variables-theme.scss +++ b/src/material/core/theming/tests/test-css-variables-theme.scss @@ -7,7 +7,6 @@ @use '../../../bottom-sheet/bottom-sheet-theme'; @use '../../../button/button-theme'; @use '../../../button-toggle/button-toggle-theme'; -@use '../../../chips/chips-theme'; @use '../../../table/table-theme'; @use '../../../datepicker/datepicker-theme'; @use '../../../divider/divider-theme'; @@ -61,7 +60,6 @@ @include bottom-sheet-theme.theme($css-var-theme); @include button-theme.theme($css-var-theme); @include button-toggle-theme.theme($css-var-theme); - @include chips-theme.theme($css-var-theme); @include table-theme.theme($css-var-theme); @include datepicker-theme.theme($css-var-theme); @include divider-theme.theme($css-var-theme); diff --git a/src/material/core/theming/tests/test-theming-api.scss b/src/material/core/theming/tests/test-theming-api.scss index 8c43d90d988e..18c33565efa4 100644 --- a/src/material/core/theming/tests/test-theming-api.scss +++ b/src/material/core/theming/tests/test-theming-api.scss @@ -63,8 +63,8 @@ $new-api-dark-theme-default-warn: theming.define-dark-theme(( color: (primary: $primary, accent: $accent) )); -$light-theme-only-density: theming.define-light-theme((density: -3)); -$dark-theme-only-density: theming.define-dark-theme((density: -3)); +$light-theme-only-density: theming.define-light-theme((density: -2)); +$dark-theme-only-density: theming.define-dark-theme((density: -2)); $typography-config: typography.define-typography-config(); $light-theme-only-typography: theming.define-light-theme((typography: $typography-config)); @@ -108,9 +108,9 @@ $dark-theme-only-typography: theming.define-dark-theme((typography: $typography- // Test which ensures that constructed themes pass through the specified // density and typography configurations. @mixin test-density-typography-config-pass-through() { - @include assert(map.get($light-theme-only-density, density), -3, + @include assert(map.get($light-theme-only-density, density), -2, 'Expected density config to be included in theme.'); - @include assert(map.get($dark-theme-only-density, density), -3, + @include assert(map.get($dark-theme-only-density, density), -2, 'Expected density config to be included in theme.'); @include assert(map.get($light-theme-only-typography, typography), $typography-config, 'Expected typography config to be included in theme.'); @@ -148,7 +148,7 @@ $dark-theme-only-typography: theming.define-dark-theme((typography: $typography- $density-config: map.get($light-theme-only-density, density); $no-density-light-theme: theming.define-light-theme((density: null)); $no-density-dark-theme: theming.define-dark-theme((density: null)); - @include assert(theming.get-density-config($light-theme-only-density), -3, + @include assert(theming.get-density-config($light-theme-only-density), -2, 'Expected that a density config can be read from a theme object.'); @include assert(theming.get-density-config($light-theme-only-typography), 0, 'Expected that by default, the density system will be configured.'); diff --git a/src/material/core/typography/_all-typography.scss b/src/material/core/typography/_all-typography.scss index f54457523338..838e8ab3e53b 100644 --- a/src/material/core/typography/_all-typography.scss +++ b/src/material/core/typography/_all-typography.scss @@ -44,7 +44,6 @@ @include button-theme.typography($config); @include button-toggle-theme.typography($config); @include checkbox-theme.typography($config); - @include chips-theme.typography($config); @include divider-theme.typography($config); @include table-theme.typography($config); @include datepicker-theme.typography($config); @@ -94,6 +93,7 @@ @include select-theme.typography($config); @include autocomplete-theme.typography($config); @include dialog-theme.typography($config); + @include chips-theme.typography($config); } // @deprecated Use `all-component-typographies`. diff --git a/src/material-experimental/mdc-chips/BUILD.bazel b/src/material/legacy-chips/BUILD.bazel similarity index 51% rename from src/material-experimental/mdc-chips/BUILD.bazel rename to src/material/legacy-chips/BUILD.bazel index 3522ed6372d9..66f6532981f1 100644 --- a/src/material-experimental/mdc-chips/BUILD.bazel +++ b/src/material/legacy-chips/BUILD.bazel @@ -1,5 +1,6 @@ load( "//tools:defaults.bzl", + "markdown_to_html", "ng_module", "ng_test_library", "ng_web_test_suite", @@ -10,67 +11,50 @@ load( package(default_visibility = ["//visibility:public"]) ng_module( - name = "mdc-chips", + name = "legacy-chips", srcs = glob( ["**/*.ts"], - exclude = [ - "**/*.spec.ts", - ], + exclude = ["**/*.spec.ts"], ), - assets = [ - ":chip_scss", - ":chip_set_scss", - ] + glob(["**/*.html"]), + assets = [":chips.css"] + glob(["**/*.html"]), deps = [ - "//src:dev_mode_types", + "//src/cdk/a11y", + "//src/cdk/bidi", + "//src/cdk/coercion", + "//src/cdk/collections", + "//src/cdk/keycodes", + "//src/cdk/platform", "//src/material/core", - "//src/material/form-field", - "@npm//@angular/animations", - "@npm//@angular/common", + "//src/material/legacy-form-field", "@npm//@angular/core", "@npm//@angular/forms", + "@npm//rxjs", ], ) sass_library( - name = "mdc_chips_scss_lib", + name = "legacy_chips_scss_lib", srcs = glob(["**/_*.scss"]), - deps = [ - "//:mdc_sass_lib", - "//src/material:sass_lib", - "//src/material/core:core_scss_lib", - ], + deps = ["//src/material/core:core_scss_lib"], ) sass_binary( - name = "chip_scss", - src = "chip.scss", + name = "chips_scss", + src = "chips.scss", deps = [ - "//:mdc_sass_lib", "//src/cdk:sass_lib", - "//src/material:sass_lib", - "//src/material/core:core_scss_lib", - ], -) - -sass_binary( - name = "chip_set_scss", - src = "chip-set.scss", - deps = [ - "//:mdc_sass_lib", - "//src/material:sass_lib", "//src/material/core:core_scss_lib", ], ) ng_test_library( - name = "chips_tests_lib", + name = "unit_test_sources", srcs = glob( ["**/*.spec.ts"], exclude = ["**/*.e2e.spec.ts"], ), deps = [ - ":mdc-chips", + ":legacy-chips", "//src/cdk/a11y", "//src/cdk/bidi", "//src/cdk/keycodes", @@ -78,10 +62,9 @@ ng_test_library( "//src/cdk/testing", "//src/cdk/testing/private", "//src/material/core", - "//src/material/form-field", - "//src/material/input", + "//src/material/legacy-form-field", + "//src/material/legacy-input", "@npm//@angular/animations", - "@npm//@angular/common", "@npm//@angular/forms", "@npm//@angular/platform-browser", "@npm//rxjs", @@ -90,7 +73,15 @@ ng_test_library( ng_web_test_suite( name = "unit_tests", - deps = [ - ":chips_tests_lib", - ], + deps = [":unit_test_sources"], +) + +markdown_to_html( + name = "overview", + srcs = [":chips.md"], +) + +filegroup( + name = "source-files", + srcs = glob(["**/*.ts"]), ) diff --git a/src/material/legacy-chips/README.md b/src/material/legacy-chips/README.md new file mode 100644 index 000000000000..d7a05de16e1d --- /dev/null +++ b/src/material/legacy-chips/README.md @@ -0,0 +1 @@ +Please see the official documentation at https://material.angular.io/components/component/chips \ No newline at end of file diff --git a/src/material/legacy-chips/_chips-legacy-index.scss b/src/material/legacy-chips/_chips-legacy-index.scss new file mode 100644 index 000000000000..aa532a13bcd2 --- /dev/null +++ b/src/material/legacy-chips/_chips-legacy-index.scss @@ -0,0 +1,7 @@ +@forward 'chips-theme' hide $chip-remove-font-size, color, theme, typography; +@forward 'chips-theme' as mat-legacy-* hide mat-legacy-chip-element-color, + mat-legacy-chip-theme-color, mat-color, mat-density, mat-ripple-background, mat-theme, + mat-typography; +@forward 'chips-theme' as mat-legacy-chips-* hide $mat-legacy-chips-chip-remove-font-size, + mat-legacy-chips-chip-element-color, mat-legacy-chips-chip-theme-color, mat-legacy-chips-density, + mat-legacy-chips-ripple-background; diff --git a/src/material/chips/_chips-legacy-index.scss b/src/material/legacy-chips/_chips-theme.import.scss similarity index 59% rename from src/material/chips/_chips-legacy-index.scss rename to src/material/legacy-chips/_chips-theme.import.scss index 78d51b9a76f4..a03aa8abd053 100644 --- a/src/material/chips/_chips-legacy-index.scss +++ b/src/material/legacy-chips/_chips-theme.import.scss @@ -1,6 +1,14 @@ +@forward '../core/style/private.import'; +@forward '../core/theming/theming.import'; +@forward '../core/typography/typography-utils.import'; @forward 'chips-theme' hide $chip-remove-font-size, color, theme, typography; @forward 'chips-theme' as mat-* hide mat-chip-element-color, mat-chip-theme-color, mat-color, mat-density, mat-ripple-background, mat-theme, mat-typography; @forward 'chips-theme' as mat-chips-* hide $mat-chips-chip-remove-font-size, mat-chips-chip-element-color, mat-chips-chip-theme-color, mat-chips-density, mat-chips-ripple-background; + +@import '../core/style/private'; +@import '../core/theming/palette'; +@import '../core/theming/theming'; +@import '../core/typography/typography-utils'; diff --git a/src/material/legacy-chips/_chips-theme.scss b/src/material/legacy-chips/_chips-theme.scss new file mode 100644 index 000000000000..b63ed20e1d93 --- /dev/null +++ b/src/material/legacy-chips/_chips-theme.scss @@ -0,0 +1,127 @@ +@use 'sass:map'; +@use 'sass:meta'; +@use '../core/style/private'; +@use '../core/theming/theming'; +@use '../core/typography/typography'; +@use '../core/typography/typography-utils'; + +$chip-remove-font-size: 18px; + +@mixin _element-color($foreground, $background) { + background-color: $background; + color: $foreground; + + .mat-chip-remove { + color: $foreground; + opacity: 0.4; + } +} + + +// Applies the background color for a ripple element. +// If the color value provided is not a Sass color, +// we assume that we've been given a CSS variable. +// Since we can't perform alpha-blending on a CSS variable, +// we instead add the opacity directly to the ripple element. +@mixin _ripple-background($palette, $default-contrast, $opacity) { + $background-color: theming.get-color-from-palette($palette, $default-contrast, $opacity); + background-color: $background-color; + @if (meta.type-of($background-color) != color) { + opacity: $opacity; + } +} + +@mixin _palette-styles($palette) { + @include _element-color(theming.get-color-from-palette($palette, default-contrast), + theming.get-color-from-palette($palette)); + + .mat-ripple-element { + @include _ripple-background($palette, default-contrast, 0.1); + } +} + +@mixin color($config-or-theme) { + $config: theming.get-color-config($config-or-theme); + $is-dark-theme: map.get($config, is-dark); + $primary: map.get($config, primary); + $accent: map.get($config, accent); + $warn: map.get($config, warn); + $background: map.get($config, background); + $foreground: map.get($config, foreground); + + $unselected-background: theming.get-color-from-palette($background, unselected-chip); + $unselected-foreground: theming.get-color-from-palette($foreground, text); + + .mat-chip.mat-standard-chip { + @include _element-color($unselected-foreground, $unselected-background); + + &:not(.mat-chip-disabled) { + &:active { + @include private.private-theme-elevation(3, $config); + } + + .mat-chip-remove:hover { + opacity: 0.54; + } + } + + &.mat-chip-disabled { + opacity: 0.4; + } + + &::after { + background: map.get($foreground, base); + } + } + + .mat-chip.mat-standard-chip.mat-chip-selected { + &.mat-primary { + @include _palette-styles($primary); + } + + &.mat-warn { + @include _palette-styles($warn); + } + + &.mat-accent { + @include _palette-styles($accent); + } + } +} + +@mixin typography($config-or-theme) { + $config: typography.private-typography-to-2014-config( + theming.get-typography-config($config-or-theme)); + .mat-chip { + font-size: typography-utils.font-size($config, body-2); + font-weight: typography-utils.font-weight($config, body-2); + + .mat-chip-trailing-icon.mat-icon, + .mat-chip-remove.mat-icon { + font-size: $chip-remove-font-size; + } + } +} + +@mixin _density($config-or-theme) {} + +@mixin theme($theme-or-color-config) { + $theme: theming.private-legacy-get-theme($theme-or-color-config); + @include theming.private-check-duplicate-theme-styles($theme, 'mat-legacy-chips') { + $color: theming.get-color-config($theme); + $density: theming.get-density-config($theme); + $typography: theming.get-typography-config($theme); + + @if $color != null { + @include color($color); + } + @if $density != null { + @include _density($density); + } + @if $typography != null { + @include typography($typography); + } + } +} + + diff --git a/src/material/chips/chip-default-options.ts b/src/material/legacy-chips/chip-default-options.ts similarity index 80% rename from src/material/chips/chip-default-options.ts rename to src/material/legacy-chips/chip-default-options.ts index 3f9f392cf6aa..be45947e52c8 100644 --- a/src/material/chips/chip-default-options.ts +++ b/src/material/legacy-chips/chip-default-options.ts @@ -9,12 +9,12 @@ import {InjectionToken} from '@angular/core'; /** Default options, for the chips module, that can be overridden. */ -export interface MatChipsDefaultOptions { +export interface MatLegacyChipsDefaultOptions { /** The list of key codes that will trigger a chipEnd event. */ separatorKeyCodes: readonly number[] | ReadonlySet; } /** Injection token to be used to override the default options for the chips module. */ -export const MAT_CHIPS_DEFAULT_OPTIONS = new InjectionToken( +export const MAT_CHIPS_DEFAULT_OPTIONS = new InjectionToken( 'mat-chips-default-options', ); diff --git a/src/material-experimental/mdc-chips/chip-input.spec.ts b/src/material/legacy-chips/chip-input.spec.ts similarity index 67% rename from src/material-experimental/mdc-chips/chip-input.spec.ts rename to src/material/legacy-chips/chip-input.spec.ts index 58f1f91e6aec..5e5d0f1fbc67 100644 --- a/src/material-experimental/mdc-chips/chip-input.spec.ts +++ b/src/material/legacy-chips/chip-input.spec.ts @@ -3,31 +3,32 @@ import {COMMA, ENTER, TAB} from '@angular/cdk/keycodes'; import {PlatformModule} from '@angular/cdk/platform'; import {dispatchKeyboardEvent} from '../../cdk/testing/private'; import {Component, DebugElement, ViewChild} from '@angular/core'; -import {waitForAsync, ComponentFixture, fakeAsync, TestBed, flush} from '@angular/core/testing'; -import {MatFormFieldModule} from '@angular/material/form-field'; +import {waitForAsync, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {MatLegacyFormFieldModule} from '@angular/material/legacy-form-field'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {Subject} from 'rxjs'; -import { - MAT_CHIPS_DEFAULT_OPTIONS, - MatChipGrid, - MatChipInput, - MatChipInputEvent, - MatChipsDefaultOptions, - MatChipsModule, -} from './index'; - -describe('MDC-based MatChipInput', () => { +import {MAT_CHIPS_DEFAULT_OPTIONS, MatLegacyChipsDefaultOptions} from './chip-default-options'; +import {MatLegacyChipInput, MatLegacyChipInputEvent} from './chip-input'; +import {MatLegacyChipList} from './chip-list'; +import {MatLegacyChipsModule} from './index'; + +describe('MatChipInput', () => { let fixture: ComponentFixture; let testChipInput: TestChipInput; let inputDebugElement: DebugElement; let inputNativeElement: HTMLElement; - let chipInputDirective: MatChipInput; - let dir = 'ltr'; + let chipInputDirective: MatLegacyChipInput; + const dir = 'ltr'; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [PlatformModule, MatChipsModule, MatFormFieldModule, NoopAnimationsModule], + imports: [ + PlatformModule, + MatLegacyChipsModule, + MatLegacyFormFieldModule, + NoopAnimationsModule, + ], declarations: [TestChipInput], providers: [ { @@ -50,8 +51,8 @@ describe('MDC-based MatChipInput', () => { testChipInput = fixture.debugElement.componentInstance; fixture.detectChanges(); - inputDebugElement = fixture.debugElement.query(By.directive(MatChipInput))!; - chipInputDirective = inputDebugElement.injector.get(MatChipInput); + inputDebugElement = fixture.debugElement.query(By.directive(MatLegacyChipInput))!; + chipInputDirective = inputDebugElement.injector.get(MatLegacyChipInput); inputNativeElement = inputDebugElement.nativeElement; })); @@ -76,60 +77,84 @@ describe('MDC-based MatChipInput', () => { expect(inputNativeElement.getAttribute('placeholder')).toBe('bound placeholder'); }); - it('should become disabled if the list is disabled', () => { - expect(inputNativeElement.hasAttribute('disabled')).toBe(false); - expect(chipInputDirective.disabled).toBe(false); - - fixture.componentInstance.chipGridInstance.disabled = true; + it('should propagate the dynamic `placeholder` value to the form field', () => { + fixture.componentInstance.placeholder = 'add a chip'; fixture.detectChanges(); - expect(inputNativeElement.getAttribute('disabled')).toBe('true'); - expect(chipInputDirective.disabled).toBe(true); - }); + const label: HTMLElement = fixture.nativeElement.querySelector('.mat-form-field-label'); - it('should be aria-required if the list is required', () => { - expect(inputNativeElement.hasAttribute('aria-required')).toBe(false); + expect(label).toBeTruthy(); + expect(label.textContent).toContain('add a chip'); - fixture.componentInstance.required = true; + fixture.componentInstance.placeholder = "or don't"; fixture.detectChanges(); - expect(inputNativeElement.getAttribute('aria-required')).toBe('true'); + expect(label.textContent).toContain("or don't"); }); - it('should be required if the list is required', () => { - expect(inputNativeElement.hasAttribute('required')).toBe(false); + it('should become disabled if the list is disabled', () => { + expect(inputNativeElement.hasAttribute('disabled')).toBe(false); + expect(chipInputDirective.disabled).toBe(false); - fixture.componentInstance.required = true; + fixture.componentInstance.chipListInstance.disabled = true; fixture.detectChanges(); - expect(inputNativeElement.getAttribute('required')).toBe('true'); + expect(inputNativeElement.getAttribute('disabled')).toBe('true'); + expect(chipInputDirective.disabled).toBe(true); }); it('should allow focus to escape when tabbing forwards', fakeAsync(() => { - const gridElement: HTMLElement = fixture.nativeElement.querySelector('mat-chip-grid'); + const listElement: HTMLElement = fixture.nativeElement.querySelector('.mat-chip-list'); - expect(gridElement.getAttribute('tabindex')).toBe('0'); + expect(listElement.getAttribute('tabindex')).toBe('0'); - dispatchKeyboardEvent(gridElement, 'keydown', TAB); + dispatchKeyboardEvent(inputNativeElement, 'keydown', TAB); fixture.detectChanges(); - expect(gridElement.getAttribute('tabindex')) + expect(listElement.getAttribute('tabindex')) .withContext('Expected tabIndex to be set to -1 temporarily.') .toBe('-1'); - flush(); + tick(); fixture.detectChanges(); - expect(gridElement.getAttribute('tabindex')) + expect(listElement.getAttribute('tabindex')) .withContext('Expected tabIndex to be reset back to 0') .toBe('0'); })); + it('should not allow focus to escape when tabbing backwards', fakeAsync(() => { + const listElement: HTMLElement = fixture.nativeElement.querySelector('.mat-chip-list'); + + expect(listElement.getAttribute('tabindex')).toBe('0'); + + dispatchKeyboardEvent(inputNativeElement, 'keydown', TAB, undefined, {shift: true}); + fixture.detectChanges(); + + expect(listElement.getAttribute('tabindex')) + .withContext('Expected tabindex to remain 0') + .toBe('0'); + + tick(); + fixture.detectChanges(); + + expect(listElement.getAttribute('tabindex')) + .withContext('Expected tabindex to remain 0') + .toBe('0'); + })); + + it('should be aria-required if the list is required', () => { + expect(inputNativeElement.hasAttribute('aria-required')).toBe(false); + + fixture.componentInstance.required = true; + fixture.detectChanges(); + + expect(inputNativeElement.getAttribute('aria-required')).toBe('true'); + }); + it('should set input styling classes', () => { - expect(inputNativeElement.classList).toContain('mat-mdc-input-element'); - expect(inputNativeElement.classList).toContain('mat-mdc-form-field-input-control'); - expect(inputNativeElement.classList).toContain('mat-mdc-chip-input'); - expect(inputNativeElement.classList).toContain('mdc-text-field__input'); + expect(inputNativeElement.classList).toContain('mat-input-element'); + expect(inputNativeElement.classList).toContain('mat-chip-input'); }); }); @@ -191,12 +216,17 @@ describe('MDC-based MatChipInput', () => { TestBed.resetTestingModule() .configureTestingModule({ - imports: [MatChipsModule, MatFormFieldModule, PlatformModule, NoopAnimationsModule], + imports: [ + MatLegacyChipsModule, + MatLegacyFormFieldModule, + PlatformModule, + NoopAnimationsModule, + ], declarations: [TestChipInput], providers: [ { provide: MAT_CHIPS_DEFAULT_OPTIONS, - useValue: {separatorKeyCodes: [COMMA]} as MatChipsDefaultOptions, + useValue: {separatorKeyCodes: [COMMA]} as MatLegacyChipsDefaultOptions, }, ], }) @@ -206,8 +236,8 @@ describe('MDC-based MatChipInput', () => { testChipInput = fixture.debugElement.componentInstance; fixture.detectChanges(); - inputDebugElement = fixture.debugElement.query(By.directive(MatChipInput))!; - chipInputDirective = inputDebugElement.injector.get(MatChipInput); + inputDebugElement = fixture.debugElement.query(By.directive(MatLegacyChipInput))!; + chipInputDirective = inputDebugElement.injector.get(MatLegacyChipInput); inputNativeElement = inputDebugElement.nativeElement; spyOn(testChipInput, 'add'); @@ -226,47 +256,27 @@ describe('MDC-based MatChipInput', () => { dispatchKeyboardEvent(inputNativeElement, 'keydown', ENTER, undefined, {shift: true}); expect(testChipInput.add).not.toHaveBeenCalled(); }); - - it('should set aria-describedby correctly when a non-empty list of ids is passed to setDescribedByIds', fakeAsync(() => { - const ids = ['a', 'b', 'c']; - - testChipInput.chipGridInstance.setDescribedByIds(ids); - flush(); - fixture.detectChanges(); - - expect(inputNativeElement.getAttribute('aria-describedby')).toEqual('a b c'); - })); - - it('should set aria-describedby correctly when an empty list of ids is passed to setDescribedByIds', fakeAsync(() => { - const ids: string[] = []; - - testChipInput.chipGridInstance.setDescribedByIds(ids); - flush(); - fixture.detectChanges(); - - expect(inputNativeElement.getAttribute('aria-describedby')).toBeNull(); - })); }); }); @Component({ template: ` - - Hello - + Hello + - + `, }) class TestChipInput { - @ViewChild(MatChipGrid) chipGridInstance: MatChipGrid; - addOnBlur: boolean = false; - placeholder = ''; + @ViewChild(MatLegacyChipList) chipListInstance: MatLegacyChipList; + addOnBlur = false; required = false; + placeholder = ''; - add(_: MatChipInputEvent) {} + add(_: MatLegacyChipInputEvent) {} } diff --git a/src/material-experimental/mdc-chips/chip-input.ts b/src/material/legacy-chips/chip-input.ts similarity index 69% rename from src/material-experimental/mdc-chips/chip-input.ts rename to src/material/legacy-chips/chip-input.ts index 776f7887d35f..1089c2d969cd 100644 --- a/src/material-experimental/mdc-chips/chip-input.ts +++ b/src/material/legacy-chips/chip-input.ts @@ -7,7 +7,7 @@ */ import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {BACKSPACE, hasModifierKey} from '@angular/cdk/keycodes'; +import {BACKSPACE, hasModifierKey, TAB} from '@angular/cdk/keycodes'; import { AfterContentInit, Directive, @@ -17,16 +17,14 @@ import { Input, OnChanges, OnDestroy, - Optional, Output, } from '@angular/core'; -import {MatFormField, MAT_FORM_FIELD} from '@angular/material/form-field'; -import {MatChipsDefaultOptions, MAT_CHIPS_DEFAULT_OPTIONS} from './tokens'; -import {MatChipGrid} from './chip-grid'; -import {MatChipTextControl} from './chip-text-control'; +import {MatLegacyChipsDefaultOptions, MAT_CHIPS_DEFAULT_OPTIONS} from './chip-default-options'; +import {MatLegacyChipList} from './chip-list'; +import {MatLegacyChipTextControl} from './chip-text-control'; /** Represents an input event on a `matChipInput`. */ -export interface MatChipInputEvent { +export interface MatLegacyChipInputEvent { /** * The native `` element that the event is being fired for. * @deprecated Use `MatChipInputEvent#chipInput.inputElement` instead. @@ -38,7 +36,7 @@ export interface MatChipInputEvent { value: string; /** Reference to the chip input that emitted the event. */ - chipInput: MatChipInput; + chipInput: MatLegacyChipInput; } // Increasing integer for generating unique ids. @@ -46,16 +44,13 @@ let nextUniqueId = 0; /** * Directive that adds chip-specific behaviors to an input element inside ``. - * May be placed inside or outside of a ``. + * May be placed inside or outside of an ``. */ @Directive({ selector: 'input[matChipInputFor]', exportAs: 'matChipInput, matChipInputFor', host: { - // TODO: eventually we should remove `mat-input-element` from here since it comes from the - // non-MDC version of the input. It's currently being kept for backwards compatibility, because - // the MDC chips were landed initially with it. - 'class': 'mat-mdc-chip-input mat-mdc-input-element mdc-text-field__input mat-input-element', + 'class': 'mat-chip-input mat-input-element', '(keydown)': '_keydown($event)', '(keyup)': '_keyup($event)', '(blur)': '_blur()', @@ -64,29 +59,26 @@ let nextUniqueId = 0; '[id]': 'id', '[attr.disabled]': 'disabled || null', '[attr.placeholder]': 'placeholder || null', - '[attr.aria-invalid]': '_chipGrid && _chipGrid.ngControl ? _chipGrid.ngControl.invalid : null', - '[attr.aria-describedby]': '_ariaDescribedby || null', - '[attr.aria-required]': '_chipGrid && _chipGrid.required || null', - '[attr.required]': '_chipGrid && _chipGrid.required || null', + '[attr.aria-invalid]': '_chipList && _chipList.ngControl ? _chipList.ngControl.invalid : null', + '[attr.aria-required]': '_chipList && _chipList.required || null', }, }) -export class MatChipInput implements MatChipTextControl, AfterContentInit, OnChanges, OnDestroy { +export class MatLegacyChipInput + implements MatLegacyChipTextControl, OnChanges, OnDestroy, AfterContentInit +{ /** Used to prevent focus moving to chips while user is holding backspace */ private _focusLastChipOnBackspace: boolean; - /** Value for ariaDescribedby property */ - _ariaDescribedby?: string; - /** Whether the control is focused. */ focused: boolean = false; - _chipGrid: MatChipGrid; + _chipList: MatLegacyChipList; /** Register input for chip list */ @Input('matChipInputFor') - set chipGrid(value: MatChipGrid) { + set chipList(value: MatLegacyChipList) { if (value) { - this._chipGrid = value; - this._chipGrid.registerInput(this); + this._chipList = value; + this._chipList.registerInput(this); } } @@ -112,19 +104,18 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha this._defaultOptions.separatorKeyCodes; /** Emitted when a chip is to be added. */ - @Output('matChipInputTokenEnd') - readonly chipEnd: EventEmitter = new EventEmitter(); + @Output('matChipInputTokenEnd') readonly chipEnd = new EventEmitter(); /** The input's placeholder text. */ @Input() placeholder: string = ''; /** Unique id for the input. */ - @Input() id: string = `mat-mdc-chip-list-input-${nextUniqueId++}`; + @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); + return this._disabled || (this._chipList && this._chipList.disabled); } set disabled(value: BooleanInput) { this._disabled = coerceBooleanProperty(value); @@ -141,18 +132,13 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha constructor( protected _elementRef: ElementRef, - @Inject(MAT_CHIPS_DEFAULT_OPTIONS) private _defaultOptions: MatChipsDefaultOptions, - @Optional() @Inject(MAT_FORM_FIELD) formField?: MatFormField, + @Inject(MAT_CHIPS_DEFAULT_OPTIONS) private _defaultOptions: MatLegacyChipsDefaultOptions, ) { this.inputElement = this._elementRef.nativeElement as HTMLInputElement; - - if (formField) { - this.inputElement.classList.add('mat-mdc-form-field-input-control'); - } } - ngOnChanges() { - this._chipGrid.stateChanges.next(); + ngOnChanges(): void { + this._chipList.stateChanges.next(); } ngOnDestroy(): void { @@ -166,11 +152,17 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha /** Utility method to make host definition/tests more clear. */ _keydown(event?: KeyboardEvent) { if (event) { + // 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.keyCode === TAB && !hasModifierKey(event, 'shiftKey')) { + this._chipList._allowFocusEscape(); + } + // To prevent the user from accidentally deleting chips when pressing BACKSPACE continuously, // We focus the last chip on backspace only after the user has released the backspace button, - // And the input is empty (see behaviour in _keyup) + // and the input is empty (see behaviour in _keyup) if (event.keyCode === BACKSPACE && this._focusLastChipOnBackspace) { - this._chipGrid._focusLastChip(); + this._chipList._keyManager.setLastItemActive(); event.preventDefault(); return; } else { @@ -199,20 +191,24 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha } this.focused = false; // Blur the chip list if it is not focused - if (!this._chipGrid.focused) { - this._chipGrid._blur(); + if (!this._chipList.focused) { + this._chipList._blur(); } - this._chipGrid.stateChanges.next(); + this._chipList.stateChanges.next(); } _focus() { this.focused = true; this._focusLastChipOnBackspace = this.empty; - this._chipGrid.stateChanges.next(); + this._chipList.stateChanges.next(); } /** Checks to see if the (chipEnd) event needs to be emitted. */ _emitChipEnd(event?: KeyboardEvent) { + if (!this.inputElement.value && !!event) { + this._chipList._keydown(event); + } + if (!event || this._isSeparatorKey(event)) { this.chipEnd.emit({ input: this.inputElement, @@ -226,12 +222,12 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha _onInput() { // Let chip list know whenever the value changes. - this._chipGrid.stateChanges.next(); + this._chipList.stateChanges.next(); } /** Focuses the input. */ - focus(): void { - this.inputElement.focus(); + focus(options?: FocusOptions): void { + this.inputElement.focus(options); } /** Clears the input */ @@ -240,10 +236,6 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha this._focusLastChipOnBackspace = true; } - setDescribedByIds(ids: string[]): void { - this._ariaDescribedby = ids.join(' '); - } - /** Checks whether a keycode is one of the configured separators. */ private _isSeparatorKey(event: KeyboardEvent) { return !hasModifierKey(event) && new Set(this.separatorKeyCodes).has(event.keyCode); diff --git a/src/material/chips/chip-list.spec.ts b/src/material/legacy-chips/chip-list.spec.ts similarity index 97% rename from src/material/chips/chip-list.spec.ts rename to src/material/legacy-chips/chip-list.spec.ts index 0295e29d46ce..c62fc4d18c3b 100644 --- a/src/material/chips/chip-list.spec.ts +++ b/src/material/legacy-chips/chip-list.spec.ts @@ -48,18 +48,23 @@ import {By} from '@angular/platform-browser'; import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations'; import {Subject} from 'rxjs'; import {MatLegacyInputModule} from '../legacy-input/index'; -import {MatChip} from './chip'; -import {MatChipInputEvent} from './chip-input'; -import {MatChipEvent, MatChipList, MatChipRemove, MatChipsModule} from './index'; +import {MatLegacyChip} from './chip'; +import {MatLegacyChipInputEvent} from './chip-input'; +import { + MatLegacyChipEvent, + MatLegacyChipList, + MatLegacyChipRemove, + MatLegacyChipsModule, +} from './index'; describe('MatChipList', () => { let fixture: ComponentFixture; let chipListDebugElement: DebugElement; let chipListNativeElement: HTMLElement; - let chipListInstance: MatChipList; + let chipListInstance: MatLegacyChipList; let testComponent: StandardChipList; - let chips: QueryList; - let manager: FocusKeyManager; + let chips: QueryList; + let manager: FocusKeyManager; let zone: MockNgZone; let dirChange: Subject; @@ -153,7 +158,7 @@ describe('MatChipList', () => { beforeEach(() => { fixture = createComponent(SelectedChipList); fixture.detectChanges(); - chipListDebugElement = fixture.debugElement.query(By.directive(MatChipList))!; + chipListDebugElement = fixture.debugElement.query(By.directive(MatLegacyChipList))!; chipListNativeElement = chipListDebugElement.nativeElement; }); @@ -319,7 +324,7 @@ describe('MatChipList', () => { fixture = createComponent(StandardChipListWithAnimations, [], BrowserAnimationsModule); fixture.detectChanges(); - chipListDebugElement = fixture.debugElement.query(By.directive(MatChipList))!; + chipListDebugElement = fixture.debugElement.query(By.directive(MatLegacyChipList))!; chipListNativeElement = chipListDebugElement.nativeElement; chipListInstance = chipListDebugElement.componentInstance; testComponent = fixture.debugElement.componentInstance; @@ -638,15 +643,15 @@ describe('MatChipList', () => { }); describe('with chip remove', () => { - let chipList: MatChipList; + let chipList: MatLegacyChipList; let chipRemoveDebugElements: DebugElement[]; beforeEach(() => { fixture = createComponent(ChipListWithRemove); fixture.detectChanges(); - chipList = fixture.debugElement.query(By.directive(MatChipList))!.componentInstance; - chipRemoveDebugElements = fixture.debugElement.queryAll(By.directive(MatChipRemove)); + chipList = fixture.debugElement.query(By.directive(MatLegacyChipList))!.componentInstance; + chipRemoveDebugElements = fixture.debugElement.queryAll(By.directive(MatLegacyChipRemove)); chips = chipList.chips; }); @@ -676,7 +681,7 @@ describe('MatChipList', () => { .queryAll(By.css('mat-chip')) .map(chip => chip.nativeElement); - chipListDebugElement = fixture.debugElement.query(By.directive(MatChipList))!; + chipListDebugElement = fixture.debugElement.query(By.directive(MatLegacyChipList))!; chipListInstance = chipListDebugElement.componentInstance; chips = chipListInstance.chips; }); @@ -1044,7 +1049,7 @@ describe('MatChipList', () => { fixture = createComponent(ChipListInsideDynamicFormGroup); fixture.detectChanges(); const instance = fixture.componentInstance; - const list: MatChipList = instance.chipList; + const list: MatLegacyChipList = instance.chipList; expect(list.disabled).toBe(false); expect(list.chips.toArray().every(chip => chip.disabled)).toBe(false); @@ -1264,7 +1269,7 @@ describe('MatChipList', () => { const expectLastItemFocused = () => expect(manager.activeItemIndex).toEqual(chips.length - 1); beforeEach(() => { - chipListDebugElement = fixture.debugElement.query(By.directive(MatChipList))!; + chipListDebugElement = fixture.debugElement.query(By.directive(MatLegacyChipList))!; chipListInstance = chipListDebugElement.componentInstance; chips = chipListInstance.chips; manager = fixture.componentInstance.chipList._keyManager; @@ -1475,13 +1480,13 @@ describe('MatChipList', () => { it('should not throw when accessing the selected value too early in single selection mode', fakeAsync(() => { fixture = createComponent(StandardChipList); - const chipList = fixture.debugElement.query(By.directive(MatChipList)).componentInstance; + const chipList = fixture.debugElement.query(By.directive(MatLegacyChipList)).componentInstance; expect(() => chipList.selected).not.toThrow(); })); it('should not throw when accessing the selected value too early in multi selection mode', fakeAsync(() => { fixture = createComponent(StandardChipList); - const chipList = fixture.debugElement.query(By.directive(MatChipList)).componentInstance; + const chipList = fixture.debugElement.query(By.directive(MatLegacyChipList)).componentInstance; chipList.multiple = true; expect(() => chipList.selected).not.toThrow(); })); @@ -1497,7 +1502,7 @@ describe('MatChipList', () => { imports: [ FormsModule, ReactiveFormsModule, - MatChipsModule, + MatLegacyChipsModule, MatLegacyFormFieldModule, MatLegacyInputModule, animationsModule, @@ -1522,7 +1527,7 @@ describe('MatChipList', () => { ]); fixture.detectChanges(); - chipListDebugElement = fixture.debugElement.query(By.directive(MatChipList))!; + chipListDebugElement = fixture.debugElement.query(By.directive(MatLegacyChipList))!; chipListNativeElement = chipListDebugElement.nativeElement; chipListInstance = chipListDebugElement.componentInstance; testComponent = fixture.debugElement.componentInstance; @@ -1533,7 +1538,7 @@ describe('MatChipList', () => { fixture = createComponent(FormFieldChipList); fixture.detectChanges(); - chipListDebugElement = fixture.debugElement.query(By.directive(MatChipList))!; + chipListDebugElement = fixture.debugElement.query(By.directive(MatLegacyChipList))!; chipListNativeElement = chipListDebugElement.nativeElement; chipListInstance = chipListDebugElement.componentInstance; testComponent = fixture.debugElement.componentInstance; @@ -1609,8 +1614,8 @@ class BasicChipList { tabIndexOverride: number; selectable: boolean; - @ViewChild(MatChipList) chipList: MatChipList; - @ViewChildren(MatChip) chips: QueryList; + @ViewChild(MatLegacyChipList) chipList: MatLegacyChipList; + @ViewChildren(MatLegacyChip) chips: QueryList; } @Component({ @@ -1643,8 +1648,8 @@ class MultiSelectionChipList { tabIndexOverride: number; selectable: boolean; - @ViewChild(MatChipList) chipList: MatChipList; - @ViewChildren(MatChip) chips: QueryList; + @ViewChild(MatLegacyChipList) chipList: MatLegacyChipList; + @ViewChildren(MatLegacyChip) chips: QueryList; } @Component({ @@ -1684,7 +1689,7 @@ class InputChipList { addOnBlur: boolean = true; isRequired: boolean; - add(event: MatChipInputEvent): void { + add(event: MatLegacyChipInputEvent): void { const value = (event.value || '').trim(); // Add our foods @@ -1707,8 +1712,8 @@ class InputChipList { } } - @ViewChild(MatChipList) chipList: MatChipList; - @ViewChildren(MatChip) chips: QueryList; + @ViewChild(MatLegacyChipList) chipList: MatLegacyChipList; + @ViewChildren(MatLegacyChip) chips: QueryList; } @Component({ @@ -1726,7 +1731,7 @@ class FalsyValueChipList { {value: 1, viewValue: 'Pizza'}, ]; control = new FormControl(null); - @ViewChildren(MatChip) chips: QueryList; + @ViewChildren(MatLegacyChip) chips: QueryList; } @Component({ @@ -1744,8 +1749,8 @@ class SelectedChipList { {value: 1, viewValue: 'Pizza', selected: false}, {value: 2, viewValue: 'Pasta', selected: true}, ]; - @ViewChildren(MatChip) chips: QueryList; - @ViewChild(MatChipList, {static: false}) chipList: MatChipList; + @ViewChildren(MatLegacyChip) chips: QueryList; + @ViewChild(MatLegacyChipList, {static: false}) chipList: MatLegacyChipList; } @Component({ @@ -1769,7 +1774,7 @@ class ChipListWithFormErrorMessages { {value: 1, viewValue: 'Pizza', selected: false}, {value: 2, viewValue: 'Pasta', selected: true}, ]; - @ViewChildren(MatChip) chips: QueryList; + @ViewChildren(MatLegacyChip) chips: QueryList; @ViewChild('form') form: NgForm; formControl = new FormControl('', Validators.required); @@ -1815,7 +1820,7 @@ class StandardChipListWithAnimations { class ChipListWithRemove { chips = [0, 1, 2, 3, 4]; - removeChip(event: MatChipEvent) { + removeChip(event: MatLegacyChipEvent) { this.chips.splice(event.chip.value, 1); } } @@ -1848,7 +1853,7 @@ class PreselectedChipInsideOnPush { `, }) class ChipListInsideDynamicFormGroup { - @ViewChild(MatChipList) chipList: MatChipList; + @ViewChild(MatLegacyChipList) chipList: MatLegacyChipList; form: FormGroup; constructor(private _formBuilder: FormBuilder) { diff --git a/src/material/chips/chip-list.ts b/src/material/legacy-chips/chip-list.ts similarity index 95% rename from src/material/chips/chip-list.ts rename to src/material/legacy-chips/chip-list.ts index 64c2991f193f..66b7d5c7b669 100644 --- a/src/material/chips/chip-list.ts +++ b/src/material/legacy-chips/chip-list.ts @@ -39,8 +39,8 @@ import {CanUpdateErrorState, ErrorStateMatcher, mixinErrorState} from '@angular/ import {MatLegacyFormFieldControl} from '@angular/material/legacy-form-field'; import {merge, Observable, Subject, Subscription} from 'rxjs'; import {startWith, takeUntil} from 'rxjs/operators'; -import {MatChip, MatChipEvent, MatChipSelectionChange} from './chip'; -import {MatChipTextControl} from './chip-text-control'; +import {MatLegacyChip, MatLegacyChipEvent, MatLegacyChipSelectionChange} from './chip'; +import {MatLegacyChipTextControl} from './chip-text-control'; // Boilerplate for applying mixins to MatChipList. /** @docs-private */ @@ -71,10 +71,10 @@ const _MatChipListBase = mixinErrorState( let nextUniqueId = 0; /** Change event object that is emitted when the chip list value has changed. */ -export class MatChipListChange { +export class MatLegacyChipListChange { constructor( /** Chip list that emitted the event. */ - public source: MatChipList, + public source: MatLegacyChipList, /** Value of the chip list when the event was emitted. */ public value: any, ) {} @@ -104,12 +104,12 @@ export class MatChipListChange { '(keydown)': '_keydown($event)', '[id]': '_uid', }, - providers: [{provide: MatLegacyFormFieldControl, useExisting: MatChipList}], + providers: [{provide: MatLegacyFormFieldControl, useExisting: MatLegacyChipList}], styleUrls: ['chips.css'], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MatChipList +export class MatLegacyChipList extends _MatChipListBase implements MatLegacyFormFieldControl, @@ -149,7 +149,7 @@ export class MatChipList private _chipRemoveSubscription: Subscription | null; /** The chip input to add more chips */ - protected _chipInput: MatChipTextControl; + protected _chipInput: MatLegacyChipTextControl; /** Uid of the chip list */ _uid: string = `mat-chip-list-${nextUniqueId++}`; @@ -164,7 +164,7 @@ export class MatChipList _userTabIndex: number | null = null; /** The FocusKeyManager which handles focus. */ - _keyManager: FocusKeyManager; + _keyManager: FocusKeyManager; /** Function when touched */ _onTouched = () => {}; @@ -172,10 +172,10 @@ export class MatChipList /** Function when changed */ _onChange: (value: any) => void = () => {}; - _selectionModel: SelectionModel; + _selectionModel: SelectionModel; /** The array of selected chips inside chip list. */ - get selected(): MatChip[] | MatChip { + get selected(): MatLegacyChip[] | MatLegacyChip { return this.multiple ? this._selectionModel?.selected || [] : this._selectionModel?.selected[0]; } @@ -343,27 +343,27 @@ export class MatChipList } /** Combined stream of all of the child chips' selection change events. */ - get chipSelectionChanges(): Observable { + get chipSelectionChanges(): Observable { return merge(...this.chips.map(chip => chip.selectionChange)); } /** Combined stream of all of the child chips' focus change events. */ - get chipFocusChanges(): Observable { + get chipFocusChanges(): Observable { return merge(...this.chips.map(chip => chip._onFocus)); } /** Combined stream of all of the child chips' blur change events. */ - get chipBlurChanges(): Observable { + get chipBlurChanges(): Observable { return merge(...this.chips.map(chip => chip._onBlur)); } /** Combined stream of all of the child chips' remove change events. */ - get chipRemoveChanges(): Observable { + get chipRemoveChanges(): Observable { return merge(...this.chips.map(chip => chip.destroyed)); } /** Event emitted when the selected chip list value has been changed by the user. */ - @Output() readonly change = new EventEmitter(); + @Output() readonly change = new EventEmitter(); /** * Event that emits whenever the raw value of the chip-list changes. This is here primarily @@ -373,12 +373,12 @@ export class MatChipList @Output() readonly valueChange = new EventEmitter(); /** The chips contained within this chip list. */ - @ContentChildren(MatChip, { + @ContentChildren(MatLegacyChip, { // We need to use `descendants: true`, because Ivy will no longer match // indirect descendants if it's left as false. descendants: true, }) - chips: QueryList; + chips: QueryList; constructor( protected _elementRef: ElementRef, @@ -396,7 +396,7 @@ export class MatChipList } ngAfterContentInit() { - this._keyManager = new FocusKeyManager(this.chips) + this._keyManager = new FocusKeyManager(this.chips) .withWrap() .withVerticalOrientation() .withHomeAndEnd() @@ -438,7 +438,7 @@ export class MatChipList } ngOnInit() { - this._selectionModel = new SelectionModel(this.multiple, undefined, false); + this._selectionModel = new SelectionModel(this.multiple, undefined, false); this.stateChanges.next(); } @@ -464,7 +464,7 @@ export class MatChipList } /** Associates an HTML input element with this chip list. */ - registerInput(inputElement: MatChipTextControl): void { + registerInput(inputElement: MatLegacyChipTextControl): void { this._chipInput = inputElement; // We use this attribute to match the chip list to its input in test harnesses. @@ -618,7 +618,7 @@ export class MatChipList * 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 { + private _selectValue(value: any, isUserInput: boolean = true): MatLegacyChip | undefined { const correspondingChip = this.chips.find(chip => { return chip.value != null && this._compareWith(chip.value, value); }); @@ -646,7 +646,7 @@ export class MatChipList * Deselects every chip in the list. * @param skip Chip that should not be deselected. */ - private _clearSelection(skip?: MatChip): void { + private _clearSelection(skip?: MatLegacyChip): void { this._selectionModel.clear(); this.chips.forEach(chip => { if (chip !== skip) { @@ -683,7 +683,7 @@ export class MatChipList valueToEmit = this.selected ? this.selected.value : fallbackValue; } this._value = valueToEmit; - this.change.emit(new MatChipListChange(this, valueToEmit)); + this.change.emit(new MatLegacyChipListChange(this, valueToEmit)); this.valueChange.emit(valueToEmit); this._onChange(valueToEmit); this._changeDetectorRef.markForCheck(); diff --git a/src/material/legacy-chips/chip-remove.spec.ts b/src/material/legacy-chips/chip-remove.spec.ts new file mode 100644 index 000000000000..1d5ba0309ddb --- /dev/null +++ b/src/material/legacy-chips/chip-remove.spec.ts @@ -0,0 +1,105 @@ +import {Component, DebugElement} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatLegacyChip, MatLegacyChipsModule} from './index'; +import {dispatchMouseEvent} from '@angular/cdk/testing/private'; + +describe('Chip Remove', () => { + let fixture: ComponentFixture; + let testChip: TestChip; + let chipDebugElement: DebugElement; + let chipNativeElement: HTMLElement; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [MatLegacyChipsModule], + declarations: [TestChip], + }); + + TestBed.compileComponents(); + })); + + beforeEach(waitForAsync(() => { + fixture = TestBed.createComponent(TestChip); + testChip = fixture.debugElement.componentInstance; + fixture.detectChanges(); + + chipDebugElement = fixture.debugElement.query(By.directive(MatLegacyChip))!; + chipNativeElement = chipDebugElement.nativeElement; + })); + + describe('basic behavior', () => { + it('should apply a CSS class to the remove icon', () => { + const buttonElement = chipNativeElement.querySelector('button')!; + + expect(buttonElement.classList).toContain('mat-chip-remove'); + }); + + it('should ensure that the button cannot submit its parent form', () => { + const buttonElement = chipNativeElement.querySelector('button')!; + + expect(buttonElement.getAttribute('type')).toBe('button'); + }); + + it('should not set the `type` attribute on non-button elements', () => { + const buttonElement = chipNativeElement.querySelector('span.mat-chip-remove')!; + + expect(buttonElement.hasAttribute('type')).toBe(false); + }); + + it('should emit (removed) on click', () => { + const buttonElement = chipNativeElement.querySelector('button')!; + + testChip.removable = true; + fixture.detectChanges(); + + spyOn(testChip, 'didRemove'); + + buttonElement.click(); + fixture.detectChanges(); + + expect(testChip.didRemove).toHaveBeenCalled(); + }); + + it('should not remove if parent chip is disabled', () => { + const buttonElement = chipNativeElement.querySelector('button')!; + + testChip.disabled = true; + testChip.removable = true; + fixture.detectChanges(); + + spyOn(testChip, 'didRemove'); + + buttonElement.click(); + fixture.detectChanges(); + + expect(testChip.didRemove).not.toHaveBeenCalled(); + }); + + it('should prevent the default click action', () => { + const buttonElement = chipNativeElement.querySelector('button')!; + const event = dispatchMouseEvent(buttonElement, 'click'); + fixture.detectChanges(); + + expect(event.defaultPrevented).toBe(true); + }); + }); +}); + +@Component({ + template: ` + + + + + `, +}) +class TestChip { + removable: boolean; + disabled = false; + + didRemove() {} +} diff --git a/src/material-experimental/mdc-chips/chip-text-control.ts b/src/material/legacy-chips/chip-text-control.ts similarity index 79% rename from src/material-experimental/mdc-chips/chip-text-control.ts rename to src/material/legacy-chips/chip-text-control.ts index 422328517e23..727ab361818f 100644 --- a/src/material-experimental/mdc-chips/chip-text-control.ts +++ b/src/material/legacy-chips/chip-text-control.ts @@ -7,7 +7,7 @@ */ /** Interface for a text control that is used to drive interaction with a mat-chip-list. */ -export interface MatChipTextControl { +export interface MatLegacyChipTextControl { /** Unique identifier for the text control. */ id: string; @@ -21,8 +21,5 @@ export interface MatChipTextControl { empty: boolean; /** Focuses the text control. */ - focus(): void; - - /** Sets the list of ids the input is described by. */ - setDescribedByIds(ids: string[]): void; + focus(options?: FocusOptions): void; } diff --git a/src/material/legacy-chips/chip.spec.ts b/src/material/legacy-chips/chip.spec.ts new file mode 100644 index 000000000000..7fe068443850 --- /dev/null +++ b/src/material/legacy-chips/chip.spec.ts @@ -0,0 +1,502 @@ +import {Directionality} from '@angular/cdk/bidi'; +import {BACKSPACE, DELETE, SPACE} from '@angular/cdk/keycodes'; +import {createKeyboardEvent, dispatchFakeEvent} from '../../cdk/testing/private'; +import {Component, DebugElement, ViewChild} from '@angular/core'; +import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; +import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core'; +import {By} from '@angular/platform-browser'; +import {Subject} from 'rxjs'; +import { + MatLegacyChip, + MatLegacyChipEvent, + MatLegacyChipSelectionChange, + MatLegacyChipsModule, + MatLegacyChipList, +} from './index'; + +describe('MatChip', () => { + let fixture: ComponentFixture; + let chipDebugElement: DebugElement; + let chipNativeElement: HTMLElement; + let chipInstance: MatLegacyChip; + let globalRippleOptions: RippleGlobalOptions; + let dir = 'ltr'; + + beforeEach(waitForAsync(() => { + globalRippleOptions = {}; + TestBed.configureTestingModule({ + imports: [MatLegacyChipsModule], + declarations: [ + BasicChip, + SingleChip, + BasicChipWithStaticTabindex, + BasicChipWithBoundTabindex, + ], + providers: [ + {provide: MAT_RIPPLE_GLOBAL_OPTIONS, useFactory: () => globalRippleOptions}, + { + provide: Directionality, + useFactory: () => ({ + value: dir, + change: new Subject(), + }), + }, + ], + }); + + TestBed.compileComponents(); + })); + + describe('MatBasicChip', () => { + it('adds a class to indicate that it is a basic chip', () => { + fixture = TestBed.createComponent(BasicChip); + fixture.detectChanges(); + + const chip = fixture.nativeElement.querySelector('mat-basic-chip'); + expect(chip.classList).toContain('mat-chip'); + expect(chip.classList).toContain('mat-basic-chip'); + }); + + it('should be able to set a static tabindex', () => { + fixture = TestBed.createComponent(BasicChipWithStaticTabindex); + fixture.detectChanges(); + + const chip = fixture.nativeElement.querySelector('mat-basic-chip'); + expect(chip.getAttribute('tabindex')).toBe('3'); + }); + + it('should be able to set a dynamic tabindex', () => { + fixture = TestBed.createComponent(BasicChipWithBoundTabindex); + fixture.detectChanges(); + + const chip = fixture.nativeElement.querySelector('mat-basic-chip'); + expect(chip.getAttribute('tabindex')).toBe('12'); + + fixture.componentInstance.tabindex = 15; + fixture.detectChanges(); + + expect(chip.getAttribute('tabindex')).toBe('15'); + }); + + it('should have the correct role', () => { + fixture = TestBed.createComponent(BasicChip); + fixture.detectChanges(); + chipDebugElement = fixture.debugElement.query(By.directive(MatLegacyChip))!; + chipNativeElement = chipDebugElement.nativeElement; + + expect(chipNativeElement.getAttribute('role')).toBe('option'); + }); + + it('should be able to set a custom role', () => { + fixture = TestBed.createComponent(BasicChip); + fixture.detectChanges(); + chipDebugElement = fixture.debugElement.query(By.directive(MatLegacyChip))!; + chipInstance = chipDebugElement.injector.get(MatLegacyChip); + chipNativeElement = chipDebugElement.nativeElement; + + chipInstance.role = 'gridcell'; + fixture.detectChanges(); + + expect(chipNativeElement.getAttribute('role')).toBe('gridcell'); + }); + }); + + describe('MatChip', () => { + let testComponent: SingleChip; + + beforeEach(() => { + fixture = TestBed.createComponent(SingleChip); + fixture.detectChanges(); + + chipDebugElement = fixture.debugElement.query(By.directive(MatLegacyChip))!; + chipNativeElement = chipDebugElement.nativeElement; + chipInstance = chipDebugElement.injector.get(MatLegacyChip); + testComponent = fixture.debugElement.componentInstance; + }); + + describe('basic behaviors', () => { + it('adds the `mat-chip` class', () => { + expect(chipNativeElement.classList).toContain('mat-chip'); + }); + + it('does not add the `mat-basic-chip` class', () => { + expect(chipNativeElement.classList).not.toContain('mat-basic-chip'); + }); + + it('emits focus only once for multiple clicks', () => { + let counter = 0; + chipInstance._onFocus.subscribe(() => { + counter++; + }); + + chipNativeElement.focus(); + chipNativeElement.focus(); + fixture.detectChanges(); + + expect(counter).toBe(1); + }); + + it('emits destroy on destruction', () => { + spyOn(testComponent, 'chipDestroy').and.callThrough(); + + // Force a destroy callback + testComponent.shouldShow = false; + fixture.detectChanges(); + + expect(testComponent.chipDestroy).toHaveBeenCalledTimes(1); + }); + + it('allows color customization', () => { + expect(chipNativeElement.classList).toContain('mat-primary'); + + testComponent.color = 'warn'; + fixture.detectChanges(); + + expect(chipNativeElement.classList).not.toContain('mat-primary'); + expect(chipNativeElement.classList).toContain('mat-warn'); + }); + + it('allows selection', () => { + spyOn(testComponent, 'chipSelectionChange'); + expect(chipNativeElement.classList).not.toContain('mat-chip-selected'); + + testComponent.selected = true; + fixture.detectChanges(); + + expect(chipNativeElement.classList).toContain('mat-chip-selected'); + expect(testComponent.chipSelectionChange).toHaveBeenCalledWith({ + source: chipInstance, + isUserInput: false, + selected: true, + }); + }); + + it('allows removal', () => { + spyOn(testComponent, 'chipRemove'); + + chipInstance.remove(); + fixture.detectChanges(); + + expect(testComponent.chipRemove).toHaveBeenCalledWith({chip: chipInstance}); + }); + + it('should not prevent the default click action', () => { + const event = dispatchFakeEvent(chipNativeElement, 'click'); + fixture.detectChanges(); + + expect(event.defaultPrevented).toBe(false); + }); + + it('should prevent the default click action when the chip is disabled', () => { + chipInstance.disabled = true; + fixture.detectChanges(); + + const event = dispatchFakeEvent(chipNativeElement, 'click'); + fixture.detectChanges(); + + expect(event.defaultPrevented).toBe(true); + }); + + it('should not dispatch `selectionChange` event when deselecting a non-selected chip', () => { + chipInstance.deselect(); + + const spy = jasmine.createSpy('selectionChange spy'); + const subscription = chipInstance.selectionChange.subscribe(spy); + + chipInstance.deselect(); + + expect(spy).not.toHaveBeenCalled(); + subscription.unsubscribe(); + }); + + it('should not dispatch `selectionChange` event when selecting a selected chip', () => { + chipInstance.select(); + + const spy = jasmine.createSpy('selectionChange spy'); + const subscription = chipInstance.selectionChange.subscribe(spy); + + chipInstance.select(); + + expect(spy).not.toHaveBeenCalled(); + subscription.unsubscribe(); + }); + + it( + 'should not dispatch `selectionChange` event when selecting a selected chip via ' + + 'user interaction', + () => { + chipInstance.select(); + + const spy = jasmine.createSpy('selectionChange spy'); + const subscription = chipInstance.selectionChange.subscribe(spy); + + chipInstance.selectViaInteraction(); + + expect(spy).not.toHaveBeenCalled(); + subscription.unsubscribe(); + }, + ); + + it('should not dispatch `selectionChange` through setter if the value did not change', () => { + chipInstance.selected = false; + + const spy = jasmine.createSpy('selectionChange spy'); + const subscription = chipInstance.selectionChange.subscribe(spy); + + chipInstance.selected = false; + + expect(spy).not.toHaveBeenCalled(); + subscription.unsubscribe(); + }); + + it('should be able to disable ripples through ripple global options at runtime', () => { + expect(chipInstance.rippleDisabled) + .withContext('Expected chip ripples to be enabled.') + .toBe(false); + + globalRippleOptions.disabled = true; + + expect(chipInstance.rippleDisabled) + .withContext('Expected chip ripples to be disabled.') + .toBe(true); + }); + + it('should return the chip text if value is undefined', () => { + expect(chipInstance.value.trim()).toBe(fixture.componentInstance.name); + }); + + it('should return the chip value if defined', () => { + fixture.componentInstance.value = 123; + fixture.detectChanges(); + + expect(chipInstance.value).toBe(123); + }); + + it('should return the chip value if set to null', () => { + fixture.componentInstance.value = null; + fixture.detectChanges(); + + expect(chipInstance.value).toBeNull(); + }); + }); + + describe('keyboard behavior', () => { + describe('when selectable is true', () => { + beforeEach(() => { + testComponent.selectable = true; + fixture.detectChanges(); + }); + + it('should selects/deselects the currently focused chip on SPACE', () => { + const SPACE_EVENT = createKeyboardEvent('keydown', SPACE); + const CHIP_SELECTED_EVENT: MatLegacyChipSelectionChange = { + source: chipInstance, + isUserInput: true, + selected: true, + }; + + const CHIP_DESELECTED_EVENT: MatLegacyChipSelectionChange = { + source: chipInstance, + isUserInput: true, + selected: false, + }; + + spyOn(testComponent, 'chipSelectionChange'); + + // Use the spacebar to select the chip + chipInstance._handleKeydown(SPACE_EVENT); + fixture.detectChanges(); + + expect(chipInstance.selected).toBeTruthy(); + expect(testComponent.chipSelectionChange).toHaveBeenCalledTimes(1); + expect(testComponent.chipSelectionChange).toHaveBeenCalledWith(CHIP_SELECTED_EVENT); + + // Use the spacebar to deselect the chip + chipInstance._handleKeydown(SPACE_EVENT); + fixture.detectChanges(); + + expect(chipInstance.selected).toBeFalsy(); + expect(testComponent.chipSelectionChange).toHaveBeenCalledTimes(2); + expect(testComponent.chipSelectionChange).toHaveBeenCalledWith(CHIP_DESELECTED_EVENT); + }); + + it('should have correct aria-selected in single selection mode', () => { + expect(chipNativeElement.hasAttribute('aria-selected')).toBe(false); + + testComponent.selected = true; + fixture.detectChanges(); + + expect(chipNativeElement.getAttribute('aria-selected')).toBe('true'); + }); + + it('should have the correct aria-selected in multi-selection mode', () => { + testComponent.chipList.multiple = true; + fixture.detectChanges(); + + expect(chipNativeElement.getAttribute('aria-selected')).toBe('false'); + + testComponent.selected = true; + fixture.detectChanges(); + + expect(chipNativeElement.getAttribute('aria-selected')).toBe('true'); + }); + }); + + describe('when selectable is false', () => { + beforeEach(() => { + testComponent.selectable = false; + fixture.detectChanges(); + }); + + it('SPACE ignores selection', () => { + const SPACE_EVENT = createKeyboardEvent('keydown', SPACE); + + spyOn(testComponent, 'chipSelectionChange'); + + // Use the spacebar to attempt to select the chip + chipInstance._handleKeydown(SPACE_EVENT); + fixture.detectChanges(); + + expect(chipInstance.selected).toBeFalsy(); + expect(testComponent.chipSelectionChange).not.toHaveBeenCalled(); + }); + + it('should not have the aria-selected attribute', () => { + expect(chipNativeElement.hasAttribute('aria-selected')).toBe(false); + }); + }); + + describe('when removable is true', () => { + beforeEach(() => { + testComponent.removable = true; + fixture.detectChanges(); + }); + + it('DELETE emits the (removed) event', () => { + const DELETE_EVENT = createKeyboardEvent('keydown', DELETE); + + spyOn(testComponent, 'chipRemove'); + + // Use the delete to remove the chip + chipInstance._handleKeydown(DELETE_EVENT); + fixture.detectChanges(); + + expect(testComponent.chipRemove).toHaveBeenCalled(); + }); + + it('BACKSPACE emits the (removed) event', () => { + const BACKSPACE_EVENT = createKeyboardEvent('keydown', BACKSPACE); + + spyOn(testComponent, 'chipRemove'); + + // Use the delete to remove the chip + chipInstance._handleKeydown(BACKSPACE_EVENT); + fixture.detectChanges(); + + expect(testComponent.chipRemove).toHaveBeenCalled(); + }); + }); + + describe('when removable is false', () => { + beforeEach(() => { + testComponent.removable = false; + fixture.detectChanges(); + }); + + it('DELETE does not emit the (removed) event', () => { + const DELETE_EVENT = createKeyboardEvent('keydown', DELETE); + + spyOn(testComponent, 'chipRemove'); + + // Use the delete to remove the chip + chipInstance._handleKeydown(DELETE_EVENT); + fixture.detectChanges(); + + expect(testComponent.chipRemove).not.toHaveBeenCalled(); + }); + + it('BACKSPACE does not emit the (removed) event', () => { + const BACKSPACE_EVENT = createKeyboardEvent('keydown', BACKSPACE); + + spyOn(testComponent, 'chipRemove'); + + // Use the delete to remove the chip + chipInstance._handleKeydown(BACKSPACE_EVENT); + fixture.detectChanges(); + + expect(testComponent.chipRemove).not.toHaveBeenCalled(); + }); + }); + + it('should update the aria-label for disabled chips', () => { + expect(chipNativeElement.getAttribute('aria-disabled')).toBe('false'); + + testComponent.disabled = true; + fixture.detectChanges(); + + expect(chipNativeElement.getAttribute('aria-disabled')).toBe('true'); + }); + + it('should make disabled chips non-focusable', () => { + expect(chipNativeElement.getAttribute('tabindex')).toBe('-1'); + + testComponent.disabled = true; + fixture.detectChanges(); + + expect(chipNativeElement.getAttribute('tabindex')).toBeFalsy(); + }); + }); + + it('should have a focus indicator', () => { + expect(chipNativeElement.classList.contains('mat-focus-indicator')).toBe(true); + }); + }); +}); + +@Component({ + template: ` + +
+ + {{name}} + +
+
`, +}) +class SingleChip { + @ViewChild(MatLegacyChipList) chipList: MatLegacyChipList; + disabled: boolean = false; + name: string = 'Test'; + color: string = 'primary'; + selected: boolean = false; + selectable: boolean = true; + removable: boolean = true; + shouldShow: boolean = true; + value: any; + + chipFocus: (event?: MatLegacyChipEvent) => void = () => {}; + chipDestroy: (event?: MatLegacyChipEvent) => void = () => {}; + chipSelectionChange: (event?: MatLegacyChipSelectionChange) => void = () => {}; + chipRemove: (event?: MatLegacyChipEvent) => void = () => {}; +} + +@Component({ + template: `Hello`, +}) +class BasicChip {} + +@Component({ + template: `Hello`, +}) +class BasicChipWithStaticTabindex {} + +@Component({ + template: `Hello`, +}) +class BasicChipWithBoundTabindex { + tabindex = 12; +} diff --git a/src/material/legacy-chips/chip.ts b/src/material/legacy-chips/chip.ts new file mode 100644 index 000000000000..4260f0f73e0a --- /dev/null +++ b/src/material/legacy-chips/chip.ts @@ -0,0 +1,505 @@ +/** + * @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 {FocusableOption} from '@angular/cdk/a11y'; +import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; +import {BACKSPACE, DELETE, SPACE} from '@angular/cdk/keycodes'; +import {Platform} from '@angular/cdk/platform'; +import {DOCUMENT} from '@angular/common'; +import { + Attribute, + ChangeDetectorRef, + ContentChild, + Directive, + ElementRef, + EventEmitter, + Inject, + InjectionToken, + Input, + NgZone, + OnDestroy, + Optional, + Output, +} from '@angular/core'; +import { + CanColor, + CanDisable, + CanDisableRipple, + HasTabIndex, + MAT_RIPPLE_GLOBAL_OPTIONS, + mixinColor, + mixinDisableRipple, + mixinTabIndex, + RippleConfig, + RippleGlobalOptions, + RippleRenderer, + RippleTarget, +} from '@angular/material/core'; +import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; +import {Subject} from 'rxjs'; +import {take} from 'rxjs/operators'; + +/** Represents an event fired on an individual `mat-chip`. */ +export interface MatLegacyChipEvent { + /** The chip the event was fired on. */ + chip: MatLegacyChip; +} + +/** Event object emitted by MatChip when selected or deselected. */ +export class MatLegacyChipSelectionChange { + constructor( + /** Reference to the chip that emitted the event. */ + public source: MatLegacyChip, + /** 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, + ) {} +} + +/** + * Injection token that can be used to reference instances of `MatChipRemove`. It serves as + * alternative token to the actual `MatChipRemove` class which could cause unnecessary + * retention of the class and its directive metadata. + */ +export const MAT_CHIP_REMOVE = new InjectionToken('MatChipRemove'); + +/** + * Injection token that can be used to reference instances of `MatChipAvatar`. It serves as + * alternative token to the actual `MatChipAvatar` class which could cause unnecessary + * retention of the class and its directive metadata. + */ +export const MAT_CHIP_AVATAR = new InjectionToken('MatChipAvatar'); + +/** + * Injection token that can be used to reference instances of `MatChipTrailingIcon`. It serves as + * alternative token to the actual `MatChipTrailingIcon` class which could cause unnecessary + * retention of the class and its directive metadata. + */ +export const MAT_CHIP_TRAILING_ICON = new InjectionToken( + 'MatChipTrailingIcon', +); + +// Boilerplate for applying mixins to MatChip. +/** @docs-private */ +abstract class MatChipBase { + abstract disabled: boolean; + constructor(public _elementRef: ElementRef) {} +} + +const _MatChipMixinBase = mixinTabIndex(mixinColor(mixinDisableRipple(MatChipBase), 'primary'), -1); + +/** + * Dummy directive to add CSS class to chip avatar. + * @docs-private + */ +@Directive({ + selector: 'mat-chip-avatar, [matChipAvatar]', + host: {'class': 'mat-chip-avatar'}, + providers: [{provide: MAT_CHIP_AVATAR, useExisting: MatLegacyChipAvatar}], +}) +export class MatLegacyChipAvatar {} + +/** + * Dummy directive to add CSS class to chip trailing icon. + * @docs-private + */ +@Directive({ + selector: 'mat-chip-trailing-icon, [matChipTrailingIcon]', + host: {'class': 'mat-chip-trailing-icon'}, + providers: [{provide: MAT_CHIP_TRAILING_ICON, useExisting: MatLegacyChipTrailingIcon}], +}) +export class MatLegacyChipTrailingIcon {} + +/** Material Design styled chip directive. Used inside the MatChipList component. */ +@Directive({ + selector: `mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]`, + inputs: ['color', 'disableRipple', 'tabIndex'], + exportAs: 'matChip', + host: { + 'class': 'mat-chip mat-focus-indicator', + '[attr.tabindex]': 'disabled ? null : tabIndex', + '[attr.role]': 'role', + '[class.mat-chip-selected]': 'selected', + '[class.mat-chip-with-avatar]': 'avatar', + '[class.mat-chip-with-trailing-icon]': 'trailingIcon || removeIcon', + '[class.mat-chip-disabled]': 'disabled', + '[class._mat-animation-noopable]': '_animationsDisabled', + '[attr.disabled]': 'disabled || null', + '[attr.aria-disabled]': 'disabled.toString()', + '[attr.aria-selected]': 'ariaSelected', + '(click)': '_handleClick($event)', + '(keydown)': '_handleKeydown($event)', + '(focus)': 'focus()', + '(blur)': '_blur()', + }, +}) +export class MatLegacyChip + extends _MatChipMixinBase + implements + FocusableOption, + OnDestroy, + CanColor, + CanDisableRipple, + RippleTarget, + HasTabIndex, + CanDisable +{ + /** Reference to the RippleRenderer for the chip. */ + private _chipRipple: RippleRenderer; + + /** + * Reference to the element that acts as the chip's ripple target. This element is + * dynamically added as a child node of the chip. The chip itself cannot be used as the + * ripple target because it must be the host of the focus indicator. + */ + private _chipRippleTarget: HTMLElement; + + /** + * Ripple configuration for ripples that are launched on pointer down. The ripple config + * is set to the global ripple options since we don't have any configurable options for + * the chip ripples. + * @docs-private + */ + rippleConfig: RippleConfig & RippleGlobalOptions; + + /** + * Whether ripples are disabled on interaction + * @docs-private + */ + get rippleDisabled(): boolean { + return ( + this.disabled || + this.disableRipple || + this._animationsDisabled || + !!this.rippleConfig.disabled + ); + } + + /** Whether the chip has focus. */ + _hasFocus: boolean = false; + + /** Whether animations for the chip are enabled. */ + _animationsDisabled: boolean; + + /** Whether the chip list is selectable */ + chipListSelectable: boolean = true; + + /** Whether the chip list is in multi-selection mode. */ + _chipListMultiple: boolean = false; + + /** Whether the chip list as a whole is disabled. */ + _chipListDisabled: boolean = false; + + /** The chip avatar */ + @ContentChild(MAT_CHIP_AVATAR) avatar: MatLegacyChipAvatar; + + /** The chip's trailing icon. */ + @ContentChild(MAT_CHIP_TRAILING_ICON) trailingIcon: MatLegacyChipTrailingIcon; + + /** The chip's remove toggler. */ + @ContentChild(MAT_CHIP_REMOVE) removeIcon: MatLegacyChipRemove; + + /** ARIA role that should be applied to the chip. */ + @Input() role: string = 'option'; + + /** Whether the chip is selected. */ + @Input() + get selected(): boolean { + return this._selected; + } + set selected(value: BooleanInput) { + const coercedValue = coerceBooleanProperty(value); + + if (coercedValue !== this._selected) { + this._selected = coercedValue; + this._dispatchSelectionChange(); + } + } + protected _selected: boolean = false; + + /** 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; + + /** + * Whether or not the chip is selectable. When a chip is not selectable, + * changes to its selected state are always ignored. By default a 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: BooleanInput) { + this._selectable = coerceBooleanProperty(value); + } + protected _selectable: boolean = true; + + /** Whether the chip is disabled. */ + @Input() + get disabled(): boolean { + return this._chipListDisabled || this._disabled; + } + set disabled(value: BooleanInput) { + this._disabled = coerceBooleanProperty(value); + } + protected _disabled: boolean = false; + + /** + * Determines whether or not the chip displays the remove styling and emits (removed) events. + */ + @Input() + get removable(): boolean { + return this._removable; + } + set removable(value: BooleanInput) { + this._removable = coerceBooleanProperty(value); + } + protected _removable: boolean = true; + + /** Emits when the chip is focused. */ + readonly _onFocus = new Subject(); + + /** Emits when the chip is blurred. */ + readonly _onBlur = new Subject(); + + /** Emitted when the chip is selected or deselected. */ + @Output() readonly selectionChange: EventEmitter = + 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 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; + } + + constructor( + elementRef: ElementRef, + private _ngZone: NgZone, + platform: Platform, + @Optional() + @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) + globalRippleOptions: RippleGlobalOptions | null, + private _changeDetectorRef: ChangeDetectorRef, + @Inject(DOCUMENT) _document: any, + @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string, + @Attribute('tabindex') tabIndex?: string, + ) { + super(elementRef); + + this._addHostClassName(); + + // Dynamically create the ripple target, append it within the chip, and use it as the + // chip's ripple target. Adding the class '.mat-chip-ripple' ensures that it will have + // the proper styles. + this._chipRippleTarget = _document.createElement('div'); + this._chipRippleTarget.classList.add('mat-chip-ripple'); + this._elementRef.nativeElement.appendChild(this._chipRippleTarget); + this._chipRipple = new RippleRenderer(this, _ngZone, this._chipRippleTarget, platform); + this._chipRipple.setupTriggerEvents(elementRef); + + this.rippleConfig = globalRippleOptions || {}; + this._animationsDisabled = animationMode === 'NoopAnimations'; + this.tabIndex = tabIndex != null ? parseInt(tabIndex) || -1 : -1; + } + + _addHostClassName() { + const basicChipAttrName = 'mat-basic-chip'; + const element = this._elementRef.nativeElement as HTMLElement; + + if ( + element.hasAttribute(basicChipAttrName) || + element.tagName.toLowerCase() === basicChipAttrName + ) { + element.classList.add(basicChipAttrName); + return; + } else { + element.classList.add('mat-standard-chip'); + } + } + + ngOnDestroy() { + this.destroyed.emit({chip: this}); + this._chipRipple._removeTriggerEvents(); + } + + /** Selects the chip. */ + select(): void { + if (!this._selected) { + this._selected = true; + this._dispatchSelectionChange(); + this._changeDetectorRef.markForCheck(); + } + } + + /** Deselects the chip. */ + deselect(): void { + if (this._selected) { + this._selected = false; + this._dispatchSelectionChange(); + this._changeDetectorRef.markForCheck(); + } + } + + /** Select this chip and emit selected event */ + selectViaInteraction(): void { + if (!this._selected) { + this._selected = true; + this._dispatchSelectionChange(true); + this._changeDetectorRef.markForCheck(); + } + } + + /** Toggles the current selected state of this chip. */ + toggleSelected(isUserInput: boolean = false): boolean { + this._selected = !this.selected; + this._dispatchSelectionChange(isUserInput); + this._changeDetectorRef.markForCheck(); + return this.selected; + } + + /** Allows for programmatic focusing of the chip. */ + focus(): void { + if (!this._hasFocus) { + this._elementRef.nativeElement.focus(); + this._onFocus.next({chip: this}); + } + this._hasFocus = true; + } + + /** + * Allows for programmatic removal of the chip. Called by the MatChipList 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.removed.emit({chip: this}); + } + } + + /** Handles click events on the chip. */ + _handleClick(event: Event) { + if (this.disabled) { + event.preventDefault(); + } + } + + /** Handle custom key presses. */ + _handleKeydown(event: KeyboardEvent): void { + if (this.disabled) { + return; + } + + switch (event.keyCode) { + case DELETE: + case BACKSPACE: + // If we are removable, remove the focused chip + this.remove(); + // Always prevent so page navigation does not occur + event.preventDefault(); + break; + case SPACE: + // If we are selectable, toggle the focused chip + if (this.selectable) { + this.toggleSelected(true); + } + + // Always prevent space from scrolling the page since the list has focus + event.preventDefault(); + break; + } + } + + _blur(): void { + // When animations are enabled, Angular may end up removing the chip from the DOM a little + // earlier than usual, causing it to be blurred and throwing off the logic in the chip list + // that moves focus not the next item. To work around the issue, we defer marking the chip + // as not focused until the next time the zone stabilizes. + this._ngZone.onStable.pipe(take(1)).subscribe(() => { + this._ngZone.run(() => { + this._hasFocus = false; + this._onBlur.next({chip: this}); + }); + }); + } + + private _dispatchSelectionChange(isUserInput = false) { + this.selectionChange.emit({ + source: this, + isUserInput, + selected: this._selected, + }); + } +} + +/** + * Applies proper (click) support and adds styling for use with the Material Design "cancel" icon + * available at https://material.io/icons/#ic_cancel. + * + * Example: + * + * ` + * cancel + * ` + * + * You *may* use a custom icon, but you may need to override the `mat-chip-remove` positioning + * styles to properly center the icon within the chip. + */ +@Directive({ + selector: '[matChipRemove]', + host: { + 'class': 'mat-chip-remove mat-chip-trailing-icon', + '(click)': '_handleClick($event)', + }, + providers: [{provide: MAT_CHIP_REMOVE, useExisting: MatLegacyChipRemove}], +}) +export class MatLegacyChipRemove { + constructor(protected _parentChip: MatLegacyChip, elementRef: ElementRef) { + if (elementRef.nativeElement.nodeName === 'BUTTON') { + elementRef.nativeElement.setAttribute('type', 'button'); + } + } + + /** Calls the parent chip's public `remove()` method if applicable. */ + _handleClick(event: Event): void { + const parentChip = this._parentChip; + + if (parentChip.removable && !parentChip.disabled) { + parentChip.remove(); + } + + // We need to stop event propagation because otherwise the event will bubble up to the + // form field and cause the `onContainerClick` method to be invoked. This method would then + // reset the focused chip that has been focused after chip removal. Usually the parent + // the parent click listener of the `MatChip` would prevent propagation, but it can happen + // that the chip is being removed before the event bubbles up. + event.stopPropagation(); + event.preventDefault(); + } +} diff --git a/src/material/chips/chips-module.ts b/src/material/legacy-chips/chips-module.ts similarity index 55% rename from src/material/chips/chips-module.ts rename to src/material/legacy-chips/chips-module.ts index d2e1c0204609..0f79b1203341 100644 --- a/src/material/chips/chips-module.ts +++ b/src/material/legacy-chips/chips-module.ts @@ -9,18 +9,23 @@ import {ENTER} from '@angular/cdk/keycodes'; import {NgModule} from '@angular/core'; import {ErrorStateMatcher, MatCommonModule} from '@angular/material/core'; -import {MatChip, MatChipAvatar, MatChipRemove, MatChipTrailingIcon} from './chip'; -import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './chip-default-options'; -import {MatChipInput} from './chip-input'; -import {MatChipList} from './chip-list'; +import { + MatLegacyChip, + MatLegacyChipAvatar, + MatLegacyChipRemove, + MatLegacyChipTrailingIcon, +} from './chip'; +import {MAT_CHIPS_DEFAULT_OPTIONS, MatLegacyChipsDefaultOptions} from './chip-default-options'; +import {MatLegacyChipInput} from './chip-input'; +import {MatLegacyChipList} from './chip-list'; const CHIP_DECLARATIONS = [ - MatChipList, - MatChip, - MatChipInput, - MatChipRemove, - MatChipAvatar, - MatChipTrailingIcon, + MatLegacyChipList, + MatLegacyChip, + MatLegacyChipInput, + MatLegacyChipRemove, + MatLegacyChipAvatar, + MatLegacyChipTrailingIcon, ]; @NgModule({ @@ -33,8 +38,8 @@ const CHIP_DECLARATIONS = [ provide: MAT_CHIPS_DEFAULT_OPTIONS, useValue: { separatorKeyCodes: [ENTER], - } as MatChipsDefaultOptions, + } as MatLegacyChipsDefaultOptions, }, ], }) -export class MatChipsModule {} +export class MatLegacyChipsModule {} diff --git a/src/material/legacy-chips/chips.md b/src/material/legacy-chips/chips.md new file mode 100644 index 000000000000..78c176948300 --- /dev/null +++ b/src/material/legacy-chips/chips.md @@ -0,0 +1,82 @@ +`` displays a list of values as individual, keyboard accessible, chips. + + + +### Unstyled chips +By default, `` has Material Design styles applied. For a chip with no styles applied, +use ``. You can then customize the chip appearance by adding your own CSS. + +_Hint: `` receives the `mat-basic-chip` CSS class in addition to the `mat-chip` class._ + +### Selection +Chips can be selected via the `selected` property. Selection can be disabled by setting +`selectable` to `false` on the ``. + +Whenever the selection state changes, a `ChipSelectionChange` event will be emitted via +`(selectionChange)`. + +### Disabled chips +Individual chips may be disabled by applying the `disabled` attribute to the chip. When disabled, +chips are neither selectable nor focusable. + +### Chip input +The `MatChipInput` directive can be used together with a chip-list to streamline the interaction +between the two components. This directive adds chip-specific behaviors to the input element +within `` for adding and removing chips. The `` with `MatChipInput` can +be placed inside or outside the chip-list element. + +An example of chip input placed inside the chip-list element. + + +An example of chip input placed outside the chip-list element. + +```html + + + Chip 1 + Chip 2 + + + +``` + +An example of chip input with an autocomplete placed inside the chip-list element. + + +### Keyboard interaction +Users can move through the chips using the arrow keys and select/deselect them with the space. Chips +also gain focus when clicked, ensuring keyboard navigation starts at the appropriate chip. + +### Orientation +If you want the chips in the list to be stacked vertically, instead of horizontally, you can apply +the `mat-chip-list-stacked` class, as well as the `aria-orientation="vertical"` attribute: + + + +### Specifying global configuration defaults +Default options for the chips module can be specified using the `MAT_CHIPS_DEFAULT_OPTIONS` +injection token. + +```ts +@NgModule({ + providers: [ + { + provide: MAT_CHIPS_DEFAULT_OPTIONS, + useValue: { + separatorKeyCodes: [ENTER, COMMA] + } + } + ] +}) +``` + +### Theming +The selected color of an `` can be changed by using the `color` property. By default, chips +use a neutral background color based on the current theme (light or dark). This can be changed to +`'primary'`, `'accent'`, or `'warn'`. + +### Accessibility +A chip-list behaves as a `role="listbox"`, with each chip being a `role="option"`. The chip input +should have a placeholder or be given a meaningful label via `aria-label` or `aria-labelledby`. diff --git a/src/material/chips/chips.scss b/src/material/legacy-chips/chips.scss similarity index 100% rename from src/material/chips/chips.scss rename to src/material/legacy-chips/chips.scss diff --git a/src/material-experimental/mdc-chips/index.ts b/src/material/legacy-chips/index.ts similarity index 100% rename from src/material-experimental/mdc-chips/index.ts rename to src/material/legacy-chips/index.ts diff --git a/src/material/legacy-chips/public-api.ts b/src/material/legacy-chips/public-api.ts new file mode 100644 index 000000000000..fd202e54df03 --- /dev/null +++ b/src/material/legacy-chips/public-api.ts @@ -0,0 +1,13 @@ +/** + * @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 + */ + +export * from './chips-module'; +export * from './chip-list'; +export * from './chip'; +export * from './chip-input'; +export * from './chip-default-options'; diff --git a/src/material-experimental/mdc-chips/testing/BUILD.bazel b/src/material/legacy-chips/testing/BUILD.bazel similarity index 50% rename from src/material-experimental/mdc-chips/testing/BUILD.bazel rename to src/material/legacy-chips/testing/BUILD.bazel index 87447da9a10a..d2b40c13eda2 100644 --- a/src/material-experimental/mdc-chips/testing/BUILD.bazel +++ b/src/material/legacy-chips/testing/BUILD.bazel @@ -9,28 +9,47 @@ ts_library( exclude = ["**/*.spec.ts"], ), deps = [ - "//src/cdk/coercion", "//src/cdk/testing", ], ) +filegroup( + name = "source-files", + srcs = glob(["**/*.ts"]), +) + ng_test_library( - name = "unit_tests_lib", - srcs = glob(["**/*.spec.ts"]), + name = "harness_tests_lib", + srcs = ["shared.spec.ts"], deps = [ ":testing", "//src/cdk/testing", + "//src/cdk/testing/private", "//src/cdk/testing/testbed", - "//src/material-experimental/mdc-chips", "//src/material/icon", "//src/material/icon/testing", - "@npm//@angular/forms", + "//src/material/legacy-chips", + "//src/material/legacy-form-field", + "@npm//@angular/platform-browser", ], ) -ng_web_test_suite( - name = "unit_tests", +ng_test_library( + name = "unit_tests_lib", + srcs = glob( + ["**/*.spec.ts"], + exclude = ["shared.spec.ts"], + ), deps = [ - ":unit_tests_lib", + ":harness_tests_lib", + ":testing", + "//src/material/icon", + "//src/material/icon/testing", + "//src/material/legacy-chips", ], ) + +ng_web_test_suite( + name = "unit_tests", + deps = [":unit_tests_lib"], +) diff --git a/src/material-experimental/mdc-chips/testing/chip-avatar-harness.ts b/src/material/legacy-chips/testing/chip-avatar-harness.ts similarity index 59% rename from src/material-experimental/mdc-chips/testing/chip-avatar-harness.ts rename to src/material/legacy-chips/testing/chip-avatar-harness.ts index 469036f7b4d1..bc6508b9d9c0 100644 --- a/src/material-experimental/mdc-chips/testing/chip-avatar-harness.ts +++ b/src/material/legacy-chips/testing/chip-avatar-harness.ts @@ -6,27 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ -import { - ComponentHarness, - ComponentHarnessConstructor, - HarnessPredicate, -} from '@angular/cdk/testing'; +import {HarnessPredicate, ComponentHarness} from '@angular/cdk/testing'; import {ChipAvatarHarnessFilters} from './chip-harness-filters'; /** Harness for interacting with a standard Material chip avatar in tests. */ -export class MatChipAvatarHarness extends ComponentHarness { - static hostSelector = '.mat-mdc-chip-avatar'; +export class MatLegacyChipAvatarHarness extends ComponentHarness { + static hostSelector = '.mat-chip-avatar'; /** - * Gets a `HarnessPredicate` that can be used to search for a chip avatar with specific - * attributes. + * Gets a `HarnessPredicate` that can be used to search for a `MatChipAvatarHarness` that meets + * certain criteria. * @param options Options for filtering which input instances are considered a match. * @return a `HarnessPredicate` configured with the given options. */ - static with( - this: ComponentHarnessConstructor, + static with( options: ChipAvatarHarnessFilters = {}, - ): HarnessPredicate { - return new HarnessPredicate(this, options); + ): HarnessPredicate { + return new HarnessPredicate(MatLegacyChipAvatarHarness, options); } } diff --git a/src/material-experimental/mdc-chips/testing/chip-harness-filters.ts b/src/material/legacy-chips/testing/chip-harness-filters.ts similarity index 54% rename from src/material-experimental/mdc-chips/testing/chip-harness-filters.ts rename to src/material/legacy-chips/testing/chip-harness-filters.ts index ec82bf202a0c..c3926451b4ad 100644 --- a/src/material-experimental/mdc-chips/testing/chip-harness-filters.ts +++ b/src/material/legacy-chips/testing/chip-harness-filters.ts @@ -5,34 +5,42 @@ * 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 {BaseHarnessFilters} from '@angular/cdk/testing'; +/** A set of criteria that can be used to filter a list of chip instances. */ export interface ChipHarnessFilters extends BaseHarnessFilters { /** Only find instances whose text matches the given value. */ text?: string | RegExp; + /** + * Only find chip instances whose selected state matches the given value. + * @deprecated Use `MatChipOptionHarness` together with `ChipOptionHarnessFilters`. + * @breaking-change 12.0.0 + */ + selected?: boolean; } -export interface ChipInputHarnessFilters extends BaseHarnessFilters { - /** Filters based on the value of the input. */ - value?: string | RegExp; - /** Filters based on the placeholder text of the input. */ - placeholder?: string | RegExp; -} - -export interface ChipListboxHarnessFilters extends BaseHarnessFilters {} - +/** A set of criteria that can be used to filter a list of selectable chip instances. */ export interface ChipOptionHarnessFilters extends ChipHarnessFilters { /** Only find chip instances whose selected state matches the given value. */ selected?: boolean; } -export interface ChipGridHarnessFilters extends BaseHarnessFilters {} +/** A set of criteria that can be used to filter chip list instances. */ +export interface ChipListHarnessFilters extends BaseHarnessFilters {} -export interface ChipRowHarnessFilters extends ChipHarnessFilters {} +/** A set of criteria that can be used to filter selectable chip list instances. */ +export interface ChipListboxHarnessFilters extends BaseHarnessFilters {} -export interface ChipSetHarnessFilters extends BaseHarnessFilters {} +/** A set of criteria that can be used to filter a list of `MatChipListInputHarness` instances. */ +export interface ChipInputHarnessFilters extends BaseHarnessFilters { + /** Filters based on the value of the input. */ + value?: string | RegExp; + /** Filters based on the placeholder text of the input. */ + placeholder?: string | RegExp; +} +/** A set of criteria that can be used to filter a list of `MatChipRemoveHarness` instances. */ export interface ChipRemoveHarnessFilters extends BaseHarnessFilters {} +/** A set of criteria that can be used to filter a list of `MatChipAvatarHarness` instances. */ export interface ChipAvatarHarnessFilters extends BaseHarnessFilters {} diff --git a/src/material/legacy-chips/testing/chip-harness.ts b/src/material/legacy-chips/testing/chip-harness.ts new file mode 100644 index 000000000000..af3991fb72e5 --- /dev/null +++ b/src/material/legacy-chips/testing/chip-harness.ts @@ -0,0 +1,117 @@ +/** + * @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 {ContentContainerComponentHarness, HarnessPredicate, TestKey} from '@angular/cdk/testing'; +import {MatLegacyChipAvatarHarness} from './chip-avatar-harness'; +import { + ChipAvatarHarnessFilters, + ChipHarnessFilters, + ChipRemoveHarnessFilters, +} from './chip-harness-filters'; +import {MatLegacyChipRemoveHarness} from './chip-remove-harness'; + +/** Harness for interacting with a standard selectable Angular Material chip in tests. */ +export class MatLegacyChipHarness extends ContentContainerComponentHarness { + /** The selector for the host element of a `MatChip` instance. */ + static hostSelector = '.mat-chip'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a `MatChipHarness` that meets + * certain criteria. + * @param options Options for filtering which chip instances are considered a match. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: ChipHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatLegacyChipHarness, options) + .addOption('text', options.text, (harness, label) => + HarnessPredicate.stringMatches(harness.getText(), label), + ) + .addOption( + 'selected', + options.selected, + async (harness, selected) => (await harness.isSelected()) === selected, + ); + } + + /** Gets the text of the chip. */ + async getText(): Promise { + return (await this.host()).text({ + exclude: '.mat-chip-avatar, .mat-chip-trailing-icon, .mat-icon', + }); + } + + /** + * Whether the chip is selected. + * @deprecated Use `MatChipOptionHarness.isSelected` instead. + * @breaking-change 12.0.0 + */ + async isSelected(): Promise { + return (await this.host()).hasClass('mat-chip-selected'); + } + + /** Whether the chip is disabled. */ + async isDisabled(): Promise { + return (await this.host()).hasClass('mat-chip-disabled'); + } + + /** + * Selects the given chip. Only applies if it's selectable. + * @deprecated Use `MatChipOptionHarness.select` instead. + * @breaking-change 12.0.0 + */ + async select(): Promise { + if (!(await this.isSelected())) { + await this.toggle(); + } + } + + /** + * Deselects the given chip. Only applies if it's selectable. + * @deprecated Use `MatChipOptionHarness.deselect` instead. + * @breaking-change 12.0.0 + */ + async deselect(): Promise { + if (await this.isSelected()) { + await this.toggle(); + } + } + + /** + * Toggles the selected state of the given chip. Only applies if it's selectable. + * @deprecated Use `MatChipOptionHarness.toggle` instead. + * @breaking-change 12.0.0 + */ + async toggle(): Promise { + return (await this.host()).sendKeys(' '); + } + + /** Removes the given chip. Only applies if it's removable. */ + async remove(): Promise { + await (await this.host()).sendKeys(TestKey.DELETE); + } + + /** + * Gets the remove button inside of a chip. + * @param filter Optionally filters which remove buttons are included. + */ + async getRemoveButton( + filter: ChipRemoveHarnessFilters = {}, + ): Promise { + return this.locatorFor(MatLegacyChipRemoveHarness.with(filter))(); + } + + /** + * Gets the avatar inside a chip. + * @param filter Optionally filters which avatars are included. + */ + async getAvatar( + filter: ChipAvatarHarnessFilters = {}, + ): Promise { + return this.locatorForOptional(MatLegacyChipAvatarHarness.with(filter))(); + } +} diff --git a/src/material-experimental/mdc-chips/testing/chip-input-harness.ts b/src/material/legacy-chips/testing/chip-input-harness.ts similarity index 75% rename from src/material-experimental/mdc-chips/testing/chip-input-harness.ts rename to src/material/legacy-chips/testing/chip-input-harness.ts index 157073d6eb09..0238cdac7c1a 100644 --- a/src/material-experimental/mdc-chips/testing/chip-input-harness.ts +++ b/src/material/legacy-chips/testing/chip-input-harness.ts @@ -6,29 +6,21 @@ * found in the LICENSE file at https://angular.io/license */ -import { - ComponentHarness, - ComponentHarnessConstructor, - HarnessPredicate, - TestKey, -} from '@angular/cdk/testing'; +import {HarnessPredicate, ComponentHarness, TestKey} from '@angular/cdk/testing'; import {ChipInputHarnessFilters} from './chip-harness-filters'; -/** Harness for interacting with a grid's chip input in tests. */ -export class MatChipInputHarness extends ComponentHarness { - static hostSelector = '.mat-mdc-chip-input'; +/** Harness for interacting with a standard Material chip inputs in tests. */ +export class MatLegacyChipInputHarness extends ComponentHarness { + static hostSelector = '.mat-chip-input'; /** - * Gets a `HarnessPredicate` that can be used to search for a chip input with specific - * attributes. + * Gets a `HarnessPredicate` that can be used to search for a `MatChipInputHarness` that meets + * certain criteria. * @param options Options for filtering which input instances are considered a match. * @return a `HarnessPredicate` configured with the given options. */ - static with( - this: ComponentHarnessConstructor, - options: ChipInputHarnessFilters = {}, - ): HarnessPredicate { - return new HarnessPredicate(this, options) + static with(options: ChipInputHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatLegacyChipInputHarness, options) .addOption('value', options.value, async (harness, value) => { return (await harness.getValue()) === value; }) @@ -39,23 +31,23 @@ export class MatChipInputHarness extends ComponentHarness { /** Whether the input is disabled. */ async isDisabled(): Promise { - return (await this.host()).getProperty('disabled'); + return (await this.host()).getProperty('disabled')!; } /** Whether the input is required. */ async isRequired(): Promise { - return (await this.host()).getProperty('required'); + return (await this.host()).getProperty('required')!; } /** Gets the value of the input. */ async getValue(): Promise { // The "value" property of the native input is never undefined. - return await (await this.host()).getProperty('value'); + return (await (await this.host()).getProperty('value'))!; } /** Gets the placeholder of the input. */ async getPlaceholder(): Promise { - return await (await this.host()).getProperty('placeholder'); + return await (await this.host()).getProperty('placeholder'); } /** diff --git a/src/material/legacy-chips/testing/chip-list-harness.spec.ts b/src/material/legacy-chips/testing/chip-list-harness.spec.ts new file mode 100644 index 000000000000..ac4f2f5337d0 --- /dev/null +++ b/src/material/legacy-chips/testing/chip-list-harness.spec.ts @@ -0,0 +1,24 @@ +import {MatLegacyChipsModule} from '@angular/material/legacy-chips'; +import {runHarnessTests} from '@angular/material/legacy-chips/testing/shared.spec'; +import {MatIconModule} from '@angular/material/icon'; +import {MatIconHarness} from '@angular/material/icon/testing'; +import {MatLegacyChipListHarness} from './chip-list-harness'; +import {MatLegacyChipHarness} from './chip-harness'; +import {MatLegacyChipInputHarness} from './chip-input-harness'; +import {MatLegacyChipRemoveHarness} from './chip-remove-harness'; +import {MatLegacyChipListboxHarness} from './chip-listbox-harness'; +import {MatLegacyChipOptionHarness} from './chip-option-harness'; + +describe('Non-MDC-based MatChipListHarness', () => { + runHarnessTests( + MatLegacyChipsModule, + MatLegacyChipListHarness, + MatLegacyChipListboxHarness, + MatLegacyChipHarness, + MatLegacyChipOptionHarness, + MatLegacyChipInputHarness, + MatLegacyChipRemoveHarness, + MatIconModule, + MatIconHarness, + ); +}); diff --git a/src/material/chips/testing/chip-list-harness.ts b/src/material/legacy-chips/testing/chip-list-harness.ts similarity index 85% rename from src/material/chips/testing/chip-list-harness.ts rename to src/material/legacy-chips/testing/chip-list-harness.ts index 6dabcc54d046..0e5eba76ab31 100644 --- a/src/material/chips/testing/chip-list-harness.ts +++ b/src/material/legacy-chips/testing/chip-list-harness.ts @@ -7,8 +7,8 @@ */ import {ComponentHarness, HarnessPredicate, parallel} from '@angular/cdk/testing'; -import {MatChipHarness} from './chip-harness'; -import {MatChipInputHarness} from './chip-input-harness'; +import {MatLegacyChipHarness} from './chip-harness'; +import {MatLegacyChipInputHarness} from './chip-input-harness'; import { ChipListHarnessFilters, ChipHarnessFilters, @@ -45,7 +45,7 @@ export abstract class _MatChipListHarnessBase extends ComponentHarness { } /** Harness for interacting with a standard chip list in tests. */ -export class MatChipListHarness extends _MatChipListHarnessBase { +export class MatLegacyChipListHarness extends _MatChipListHarnessBase { /** The selector for the host element of a `MatChipList` instance. */ static hostSelector = '.mat-chip-list'; @@ -55,16 +55,16 @@ export class MatChipListHarness extends _MatChipListHarnessBase { * @param options Options for filtering which chip list instances are considered a match. * @return a `HarnessPredicate` configured with the given options. */ - static with(options: ChipListHarnessFilters = {}): HarnessPredicate { - return new HarnessPredicate(MatChipListHarness, options); + static with(options: ChipListHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatLegacyChipListHarness, options); } /** * Gets the list of chips inside the chip list. * @param filter Optionally filters which chips are included. */ - async getChips(filter: ChipHarnessFilters = {}): Promise { - return this.locatorForAll(MatChipHarness.with(filter))(); + async getChips(filter: ChipHarnessFilters = {}): Promise { + return this.locatorForAll(MatLegacyChipHarness.with(filter))(); } /** @@ -86,7 +86,7 @@ export class MatChipListHarness extends _MatChipListHarnessBase { * Gets the `MatChipInput` inside the chip list. * @param filter Optionally filters which chip input is included. */ - async getInput(filter: ChipInputHarnessFilters = {}): Promise { + async getInput(filter: ChipInputHarnessFilters = {}): Promise { // The input isn't required to be a descendant of the chip list so we have to look it up by id. const inputId = await (await this.host()).getAttribute('data-mat-chip-input'); @@ -95,7 +95,7 @@ export class MatChipListHarness extends _MatChipListHarnessBase { } return this.documentRootLocatorFactory().locatorFor( - MatChipInputHarness.with({...filter, selector: `#${inputId}`}), + MatLegacyChipInputHarness.with({...filter, selector: `#${inputId}`}), )(); } } diff --git a/src/material/legacy-chips/testing/chip-listbox-harness.ts b/src/material/legacy-chips/testing/chip-listbox-harness.ts new file mode 100644 index 000000000000..bb9cceba9c99 --- /dev/null +++ b/src/material/legacy-chips/testing/chip-listbox-harness.ts @@ -0,0 +1,51 @@ +/** + * @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 {HarnessPredicate, parallel} from '@angular/cdk/testing'; +import {MatLegacyChipOptionHarness} from './chip-option-harness'; +import {ChipListboxHarnessFilters, ChipOptionHarnessFilters} from './chip-harness-filters'; +import {_MatChipListHarnessBase} from './chip-list-harness'; + +/** Harness for interacting with a standard selectable chip list in tests. */ +export class MatLegacyChipListboxHarness extends _MatChipListHarnessBase { + /** The selector for the host element of a `MatChipList` instance. */ + static hostSelector = '.mat-chip-list'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a `MatChipListHarness` that meets + * certain criteria. + * @param options Options for filtering which chip list instances are considered a match. + * @return a `HarnessPredicate` configured with the given options. + */ + static with( + options: ChipListboxHarnessFilters = {}, + ): HarnessPredicate { + return new HarnessPredicate(MatLegacyChipListboxHarness, options); + } + + /** + * Gets the list of chips inside the chip list. + * @param filter Optionally filters which chips are included. + */ + async getChips(filter: ChipOptionHarnessFilters = {}): Promise { + return this.locatorForAll(MatLegacyChipOptionHarness.with(filter))(); + } + + /** + * Selects a chip inside the chip list. + * @param filter An optional filter to apply to the child chips. + * All the chips matching the filter will be selected. + */ + async selectChips(filter: ChipOptionHarnessFilters = {}): Promise { + const chips = await this.getChips(filter); + if (!chips.length) { + throw Error(`Cannot find chip matching filter ${JSON.stringify(filter)}`); + } + await parallel(() => chips.map(chip => chip.select())); + } +} diff --git a/src/material-experimental/mdc-chips/testing/chip-option-harness.ts b/src/material/legacy-chips/testing/chip-option-harness.ts similarity index 55% rename from src/material-experimental/mdc-chips/testing/chip-option-harness.ts rename to src/material/legacy-chips/testing/chip-option-harness.ts index e4ca239a8307..787887686251 100644 --- a/src/material-experimental/mdc-chips/testing/chip-option-harness.ts +++ b/src/material/legacy-chips/testing/chip-option-harness.ts @@ -6,25 +6,24 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentHarnessConstructor, HarnessPredicate} from '@angular/cdk/testing'; -import {MatChipHarness} from './chip-harness'; +import {HarnessPredicate} from '@angular/cdk/testing'; +import {MatLegacyChipHarness} from './chip-harness'; import {ChipOptionHarnessFilters} from './chip-harness-filters'; -/** Harness for interacting with a mat-chip-option in tests. */ -export class MatChipOptionHarness extends MatChipHarness { - static override hostSelector = '.mat-mdc-chip-option'; +export class MatLegacyChipOptionHarness extends MatLegacyChipHarness { + /** The selector for the host element of a selectable chip instance. */ + static override hostSelector = '.mat-chip'; /** - * Gets a `HarnessPredicate` that can be used to search for a chip option with specific - * attributes. - * @param options Options for narrowing the search. + * Gets a `HarnessPredicate` that can be used to search for a `MatChipOptionHarness` + * that meets certain criteria. + * @param options Options for filtering which chip instances are considered a match. * @return a `HarnessPredicate` configured with the given options. */ - static override with( - this: ComponentHarnessConstructor, + static override with( options: ChipOptionHarnessFilters = {}, - ): HarnessPredicate { - return new HarnessPredicate(MatChipOptionHarness, options) + ): HarnessPredicate { + return new HarnessPredicate(MatLegacyChipOptionHarness, options) .addOption('text', options.text, (harness, label) => HarnessPredicate.stringMatches(harness.getText(), label), ) @@ -32,30 +31,30 @@ export class MatChipOptionHarness extends MatChipHarness { 'selected', options.selected, async (harness, selected) => (await harness.isSelected()) === selected, - ) as unknown as HarnessPredicate; + ); } /** Whether the chip is selected. */ - async isSelected(): Promise { - return (await this.host()).hasClass('mat-mdc-chip-selected'); + override async isSelected(): Promise { + return (await this.host()).hasClass('mat-chip-selected'); } /** Selects the given chip. Only applies if it's selectable. */ - async select(): Promise { + override async select(): Promise { if (!(await this.isSelected())) { await this.toggle(); } } /** Deselects the given chip. Only applies if it's selectable. */ - async deselect(): Promise { + override async deselect(): Promise { if (await this.isSelected()) { await this.toggle(); } } /** Toggles the selected state of the given chip. */ - async toggle(): Promise { - return (await this._primaryAction()).click(); + override async toggle(): Promise { + return (await this.host()).sendKeys(' '); } } diff --git a/src/material-experimental/mdc-chips/testing/chip-remove-harness.ts b/src/material/legacy-chips/testing/chip-remove-harness.ts similarity index 63% rename from src/material-experimental/mdc-chips/testing/chip-remove-harness.ts rename to src/material/legacy-chips/testing/chip-remove-harness.ts index e40633b8e652..e8ae66f31cf4 100644 --- a/src/material-experimental/mdc-chips/testing/chip-remove-harness.ts +++ b/src/material/legacy-chips/testing/chip-remove-harness.ts @@ -6,28 +6,23 @@ * found in the LICENSE file at https://angular.io/license */ -import { - ComponentHarness, - ComponentHarnessConstructor, - HarnessPredicate, -} from '@angular/cdk/testing'; +import {HarnessPredicate, ComponentHarness} from '@angular/cdk/testing'; import {ChipRemoveHarnessFilters} from './chip-harness-filters'; /** Harness for interacting with a standard Material chip remove button in tests. */ -export class MatChipRemoveHarness extends ComponentHarness { - static hostSelector = '.mat-mdc-chip-remove'; +export class MatLegacyChipRemoveHarness extends ComponentHarness { + static hostSelector = '.mat-chip-remove'; /** - * Gets a `HarnessPredicate` that can be used to search for a chip remove with specific - * attributes. + * Gets a `HarnessPredicate` that can be used to search for a `MatChipRemoveHarness` that meets + * certain criteria. * @param options Options for filtering which input instances are considered a match. * @return a `HarnessPredicate` configured with the given options. */ - static with( - this: ComponentHarnessConstructor, + static with( options: ChipRemoveHarnessFilters = {}, - ): HarnessPredicate { - return new HarnessPredicate(this, options); + ): HarnessPredicate { + return new HarnessPredicate(MatLegacyChipRemoveHarness, options); } /** Clicks the remove button. */ diff --git a/src/material-experimental/mdc-chips/testing/index.ts b/src/material/legacy-chips/testing/index.ts similarity index 100% rename from src/material-experimental/mdc-chips/testing/index.ts rename to src/material/legacy-chips/testing/index.ts diff --git a/src/material-experimental/mdc-chips/testing/public-api.ts b/src/material/legacy-chips/testing/public-api.ts similarity index 74% rename from src/material-experimental/mdc-chips/testing/public-api.ts rename to src/material/legacy-chips/testing/public-api.ts index 0689ddb6c35a..c227bc756572 100644 --- a/src/material-experimental/mdc-chips/testing/public-api.ts +++ b/src/material/legacy-chips/testing/public-api.ts @@ -6,13 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './chip-avatar-harness'; export * from './chip-harness'; export * from './chip-harness-filters'; +export {MatLegacyChipListHarness} from './chip-list-harness'; export * from './chip-input-harness'; export * from './chip-remove-harness'; export * from './chip-option-harness'; export * from './chip-listbox-harness'; -export * from './chip-grid-harness'; -export * from './chip-row-harness'; -export * from './chip-set-harness'; diff --git a/src/material/chips/testing/shared.spec.ts b/src/material/legacy-chips/testing/shared.spec.ts similarity index 93% rename from src/material/chips/testing/shared.spec.ts rename to src/material/legacy-chips/testing/shared.spec.ts index 03f2f54f3038..b00dab424f5a 100644 --- a/src/material/chips/testing/shared.spec.ts +++ b/src/material/legacy-chips/testing/shared.spec.ts @@ -3,26 +3,26 @@ import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {HarnessLoader, parallel, TestKey} from '@angular/cdk/testing'; import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; import {ComponentFixture, TestBed} from '@angular/core/testing'; -import {MatChipsModule} from '@angular/material/chips'; +import {MatLegacyChipsModule} from '@angular/material/legacy-chips'; import {MatLegacyFormFieldModule} from '@angular/material/legacy-form-field'; import {MatIconModule} from '@angular/material/icon'; import {MatIconHarness} from '@angular/material/icon/testing'; -import {MatChipListHarness} from './chip-list-harness'; -import {MatChipHarness} from './chip-harness'; -import {MatChipInputHarness} from './chip-input-harness'; -import {MatChipRemoveHarness} from './chip-remove-harness'; -import {MatChipOptionHarness} from './chip-option-harness'; -import {MatChipListboxHarness} from './chip-listbox-harness'; +import {MatLegacyChipListHarness} from './chip-list-harness'; +import {MatLegacyChipHarness} from './chip-harness'; +import {MatLegacyChipInputHarness} from './chip-input-harness'; +import {MatLegacyChipRemoveHarness} from './chip-remove-harness'; +import {MatLegacyChipOptionHarness} from './chip-option-harness'; +import {MatLegacyChipListboxHarness} from './chip-listbox-harness'; /** Shared tests to run on both the original and MDC-based chips. */ export function runHarnessTests( - chipsModule: typeof MatChipsModule, - chipListHarness: typeof MatChipListHarness, - listboxHarness: typeof MatChipListboxHarness, - chipHarness: typeof MatChipHarness, - chipOptionHarness: typeof MatChipOptionHarness, - chipInputHarness: typeof MatChipInputHarness, - chipRemoveHarness: typeof MatChipRemoveHarness, + chipsModule: typeof MatLegacyChipsModule, + chipListHarness: typeof MatLegacyChipListHarness, + listboxHarness: typeof MatLegacyChipListboxHarness, + chipHarness: typeof MatLegacyChipHarness, + chipOptionHarness: typeof MatLegacyChipOptionHarness, + chipInputHarness: typeof MatLegacyChipInputHarness, + chipRemoveHarness: typeof MatLegacyChipRemoveHarness, iconModule: typeof MatIconModule, iconHarness: typeof MatIconHarness, ) { diff --git a/src/material/legacy-core/theming/_all-theme.scss b/src/material/legacy-core/theming/_all-theme.scss index 82560eb8c6a2..53af7abca282 100644 --- a/src/material/legacy-core/theming/_all-theme.scss +++ b/src/material/legacy-core/theming/_all-theme.scss @@ -10,6 +10,7 @@ @use '../../legacy-select/select-theme'; @use '../../legacy-autocomplete/autocomplete-theme'; @use '../../legacy-dialog/dialog-theme'; +@use '../../legacy-chips/chips-theme'; // Create a theme. @mixin all-legacy-component-themes($theme-or-color-config) { @@ -25,6 +26,7 @@ @include checkbox-theme.theme($theme-or-color-config); @include autocomplete-theme.theme($theme-or-color-config); @include dialog-theme.theme($theme-or-color-config); + @include chips-theme.theme($theme-or-color-config); @include all-theme.private-all-unmigrated-component-themes($theme-or-color-config); } } diff --git a/src/material/legacy-core/typography/_all-typography.scss b/src/material/legacy-core/typography/_all-typography.scss index 6a117ecdd963..0310fe007fbd 100644 --- a/src/material/legacy-core/typography/_all-typography.scss +++ b/src/material/legacy-core/typography/_all-typography.scss @@ -12,6 +12,7 @@ @use '../../legacy-select/select-theme'; @use '../../legacy-autocomplete/autocomplete-theme'; @use '../../legacy-dialog/dialog-theme'; +@use '../../legacy-chips/chips-theme'; // Includes all of the typographic styles. @mixin all-legacy-component-typographies($config-or-theme: null) { @@ -41,6 +42,7 @@ @include checkbox-theme.typography($config); @include autocomplete-theme.typography($config); @include dialog-theme.typography($config); + @include chips-theme.typography($config); } // @deprecated Use `all-legacy-component-typographies`. diff --git a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.ts b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.ts index 0c34abb52973..c0f6a074ac85 100644 --- a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.ts +++ b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.ts @@ -7,7 +7,7 @@ import {MatFormFieldModule} from '@angular/material/form-field'; import {MatInputModule} from '@angular/material/input'; import {MatListModule} from '@angular/material-experimental/mdc-list'; import {MatProgressBarModule} from '@angular/material/progress-bar'; -import {MatChipsModule} from '@angular/material-experimental/mdc-chips'; +import {MatChipsModule} from '@angular/material/chips'; import {MatMenuModule} from '@angular/material-experimental/mdc-menu'; import {MatRadioModule} from '@angular/material-experimental/mdc-radio'; import {MatSlideToggleModule} from '@angular/material-experimental/mdc-slide-toggle'; diff --git a/src/universal-app/kitchen-sink/kitchen-sink.ts b/src/universal-app/kitchen-sink/kitchen-sink.ts index c439aaf668f7..25fc86034e33 100644 --- a/src/universal-app/kitchen-sink/kitchen-sink.ts +++ b/src/universal-app/kitchen-sink/kitchen-sink.ts @@ -9,7 +9,7 @@ import {MatButtonModule} from '@angular/material/button'; import {MatButtonToggleModule} from '@angular/material/button-toggle'; import {MatLegacyCardModule} from '@angular/material/legacy-card'; import {MatLegacyCheckboxModule} from '@angular/material/legacy-checkbox'; -import {MatChipsModule} from '@angular/material/chips'; +import {MatLegacyChipsModule} from '@angular/material/legacy-chips'; import {MatTableModule} from '@angular/material/table'; import {MatDatepickerModule} from '@angular/material/datepicker'; import {MatLegacyDialogModule, MatLegacyDialog} from '@angular/material/legacy-dialog'; @@ -105,7 +105,7 @@ export class KitchenSink { MatButtonToggleModule, MatLegacyCardModule, MatLegacyCheckboxModule, - MatChipsModule, + MatLegacyChipsModule, MatDatepickerModule, MatLegacyDialogModule, MatDividerModule, diff --git a/tools/public_api_guard/material/chips-testing.md b/tools/public_api_guard/material/chips-testing.md index e993d3a879ea..6fed5283ed56 100644 --- a/tools/public_api_guard/material/chips-testing.md +++ b/tools/public_api_guard/material/chips-testing.md @@ -4,63 +4,86 @@ ```ts +import { AsyncFactoryFn } from '@angular/cdk/testing'; import { BaseHarnessFilters } from '@angular/cdk/testing'; import { ComponentHarness } from '@angular/cdk/testing'; +import { ComponentHarnessConstructor } from '@angular/cdk/testing'; import { ContentContainerComponentHarness } from '@angular/cdk/testing'; import { HarnessPredicate } from '@angular/cdk/testing'; +import { TestElement } from '@angular/cdk/testing'; import { TestKey } from '@angular/cdk/testing'; -// @public +// @public (undocumented) export interface ChipAvatarHarnessFilters extends BaseHarnessFilters { } -// @public +// @public (undocumented) +export interface ChipGridHarnessFilters extends BaseHarnessFilters { +} + +// @public (undocumented) export interface ChipHarnessFilters extends BaseHarnessFilters { - // @deprecated - selected?: boolean; text?: string | RegExp; } -// @public +// @public (undocumented) export interface ChipInputHarnessFilters extends BaseHarnessFilters { placeholder?: string | RegExp; value?: string | RegExp; } -// @public +// @public (undocumented) export interface ChipListboxHarnessFilters extends BaseHarnessFilters { } -// @public -export interface ChipListHarnessFilters extends BaseHarnessFilters { +// @public (undocumented) +export interface ChipOptionHarnessFilters extends ChipHarnessFilters { + selected?: boolean; +} + +// @public (undocumented) +export interface ChipRemoveHarnessFilters extends BaseHarnessFilters { +} + +// @public (undocumented) +export interface ChipRowHarnessFilters extends ChipHarnessFilters { +} + +// @public (undocumented) +export interface ChipSetHarnessFilters extends BaseHarnessFilters { } // @public -export interface ChipOptionHarnessFilters extends ChipHarnessFilters { - selected?: boolean; +export class MatChipAvatarHarness extends ComponentHarness { + // (undocumented) + static hostSelector: string; + static with(this: ComponentHarnessConstructor, options?: ChipAvatarHarnessFilters): HarnessPredicate; } // @public -export interface ChipRemoveHarnessFilters extends BaseHarnessFilters { +export class MatChipGridHarness extends ComponentHarness { + getInput(filter?: ChipInputHarnessFilters): Promise; + getRows(filter?: ChipRowHarnessFilters): Promise; + // (undocumented) + static hostSelector: string; + isDisabled(): Promise; + isInvalid(): Promise; + isRequired(): Promise; + static with(this: ComponentHarnessConstructor, options?: ChipGridHarnessFilters): HarnessPredicate; } // @public export class MatChipHarness extends ContentContainerComponentHarness { - // @deprecated - deselect(): Promise; getAvatar(filter?: ChipAvatarHarnessFilters): Promise; getRemoveButton(filter?: ChipRemoveHarnessFilters): Promise; getText(): Promise; + // (undocumented) static hostSelector: string; isDisabled(): Promise; - // @deprecated - isSelected(): Promise; + // (undocumented) + protected _primaryAction: AsyncFactoryFn; remove(): Promise; - // @deprecated - select(): Promise; - // @deprecated - toggle(): Promise; - static with(options?: ChipHarnessFilters): HarnessPredicate; + static with(this: ComponentHarnessConstructor, options?: ChipHarnessFilters): HarnessPredicate; } // @public @@ -76,35 +99,31 @@ export class MatChipInputHarness extends ComponentHarness { isRequired(): Promise; sendSeparatorKey(key: TestKey | string): Promise; setValue(newValue: string): Promise; - static with(options?: ChipInputHarnessFilters): HarnessPredicate; + static with(this: ComponentHarnessConstructor, options?: ChipInputHarnessFilters): HarnessPredicate; } // @public -export class MatChipListboxHarness extends _MatChipListHarnessBase { +export class MatChipListboxHarness extends ComponentHarness { getChips(filter?: ChipOptionHarnessFilters): Promise; + getOrientation(): Promise<'horizontal' | 'vertical'>; + // (undocumented) static hostSelector: string; + isDisabled(): Promise; + isMultiple(): Promise; + isRequired(): Promise; selectChips(filter?: ChipOptionHarnessFilters): Promise; - static with(options?: ChipListboxHarnessFilters): HarnessPredicate; + static with(this: ComponentHarnessConstructor, options?: ChipListboxHarnessFilters): HarnessPredicate; } // @public -export class MatChipListHarness extends _MatChipListHarnessBase { - getChips(filter?: ChipHarnessFilters): Promise; - getInput(filter?: ChipInputHarnessFilters): Promise; - static hostSelector: string; - // @deprecated - selectChips(filter?: ChipHarnessFilters): Promise; - static with(options?: ChipListHarnessFilters): HarnessPredicate; -} - -// @public (undocumented) export class MatChipOptionHarness extends MatChipHarness { deselect(): Promise; + // (undocumented) static hostSelector: string; isSelected(): Promise; select(): Promise; toggle(): Promise; - static with(options?: ChipOptionHarnessFilters): HarnessPredicate; + static with(this: ComponentHarnessConstructor, options?: ChipOptionHarnessFilters): HarnessPredicate; } // @public @@ -112,7 +131,23 @@ export class MatChipRemoveHarness extends ComponentHarness { click(): Promise; // (undocumented) static hostSelector: string; - static with(options?: ChipRemoveHarnessFilters): HarnessPredicate; + static with(this: ComponentHarnessConstructor, options?: ChipRemoveHarnessFilters): HarnessPredicate; +} + +// @public +export class MatChipRowHarness extends MatChipHarness { + // (undocumented) + static hostSelector: string; + isEditable(): Promise; + isEditing(): Promise; +} + +// @public +export class MatChipSetHarness extends ComponentHarness { + getChips(filter?: ChipHarnessFilters): Promise; + // (undocumented) + static hostSelector: string; + static with(this: ComponentHarnessConstructor, options?: ChipSetHarnessFilters): HarnessPredicate; } // (No @packageDocumentation comment for this package) diff --git a/tools/public_api_guard/material/chips.md b/tools/public_api_guard/material/chips.md index 9bbb852fcf03..9a17fc1883b0 100644 --- a/tools/public_api_guard/material/chips.md +++ b/tools/public_api_guard/material/chips.md @@ -6,6 +6,7 @@ import { _AbstractConstructor } from '@angular/material/core'; import { AfterContentInit } from '@angular/core'; +import { AfterViewInit } from '@angular/core'; import { BooleanInput } from '@angular/cdk/coercion'; import { CanColor } from '@angular/material/core'; import { CanDisable } from '@angular/material/core'; @@ -19,14 +20,18 @@ import { DoCheck } from '@angular/core'; import { ElementRef } from '@angular/core'; import { ErrorStateMatcher } from '@angular/material/core'; import { EventEmitter } from '@angular/core'; -import { FocusableOption } from '@angular/cdk/a11y'; import { FocusKeyManager } from '@angular/cdk/a11y'; +import { FocusMonitor } from '@angular/cdk/a11y'; import { FormGroupDirective } from '@angular/forms'; import { HasTabIndex } from '@angular/material/core'; import * as i0 from '@angular/core'; -import * as i4 from '@angular/material/core'; +import * as i11 from '@angular/material/core'; +import * as i12 from '@angular/common'; import { InjectionToken } from '@angular/core'; -import { MatLegacyFormFieldControl } from '@angular/material/legacy-form-field'; +import { MatChipAvatar as MatChipAvatar_2 } from '@angular/material/chips'; +import { MatFormField } from '@angular/material/form-field'; +import { MatFormFieldControl } from '@angular/material/form-field'; +import { MatRipple } from '@angular/material/core'; import { NgControl } from '@angular/forms'; import { NgForm } from '@angular/forms'; import { NgZone } from '@angular/core'; @@ -34,53 +39,65 @@ import { Observable } from 'rxjs'; import { OnChanges } from '@angular/core'; import { OnDestroy } from '@angular/core'; import { OnInit } from '@angular/core'; -import { Platform } from '@angular/cdk/platform'; import { QueryList } from '@angular/core'; -import { RippleConfig } from '@angular/material/core'; import { RippleGlobalOptions } from '@angular/material/core'; -import { RippleTarget } from '@angular/material/core'; -import { SelectionModel } from '@angular/cdk/collections'; import { Subject } from 'rxjs'; // @public -export const MAT_CHIP_AVATAR: InjectionToken; +export const MAT_CHIP: InjectionToken; // @public -export const MAT_CHIP_REMOVE: InjectionToken; +export const MAT_CHIP_AVATAR: InjectionToken; // @public -export const MAT_CHIP_TRAILING_ICON: InjectionToken; +export const MAT_CHIP_LISTBOX_CONTROL_VALUE_ACCESSOR: any; + +// @public +export const MAT_CHIP_REMOVE: InjectionToken; + +// @public +export const MAT_CHIP_TRAILING_ICON: InjectionToken; // @public export const MAT_CHIPS_DEFAULT_OPTIONS: InjectionToken; // @public -export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDestroy, CanColor, CanDisableRipple, RippleTarget, HasTabIndex, CanDisable { - constructor(elementRef: ElementRef, _ngZone: NgZone, platform: Platform, globalRippleOptions: RippleGlobalOptions | null, _changeDetectorRef: ChangeDetectorRef, _document: any, animationMode?: string, tabIndex?: string); - // (undocumented) - _addHostClassName(): void; +export class MatChip extends _MatChipMixinBase implements AfterViewInit, CanColor, CanDisableRipple, CanDisable, HasTabIndex, OnDestroy { + constructor(_changeDetectorRef: ChangeDetectorRef, elementRef: ElementRef, _ngZone: NgZone, _focusMonitor: FocusMonitor, _document: any, animationMode?: string, _globalRippleOptions?: RippleGlobalOptions | undefined, tabIndex?: string); _animationsDisabled: boolean; - get ariaSelected(): string | null; - avatar: MatChipAvatar; + ariaLabel: string | null; + protected basicChipAttrName: string; // (undocumented) - _blur(): void; - _chipListDisabled: boolean; - _chipListMultiple: boolean; - chipListSelectable: boolean; - deselect(): void; + _changeDetectorRef: ChangeDetectorRef; readonly destroyed: EventEmitter; - get disabled(): boolean; - set disabled(value: BooleanInput); // (undocumented) - protected _disabled: boolean; + protected _document: Document; focus(): void; - _handleClick(event: Event): void; + _getActions(): MatChipAction[]; + _getSourceAction(target: Node): MatChipAction | undefined; _handleKeydown(event: KeyboardEvent): void; - _hasFocus: boolean; + _handlePrimaryActionInteraction(): void; + // (undocumented) + _hasFocus(): boolean; + _hasTrailingIcon(): boolean; + get highlighted(): boolean; + set highlighted(value: BooleanInput); + // (undocumented) + protected _highlighted: boolean; + id: string; + readonly _isBasicChip: boolean; + readonly _isRippleCentered = false; + _isRippleDisabled(): boolean; + leadingIcon: MatChipAvatar; + // (undocumented) + ngAfterViewInit(): void; // (undocumented) ngOnDestroy(): void; + // (undocumented) + protected _ngZone: NgZone; readonly _onBlur: Subject; readonly _onFocus: Subject; + primaryAction: MatChipAction; get removable(): boolean; set removable(value: BooleanInput); // (undocumented) @@ -88,30 +105,48 @@ export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDes remove(): void; readonly removed: EventEmitter; removeIcon: MatChipRemove; - rippleConfig: RippleConfig & RippleGlobalOptions; - get rippleDisabled(): boolean; - role: string; - select(): void; - get selectable(): boolean; - set selectable(value: BooleanInput); - // (undocumented) - protected _selectable: boolean; - get selected(): boolean; - set selected(value: BooleanInput); - // (undocumented) - protected _selected: boolean; - readonly selectionChange: EventEmitter; - selectViaInteraction(): void; - toggleSelected(isUserInput?: boolean): boolean; + ripple: MatRipple; + role: string | null; trailingIcon: MatChipTrailingIcon; get value(): any; set value(value: any); // (undocumented) protected _value: any; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public +class MatChipAction extends _MatChipActionMixinBase implements HasTabIndex { + constructor(_elementRef: ElementRef, _parentChip: { + _handlePrimaryActionInteraction(): void; + remove(): void; + disabled: boolean; + }); + get disabled(): boolean; + set disabled(value: BooleanInput); + // (undocumented) + _elementRef: ElementRef; + // (undocumented) + focus(): void; + // (undocumented) + _handleClick(event: MouseEvent): void; + // (undocumented) + _handleKeydown(event: KeyboardEvent): void; + isInteractive: boolean; + _isPrimary: boolean; + // (undocumented) + protected _parentChip: { + _handlePrimaryActionInteraction(): void; + remove(): void; + disabled: boolean; + }; + // (undocumented) + static ɵdir: i0.ɵɵDirectiveDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; } // @public @@ -122,23 +157,115 @@ export class MatChipAvatar { static ɵfac: i0.ɵɵFactoryDeclaration; } +// @public +export interface MatChipEditedEvent extends MatChipEvent { + value: string; +} + +// @public +export class MatChipEditInput { + constructor(_elementRef: ElementRef, _document: any); + // (undocumented) + getNativeElement(): HTMLElement; + // (undocumented) + getValue(): string; + // (undocumented) + initialize(initialValue: string): void; + // (undocumented) + setValue(value: string): void; + // (undocumented) + static ɵdir: i0.ɵɵDirectiveDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + // @public export interface MatChipEvent { chip: MatChip; } // @public -export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, AfterContentInit { - constructor(_elementRef: ElementRef, _defaultOptions: MatChipsDefaultOptions); +export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentInit, AfterViewInit, CanUpdateErrorState, ControlValueAccessor, DoCheck, MatFormFieldControl, OnDestroy { + constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, dir: Directionality, parentForm: NgForm, parentFormGroup: FormGroupDirective, defaultErrorStateMatcher: ErrorStateMatcher, ngControl: NgControl); + protected _allowFocusEscape(): void; + _blur(): void; + readonly change: EventEmitter; + get chipBlurChanges(): Observable; + protected _chipInput: MatChipTextControl; + // (undocumented) + _chips: QueryList; + readonly controlType: string; + // (undocumented) + protected _defaultRole: string; + get disabled(): boolean; + set disabled(value: BooleanInput); + get empty(): boolean; + errorStateMatcher: ErrorStateMatcher; + focus(): void; + get focused(): boolean; + // (undocumented) + _focusLastChip(): void; + _handleKeydown(event: KeyboardEvent): void; + get id(): string; + // (undocumented) + ngAfterContentInit(): void; + // (undocumented) + ngAfterViewInit(): void; + // (undocumented) + ngDoCheck(): void; + // (undocumented) + ngOnDestroy(): void; + _onChange: (value: any) => void; + onContainerClick(event: MouseEvent): void; + _onTouched: () => void; + get placeholder(): string; + set placeholder(value: string); + // (undocumented) + protected _placeholder: string; + registerInput(inputElement: MatChipTextControl): void; + registerOnChange(fn: (value: any) => void): void; + registerOnTouched(fn: () => void): void; + get required(): boolean; + set required(value: BooleanInput); + // (undocumented) + protected _required: boolean | undefined; + setDescribedByIds(ids: string[]): void; + setDisabledState(isDisabled: boolean): void; + get shouldLabelFloat(): boolean; + get value(): any; + set value(value: any); + // (undocumented) + protected _value: any[]; + readonly valueChange: EventEmitter; + writeValue(value: any): void; + // (undocumented) + static ɵcmp: i0.ɵɵComponentDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public +export class MatChipGridChange { + constructor( + source: MatChipGrid, + value: any); + source: MatChipGrid; + value: any; +} + +// @public +export class MatChipInput implements MatChipTextControl, AfterContentInit, OnChanges, OnDestroy { + constructor(_elementRef: ElementRef, _defaultOptions: MatChipsDefaultOptions, formField?: MatFormField); get addOnBlur(): boolean; set addOnBlur(value: BooleanInput); // (undocumented) _addOnBlur: boolean; + _ariaDescribedby?: string; _blur(): void; readonly chipEnd: EventEmitter; - set chipList(value: MatChipList); + set chipGrid(value: MatChipGrid); // (undocumented) - _chipList: MatChipList; + _chipGrid: MatChipGrid; clear(): void; get disabled(): boolean; set disabled(value: BooleanInput); @@ -146,7 +273,7 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, A protected _elementRef: ElementRef; _emitChipEnd(event?: KeyboardEvent): void; get empty(): boolean; - focus(options?: FocusOptions): void; + focus(): void; // (undocumented) _focus(): void; focused: boolean; @@ -165,9 +292,11 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, A placeholder: string; separatorKeyCodes: readonly number[] | ReadonlySet; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + setDescribedByIds(ids: string[]): void; + // (undocumented) + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵfac: i0.ɵɵFactoryDeclaration; } // @public @@ -179,119 +308,129 @@ export interface MatChipInputEvent { } // @public -export class MatChipList extends _MatChipListBase implements MatLegacyFormFieldControl, ControlValueAccessor, AfterContentInit, DoCheck, OnInit, OnDestroy, CanUpdateErrorState { - constructor(_elementRef: ElementRef, _changeDetectorRef: ChangeDetectorRef, _dir: Directionality, _parentForm: NgForm, _parentFormGroup: FormGroupDirective, _defaultErrorStateMatcher: ErrorStateMatcher, ngControl: NgControl); - _allowFocusEscape(): void; +export class MatChipListbox extends MatChipSet implements AfterContentInit, OnDestroy, ControlValueAccessor { ariaOrientation: 'horizontal' | 'vertical'; _blur(): void; - readonly change: EventEmitter; + readonly change: EventEmitter; get chipBlurChanges(): Observable; - get chipFocusChanges(): Observable; - protected _chipInput: MatChipTextControl; - get chipRemoveChanges(): Observable; - chips: QueryList; + // (undocumented) + _chips: QueryList; get chipSelectionChanges(): Observable; - get compareWith(): (o1: any, o2: any) => boolean; - set compareWith(fn: (o1: any, o2: any) => boolean); - readonly controlType: string; - get disabled(): boolean; - set disabled(value: BooleanInput); + compareWith: (o1: any, o2: any) => boolean; // (undocumented) - protected _disabled: boolean; + protected _defaultRole: string; + focus(): void; // (undocumented) - protected _elementRef: ElementRef; - get empty(): boolean; - errorStateMatcher: ErrorStateMatcher; - focus(options?: FocusOptions): void; - get focused(): boolean; - _focusInput(options?: FocusOptions): void; - get id(): string; _keydown(event: KeyboardEvent): void; - _keyManager: FocusKeyManager; - _markAsTouched(): void; get multiple(): boolean; set multiple(value: BooleanInput); // (undocumented) ngAfterContentInit(): void; - // (undocumented) - ngDoCheck(): void; - // (undocumented) - ngOnDestroy(): void; - // (undocumented) - ngOnInit(): void; _onChange: (value: any) => void; - onContainerClick(event: MouseEvent): void; _onTouched: () => void; - get placeholder(): string; - set placeholder(value: string); - // (undocumented) - protected _placeholder: string; - registerInput(inputElement: MatChipTextControl): void; - // (undocumented) registerOnChange(fn: (value: any) => void): void; - // (undocumented) registerOnTouched(fn: () => void): void; get required(): boolean; set required(value: BooleanInput); // (undocumented) - protected _required: boolean | undefined; - get role(): string | null; - set role(role: string | null); + protected _required: boolean; get selectable(): boolean; set selectable(value: BooleanInput); // (undocumented) protected _selectable: boolean; - get selected(): MatChip[] | MatChip; - // (undocumented) - _selectionModel: SelectionModel; - setDescribedByIds(ids: string[]): void; - // (undocumented) + get selected(): MatChipOption[] | MatChipOption; setDisabledState(isDisabled: boolean): void; - // (undocumented) _setSelectionByValue(value: any, isUserInput?: boolean): void; - get shouldLabelFloat(): boolean; - // (undocumented) - set tabIndex(value: number); - _tabIndex: number; - _uid: string; - protected _updateFocusForDestroyedChips(): void; - protected _updateTabIndex(): void; - userAriaDescribedBy: string; - _userTabIndex: number | null; get value(): any; set value(value: any); // (undocumented) protected _value: any; - readonly valueChange: EventEmitter; - // (undocumented) writeValue(value: any): void; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵfac: i0.ɵɵFactoryDeclaration; } // @public -export class MatChipListChange { +export class MatChipListboxChange { constructor( - source: MatChipList, + source: MatChipListbox, value: any); - source: MatChipList; + source: MatChipListbox; value: any; } // @public -export class MatChipRemove { - constructor(_parentChip: MatChip, elementRef: ElementRef); - _handleClick(event: Event): void; +export class MatChipOption extends MatChip implements OnInit { + get ariaSelected(): string | null; + protected basicChipAttrName: string; + _chipListMultiple: boolean; + chipListSelectable: boolean; + deselect(): void; + // (undocumented) + _handlePrimaryActionInteraction(): void; + // (undocumented) + _hasLeadingGraphic(): MatChipAvatar_2; + // (undocumented) + ngOnInit(): void; + select(): void; + get selectable(): boolean; + set selectable(value: BooleanInput); + // (undocumented) + protected _selectable: boolean; + get selected(): boolean; + set selected(value: BooleanInput); + readonly selectionChange: EventEmitter; + selectViaInteraction(): void; + // (undocumented) + _setSelectedState(isSelected: boolean, isUserInput: boolean, emitEvent: boolean): void; + toggleSelected(isUserInput?: boolean): boolean; + // (undocumented) + static ɵcmp: i0.ɵɵComponentDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public +export class MatChipRemove extends MatChipAction { // (undocumented) - protected _parentChip: MatChip; + _handleClick(event: MouseEvent): void; + // (undocumented) + _handleKeydown(event: KeyboardEvent): void; + // (undocumented) + _isPrimary: boolean; // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } +// @public +export class MatChipRow extends MatChip implements AfterViewInit { + constructor(changeDetectorRef: ChangeDetectorRef, elementRef: ElementRef, ngZone: NgZone, focusMonitor: FocusMonitor, _document: any, animationMode?: string, globalRippleOptions?: RippleGlobalOptions, tabIndex?: string); + // (undocumented) + protected basicChipAttrName: string; + contentEditInput?: MatChipEditInput; + defaultEditInput?: MatChipEditInput; + // (undocumented) + _doubleclick(event: MouseEvent): void; + // (undocumented) + editable: boolean; + readonly edited: EventEmitter; + // (undocumented) + _handleKeydown(event: KeyboardEvent): void; + // (undocumented) + _hasTrailingIcon(): boolean; + // (undocumented) + _isEditing: boolean; + _mousedown(event: MouseEvent): void; + // (undocumented) + static ɵcmp: i0.ɵɵComponentDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + // @public export interface MatChipsDefaultOptions { separatorKeyCodes: readonly number[] | ReadonlySet; @@ -300,12 +439,52 @@ export interface MatChipsDefaultOptions { // @public export class MatChipSelectionChange { constructor( - source: MatChip, + source: MatChipOption, selected: boolean, isUserInput?: boolean); isUserInput: boolean; selected: boolean; - source: MatChip; + source: MatChipOption; +} + +// @public +export class MatChipSet extends _MatChipSetMixinBase implements AfterViewInit, HasTabIndex, OnDestroy { + constructor(_elementRef: ElementRef, _changeDetectorRef: ChangeDetectorRef, _dir: Directionality); + protected _allowFocusEscape(): void; + // (undocumented) + protected _changeDetectorRef: ChangeDetectorRef; + _chipActions: QueryList; + get chipDestroyedChanges(): Observable; + get chipFocusChanges(): Observable; + _chips: QueryList; + protected _defaultRole: string; + protected _destroyed: Subject; + get disabled(): boolean; + set disabled(value: BooleanInput); + // (undocumented) + protected _disabled: boolean; + // (undocumented) + protected _elementRef: ElementRef; + get empty(): boolean; + focus(): void; + get focused(): boolean; + protected _getChipStream(mappingFunction: (chip: C) => Observable): Observable; + _handleKeydown(event: KeyboardEvent): void; + protected _hasFocusedChip(): boolean; + protected _isValidIndex(index: number): boolean; + protected _keyManager: FocusKeyManager; + // (undocumented) + ngAfterViewInit(): void; + // (undocumented) + ngOnDestroy(): void; + protected _originatesFromChip(event: Event): boolean; + get role(): string | null; + set role(value: string | null); + protected _syncChipsState(): void; + // (undocumented) + static ɵcmp: i0.ɵɵComponentDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; } // @public (undocumented) @@ -315,11 +494,24 @@ export class MatChipsModule { // (undocumented) static ɵinj: i0.ɵɵInjectorDeclaration; // (undocumented) - static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; } // @public -export class MatChipTrailingIcon { +export interface MatChipTextControl { + empty: boolean; + focus(): void; + focused: boolean; + id: string; + placeholder: string; + setDescribedByIds(ids: string[]): void; +} + +// @public +export class MatChipTrailingIcon extends MatChipAction { + isInteractive: boolean; + // (undocumented) + _isPrimary: boolean; // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) diff --git a/tools/public_api_guard/material/legacy-chips-testing.md b/tools/public_api_guard/material/legacy-chips-testing.md new file mode 100644 index 000000000000..8bc29a618363 --- /dev/null +++ b/tools/public_api_guard/material/legacy-chips-testing.md @@ -0,0 +1,120 @@ +## API Report File for "components-srcs" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BaseHarnessFilters } from '@angular/cdk/testing'; +import { ComponentHarness } from '@angular/cdk/testing'; +import { ContentContainerComponentHarness } from '@angular/cdk/testing'; +import { HarnessPredicate } from '@angular/cdk/testing'; +import { TestKey } from '@angular/cdk/testing'; + +// @public +export interface ChipAvatarHarnessFilters extends BaseHarnessFilters { +} + +// @public +export interface ChipHarnessFilters extends BaseHarnessFilters { + // @deprecated + selected?: boolean; + text?: string | RegExp; +} + +// @public +export interface ChipInputHarnessFilters extends BaseHarnessFilters { + placeholder?: string | RegExp; + value?: string | RegExp; +} + +// @public +export interface ChipListboxHarnessFilters extends BaseHarnessFilters { +} + +// @public +export interface ChipListHarnessFilters extends BaseHarnessFilters { +} + +// @public +export interface ChipOptionHarnessFilters extends ChipHarnessFilters { + selected?: boolean; +} + +// @public +export interface ChipRemoveHarnessFilters extends BaseHarnessFilters { +} + +// @public +export class MatLegacyChipHarness extends ContentContainerComponentHarness { + // @deprecated + deselect(): Promise; + getAvatar(filter?: ChipAvatarHarnessFilters): Promise; + getRemoveButton(filter?: ChipRemoveHarnessFilters): Promise; + getText(): Promise; + static hostSelector: string; + isDisabled(): Promise; + // @deprecated + isSelected(): Promise; + remove(): Promise; + // @deprecated + select(): Promise; + // @deprecated + toggle(): Promise; + static with(options?: ChipHarnessFilters): HarnessPredicate; +} + +// @public +export class MatLegacyChipInputHarness extends ComponentHarness { + blur(): Promise; + focus(): Promise; + getPlaceholder(): Promise; + getValue(): Promise; + // (undocumented) + static hostSelector: string; + isDisabled(): Promise; + isFocused(): Promise; + isRequired(): Promise; + sendSeparatorKey(key: TestKey | string): Promise; + setValue(newValue: string): Promise; + static with(options?: ChipInputHarnessFilters): HarnessPredicate; +} + +// @public +export class MatLegacyChipListboxHarness extends _MatChipListHarnessBase { + getChips(filter?: ChipOptionHarnessFilters): Promise; + static hostSelector: string; + selectChips(filter?: ChipOptionHarnessFilters): Promise; + static with(options?: ChipListboxHarnessFilters): HarnessPredicate; +} + +// @public +export class MatLegacyChipListHarness extends _MatChipListHarnessBase { + getChips(filter?: ChipHarnessFilters): Promise; + getInput(filter?: ChipInputHarnessFilters): Promise; + static hostSelector: string; + // @deprecated + selectChips(filter?: ChipHarnessFilters): Promise; + static with(options?: ChipListHarnessFilters): HarnessPredicate; +} + +// @public (undocumented) +export class MatLegacyChipOptionHarness extends MatLegacyChipHarness { + deselect(): Promise; + static hostSelector: string; + isSelected(): Promise; + select(): Promise; + toggle(): Promise; + static with(options?: ChipOptionHarnessFilters): HarnessPredicate; +} + +// @public +export class MatLegacyChipRemoveHarness extends ComponentHarness { + click(): Promise; + // (undocumented) + static hostSelector: string; + static with(options?: ChipRemoveHarnessFilters): HarnessPredicate; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/tools/public_api_guard/material/legacy-chips.md b/tools/public_api_guard/material/legacy-chips.md new file mode 100644 index 000000000000..2b1f27148051 --- /dev/null +++ b/tools/public_api_guard/material/legacy-chips.md @@ -0,0 +1,331 @@ +## API Report File for "components-srcs" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { _AbstractConstructor } from '@angular/material/core'; +import { AfterContentInit } from '@angular/core'; +import { BooleanInput } from '@angular/cdk/coercion'; +import { CanColor } from '@angular/material/core'; +import { CanDisable } from '@angular/material/core'; +import { CanDisableRipple } from '@angular/material/core'; +import { CanUpdateErrorState } from '@angular/material/core'; +import { ChangeDetectorRef } from '@angular/core'; +import { _Constructor } from '@angular/material/core'; +import { ControlValueAccessor } from '@angular/forms'; +import { Directionality } from '@angular/cdk/bidi'; +import { DoCheck } from '@angular/core'; +import { ElementRef } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { EventEmitter } from '@angular/core'; +import { FocusableOption } from '@angular/cdk/a11y'; +import { FocusKeyManager } from '@angular/cdk/a11y'; +import { FormGroupDirective } from '@angular/forms'; +import { HasTabIndex } from '@angular/material/core'; +import * as i0 from '@angular/core'; +import * as i4 from '@angular/material/core'; +import { InjectionToken } from '@angular/core'; +import { MatLegacyFormFieldControl } from '@angular/material/legacy-form-field'; +import { NgControl } from '@angular/forms'; +import { NgForm } from '@angular/forms'; +import { NgZone } from '@angular/core'; +import { Observable } from 'rxjs'; +import { OnChanges } from '@angular/core'; +import { OnDestroy } from '@angular/core'; +import { OnInit } from '@angular/core'; +import { Platform } from '@angular/cdk/platform'; +import { QueryList } from '@angular/core'; +import { RippleConfig } from '@angular/material/core'; +import { RippleGlobalOptions } from '@angular/material/core'; +import { RippleTarget } from '@angular/material/core'; +import { SelectionModel } from '@angular/cdk/collections'; +import { Subject } from 'rxjs'; + +// @public +export const MAT_CHIP_AVATAR: InjectionToken; + +// @public +export const MAT_CHIP_REMOVE: InjectionToken; + +// @public +export const MAT_CHIP_TRAILING_ICON: InjectionToken; + +// @public +export const MAT_CHIPS_DEFAULT_OPTIONS: InjectionToken; + +// @public +export class MatLegacyChip extends _MatChipMixinBase implements FocusableOption, OnDestroy, CanColor, CanDisableRipple, RippleTarget, HasTabIndex, CanDisable { + constructor(elementRef: ElementRef, _ngZone: NgZone, platform: Platform, globalRippleOptions: RippleGlobalOptions | null, _changeDetectorRef: ChangeDetectorRef, _document: any, animationMode?: string, tabIndex?: string); + // (undocumented) + _addHostClassName(): void; + _animationsDisabled: boolean; + get ariaSelected(): string | null; + avatar: MatLegacyChipAvatar; + // (undocumented) + _blur(): void; + _chipListDisabled: boolean; + _chipListMultiple: boolean; + chipListSelectable: boolean; + deselect(): void; + readonly destroyed: EventEmitter; + get disabled(): boolean; + set disabled(value: BooleanInput); + // (undocumented) + protected _disabled: boolean; + focus(): void; + _handleClick(event: Event): void; + _handleKeydown(event: KeyboardEvent): void; + _hasFocus: boolean; + // (undocumented) + ngOnDestroy(): void; + readonly _onBlur: Subject; + readonly _onFocus: Subject; + get removable(): boolean; + set removable(value: BooleanInput); + // (undocumented) + protected _removable: boolean; + remove(): void; + readonly removed: EventEmitter; + removeIcon: MatLegacyChipRemove; + rippleConfig: RippleConfig & RippleGlobalOptions; + get rippleDisabled(): boolean; + role: string; + select(): void; + get selectable(): boolean; + set selectable(value: BooleanInput); + // (undocumented) + protected _selectable: boolean; + get selected(): boolean; + set selected(value: BooleanInput); + // (undocumented) + protected _selected: boolean; + readonly selectionChange: EventEmitter; + selectViaInteraction(): void; + toggleSelected(isUserInput?: boolean): boolean; + trailingIcon: MatLegacyChipTrailingIcon; + get value(): any; + set value(value: any); + // (undocumented) + protected _value: any; + // (undocumented) + static ɵdir: i0.ɵɵDirectiveDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public +export class MatLegacyChipAvatar { + // (undocumented) + static ɵdir: i0.ɵɵDirectiveDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public +export interface MatLegacyChipEvent { + chip: MatLegacyChip; +} + +// @public +export class MatLegacyChipInput implements MatLegacyChipTextControl, OnChanges, OnDestroy, AfterContentInit { + constructor(_elementRef: ElementRef, _defaultOptions: MatLegacyChipsDefaultOptions); + get addOnBlur(): boolean; + set addOnBlur(value: BooleanInput); + // (undocumented) + _addOnBlur: boolean; + _blur(): void; + readonly chipEnd: EventEmitter; + set chipList(value: MatLegacyChipList); + // (undocumented) + _chipList: MatLegacyChipList; + clear(): void; + get disabled(): boolean; + set disabled(value: BooleanInput); + // (undocumented) + protected _elementRef: ElementRef; + _emitChipEnd(event?: KeyboardEvent): void; + get empty(): boolean; + focus(options?: FocusOptions): void; + // (undocumented) + _focus(): void; + focused: boolean; + id: string; + readonly inputElement: HTMLInputElement; + _keydown(event?: KeyboardEvent): void; + _keyup(event: KeyboardEvent): void; + // (undocumented) + ngAfterContentInit(): void; + // (undocumented) + ngOnChanges(): void; + // (undocumented) + ngOnDestroy(): void; + // (undocumented) + _onInput(): void; + placeholder: string; + separatorKeyCodes: readonly number[] | ReadonlySet; + // (undocumented) + static ɵdir: i0.ɵɵDirectiveDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public +export interface MatLegacyChipInputEvent { + chipInput: MatLegacyChipInput; + // @deprecated + input: HTMLInputElement; + value: string; +} + +// @public +export class MatLegacyChipList extends _MatChipListBase implements MatLegacyFormFieldControl, ControlValueAccessor, AfterContentInit, DoCheck, OnInit, OnDestroy, CanUpdateErrorState { + constructor(_elementRef: ElementRef, _changeDetectorRef: ChangeDetectorRef, _dir: Directionality, _parentForm: NgForm, _parentFormGroup: FormGroupDirective, _defaultErrorStateMatcher: ErrorStateMatcher, ngControl: NgControl); + _allowFocusEscape(): void; + ariaOrientation: 'horizontal' | 'vertical'; + _blur(): void; + readonly change: EventEmitter; + get chipBlurChanges(): Observable; + get chipFocusChanges(): Observable; + protected _chipInput: MatLegacyChipTextControl; + get chipRemoveChanges(): Observable; + chips: QueryList; + get chipSelectionChanges(): Observable; + get compareWith(): (o1: any, o2: any) => boolean; + set compareWith(fn: (o1: any, o2: any) => boolean); + readonly controlType: string; + get disabled(): boolean; + set disabled(value: BooleanInput); + // (undocumented) + protected _disabled: boolean; + // (undocumented) + protected _elementRef: ElementRef; + get empty(): boolean; + errorStateMatcher: ErrorStateMatcher; + focus(options?: FocusOptions): void; + get focused(): boolean; + _focusInput(options?: FocusOptions): void; + get id(): string; + _keydown(event: KeyboardEvent): void; + _keyManager: FocusKeyManager; + _markAsTouched(): void; + get multiple(): boolean; + set multiple(value: BooleanInput); + // (undocumented) + ngAfterContentInit(): void; + // (undocumented) + ngDoCheck(): void; + // (undocumented) + ngOnDestroy(): void; + // (undocumented) + ngOnInit(): void; + _onChange: (value: any) => void; + onContainerClick(event: MouseEvent): void; + _onTouched: () => void; + get placeholder(): string; + set placeholder(value: string); + // (undocumented) + protected _placeholder: string; + registerInput(inputElement: MatLegacyChipTextControl): void; + // (undocumented) + registerOnChange(fn: (value: any) => void): void; + // (undocumented) + registerOnTouched(fn: () => void): void; + get required(): boolean; + set required(value: BooleanInput); + // (undocumented) + protected _required: boolean | undefined; + get role(): string | null; + set role(role: string | null); + get selectable(): boolean; + set selectable(value: BooleanInput); + // (undocumented) + protected _selectable: boolean; + get selected(): MatLegacyChip[] | MatLegacyChip; + // (undocumented) + _selectionModel: SelectionModel; + setDescribedByIds(ids: string[]): void; + // (undocumented) + setDisabledState(isDisabled: boolean): void; + // (undocumented) + _setSelectionByValue(value: any, isUserInput?: boolean): void; + get shouldLabelFloat(): boolean; + // (undocumented) + set tabIndex(value: number); + _tabIndex: number; + _uid: string; + protected _updateFocusForDestroyedChips(): void; + protected _updateTabIndex(): void; + userAriaDescribedBy: string; + _userTabIndex: number | null; + get value(): any; + set value(value: any); + // (undocumented) + protected _value: any; + readonly valueChange: EventEmitter; + // (undocumented) + writeValue(value: any): void; + // (undocumented) + static ɵcmp: i0.ɵɵComponentDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public +export class MatLegacyChipListChange { + constructor( + source: MatLegacyChipList, + value: any); + source: MatLegacyChipList; + value: any; +} + +// @public +export class MatLegacyChipRemove { + constructor(_parentChip: MatLegacyChip, elementRef: ElementRef); + _handleClick(event: Event): void; + // (undocumented) + protected _parentChip: MatLegacyChip; + // (undocumented) + static ɵdir: i0.ɵɵDirectiveDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public +export interface MatLegacyChipsDefaultOptions { + separatorKeyCodes: readonly number[] | ReadonlySet; +} + +// @public +export class MatLegacyChipSelectionChange { + constructor( + source: MatLegacyChip, + selected: boolean, + isUserInput?: boolean); + isUserInput: boolean; + selected: boolean; + source: MatLegacyChip; +} + +// @public (undocumented) +export class MatLegacyChipsModule { + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; + // (undocumented) + static ɵinj: i0.ɵɵInjectorDeclaration; + // (undocumented) + static ɵmod: i0.ɵɵNgModuleDeclaration; +} + +// @public +export class MatLegacyChipTrailingIcon { + // (undocumented) + static ɵdir: i0.ɵɵDirectiveDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// (No @packageDocumentation comment for this package) + +```