From 9cf4a6dd8403d2c047aa8b6cd4644c16361aa2a6 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 20 Mar 2025 09:55:13 +0100 Subject: [PATCH] fix(material/chips): implement disabledInteractive in chip input Adds a `disabledInteractive` input to `MatChipInput`, similar to what we have in other components. I've also cleaned up the chips demo a bit. --- goldens/material/chips/index.api.md | 10 +++- src/dev-app/chips/BUILD.bazel | 1 - src/dev-app/chips/chips-demo.html | 54 ++++++++----------- src/dev-app/chips/chips-demo.ts | 3 +- src/material/chips/chip-input.spec.ts | 35 +++++++++--- src/material/chips/chip-input.ts | 18 ++++++- src/material/chips/testing/BUILD.bazel | 3 ++ .../chips/testing/chip-input-harness.spec.ts | 14 ++++- .../chips/testing/chip-input-harness.ts | 10 +++- src/material/chips/tokens.ts | 3 ++ 10 files changed, 106 insertions(+), 45 deletions(-) diff --git a/goldens/material/chips/index.api.md b/goldens/material/chips/index.api.md index 2cc8f03b4191..a46a68404f9f 100644 --- a/goldens/material/chips/index.api.md +++ b/goldens/material/chips/index.api.md @@ -252,6 +252,7 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy { clear(): void; get disabled(): boolean; set disabled(value: boolean); + disabledInteractive: boolean; // (undocumented) protected _elementRef: ElementRef; _emitChipEnd(event?: KeyboardEvent): void; @@ -260,6 +261,7 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy { // (undocumented) _focus(): void; focused: boolean; + protected _getReadonlyAttribute(): string | null; id: string; readonly inputElement: HTMLInputElement; _keydown(event: KeyboardEvent): void; @@ -268,17 +270,22 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy { // (undocumented) static ngAcceptInputType_disabled: unknown; // (undocumented) + static ngAcceptInputType_disabledInteractive: unknown; + // (undocumented) + static ngAcceptInputType_readonly: unknown; + // (undocumented) ngOnChanges(): void; // (undocumented) ngOnDestroy(): void; // (undocumented) _onInput(): void; placeholder: string; + readonly: boolean; separatorKeyCodes: readonly number[] | ReadonlySet; // (undocumented) setDescribedByIds(ids: string[]): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -433,6 +440,7 @@ export class MatChipRow extends MatChip implements AfterViewInit { // @public export interface MatChipsDefaultOptions { hideSingleSelectionIndicator?: boolean; + inputDisabledInteractive?: boolean; separatorKeyCodes: readonly number[] | ReadonlySet; } diff --git a/src/dev-app/chips/BUILD.bazel b/src/dev-app/chips/BUILD.bazel index 61a3d80ab3eb..7fd5d0b6acd5 100644 --- a/src/dev-app/chips/BUILD.bazel +++ b/src/dev-app/chips/BUILD.bazel @@ -17,7 +17,6 @@ ng_module( "//src/material/core", "//src/material/form-field", "//src/material/icon", - "//src/material/toolbar", ], ) diff --git a/src/dev-app/chips/chips-demo.html b/src/dev-app/chips/chips-demo.html index c1dd192f7534..4b4b169ba162 100644 --- a/src/dev-app/chips/chips-demo.html +++ b/src/dev-app/chips/chips-demo.html @@ -1,6 +1,6 @@
- Static Chips + Static Chips

Simple

@@ -111,26 +111,24 @@

With Events

- Selectable Chips + Chip Listbox - - +

Chip list utilizing the listbox pattern. Should be used for selectable chips.

+ + Disabled + Show avatar

Single selection

@for (shirtSize of shirtSizes; track shirtSize) { - - {{shirtSize.label}} - @if (listboxesWithAvatar) { - {{shirtSize.avatar}} - } - + + {{shirtSize.label}} + @if (listboxesWithAvatar) { + {{shirtSize.avatar}} + } + } @@ -151,7 +149,7 @@

Multi selection

- Input Chips + Chip Grid

@@ -160,13 +158,9 @@

Multi selection

They can be used inside a <mat-form-field>.

- - - + Disabled + Editable + Disabled Interactive

Input is last child of chip grid

@@ -188,19 +182,15 @@

Input is last child of chip grid

[matChipInputFor]="chipGrid1" [matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="addOnBlur" + [matChipInputDisabledInteractive]="disabledInteractive" (matChipInputTokenEnd)="add($event)" - aria-label="New contributor input..." /> + placeholder="Add a contributor"/> -

Input is next sibling child of chip grid

- + New Contributor... @for (person of people; track person) { @@ -215,7 +205,9 @@

Input is next sibling child of chip grid

+ [matChipInputDisabledInteractive]="disabledInteractive" + (matChipInputTokenEnd)="add($event)" + placeholder="Add a contributor"/>

@@ -232,7 +224,7 @@

Options

- Miscellaneous + Miscellaneous

Stacked

diff --git a/src/dev-app/chips/chips-demo.ts b/src/dev-app/chips/chips-demo.ts index 63fd21b755ea..13dd012ed549 100644 --- a/src/dev-app/chips/chips-demo.ts +++ b/src/dev-app/chips/chips-demo.ts @@ -17,7 +17,6 @@ import {MatChipEditedEvent, MatChipInputEvent, MatChipsModule} from '@angular/ma import {ThemePalette} from '@angular/material/core'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MatIconModule} from '@angular/material/icon'; -import {MatToolbarModule} from '@angular/material/toolbar'; export interface Person { name: string; @@ -40,7 +39,6 @@ export interface DemoColor { MatChipsModule, MatFormFieldModule, MatIconModule, - MatToolbarModule, ReactiveFormsModule, ], changeDetection: ChangeDetectionStrategy.OnPush, @@ -54,6 +52,7 @@ export class ChipsDemo { listboxesWithAvatar = false; disableInputs = false; editable = false; + disabledInteractive = false; message = ''; shirtSizes = [ diff --git a/src/material/chips/chip-input.spec.ts b/src/material/chips/chip-input.spec.ts index b227d11fae30..36033b9e38e0 100644 --- a/src/material/chips/chip-input.spec.ts +++ b/src/material/chips/chip-input.spec.ts @@ -22,10 +22,10 @@ import { } from './index'; describe('MatChipInput', () => { - let fixture: ComponentFixture; + let fixture: ComponentFixture; let testChipInput: TestChipInput; let inputDebugElement: DebugElement; - let inputNativeElement: HTMLElement; + let inputNativeElement: HTMLInputElement; let chipInputDirective: MatChipInput; let dir = 'ltr'; @@ -87,10 +87,28 @@ describe('MatChipInput', () => { fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - expect(inputNativeElement.getAttribute('disabled')).toBe('true'); + expect(inputNativeElement.disabled).toBe(true); expect(chipInputDirective.disabled).toBe(true); }); + it('should be able to set an input as being disabled and interactive', fakeAsync(() => { + fixture.componentInstance.chipGridInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + expect(inputNativeElement.disabled).toBe(true); + expect(inputNativeElement.readOnly).toBe(false); + expect(inputNativeElement.hasAttribute('aria-disabled')).toBe(false); + + fixture.componentInstance.disabledInteractive = true; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + expect(inputNativeElement.disabled).toBe(false); + expect(inputNativeElement.readOnly).toBe(true); + expect(inputNativeElement.getAttribute('aria-disabled')).toBe('true'); + })); + it('should be aria-required if the list is required', () => { expect(inputNativeElement.hasAttribute('aria-required')).toBe(false); @@ -274,10 +292,12 @@ describe('MatChipInput', () => { Hello - + `, @@ -288,6 +308,7 @@ class TestChipInput { addOnBlur: boolean = false; placeholder = ''; required = false; + disabledInteractive = false; add(_: MatChipInputEvent) {} } diff --git a/src/material/chips/chip-input.ts b/src/material/chips/chip-input.ts index 1fd8c53395ee..e02a5ea5b98e 100644 --- a/src/material/chips/chip-input.ts +++ b/src/material/chips/chip-input.ts @@ -57,10 +57,12 @@ export interface MatChipInputEvent { '(focus)': '_focus()', '(input)': '_onInput()', '[id]': 'id', - '[attr.disabled]': 'disabled || null', + '[attr.disabled]': 'disabled && !disabledInteractive ? "" : null', '[attr.placeholder]': 'placeholder || null', '[attr.aria-invalid]': '_chipGrid && _chipGrid.ngControl ? _chipGrid.ngControl.invalid : null', '[attr.aria-required]': '_chipGrid && _chipGrid.required || null', + '[attr.aria-disabled]': 'disabled && disabledInteractive ? "true" : null', + '[attr.readonly]': '_getReadonlyAttribute()', '[attr.required]': '_chipGrid && _chipGrid.required || null', }, }) @@ -117,6 +119,14 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy { } private _disabled: boolean = false; + /** Whether the input is readonly. */ + @Input({transform: booleanAttribute}) + readonly: boolean = false; + + /** Whether the input should remain interactive when it is disabled. */ + @Input({alias: 'matChipInputDisabledInteractive', transform: booleanAttribute}) + disabledInteractive: boolean; + /** Whether the input is empty. */ get empty(): boolean { return !this.inputElement.value; @@ -133,6 +143,7 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy { this.inputElement = this._elementRef.nativeElement as HTMLInputElement; this.separatorKeyCodes = defaultOptions.separatorKeyCodes; + this.disabledInteractive = defaultOptions.inputDisabledInteractive ?? false; if (formField) { this.inputElement.classList.add('mat-mdc-form-field-input-control'); @@ -223,4 +234,9 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy { private _isSeparatorKey(event: KeyboardEvent) { return !hasModifierKey(event) && new Set(this.separatorKeyCodes).has(event.keyCode); } + + /** Gets the value to set on the `readonly` attribute. */ + protected _getReadonlyAttribute(): string | null { + return this.readonly || (this.disabled && this.disabledInteractive) ? 'true' : null; + } } diff --git a/src/material/chips/testing/BUILD.bazel b/src/material/chips/testing/BUILD.bazel index 4f2af8c82f58..1e2f0628a3d5 100644 --- a/src/material/chips/testing/BUILD.bazel +++ b/src/material/chips/testing/BUILD.bazel @@ -9,6 +9,9 @@ ts_project( ["**/*.ts"], exclude = ["**/*.spec.ts"], ), + interop_deps = [ + "//src/cdk/coercion", + ], deps = [ "//src/cdk/testing:testing_rjs", ], diff --git a/src/material/chips/testing/chip-input-harness.spec.ts b/src/material/chips/testing/chip-input-harness.spec.ts index 2172c1b1aa18..deaded9d8547 100644 --- a/src/material/chips/testing/chip-input-harness.spec.ts +++ b/src/material/chips/testing/chip-input-harness.spec.ts @@ -38,6 +38,14 @@ describe('MatChipInputHarness', () => { expect(await harnesses[1].isDisabled()).toBe(true); }); + it('should get the disabled state when disabledInteractive is enabled', async () => { + fixture.componentInstance.disabledInteractive = true; + fixture.changeDetectorRef.markForCheck(); + const harnesses = await loader.getAllHarnesses(MatChipInputHarness); + expect(await harnesses[0].isDisabled()).toBe(false); + expect(await harnesses[1].isDisabled()).toBe(true); + }); + it('should get whether the input is required', async () => { const harness = await loader.getHarness(MatChipInputHarness); expect(await harness.isRequired()).toBe(false); @@ -91,7 +99,10 @@ describe('MatChipInputHarness', () => { - + `, imports: [MatChipsModule], @@ -100,4 +111,5 @@ class ChipInputHarnessTest { required = false; add = jasmine.createSpy('add spy'); separatorKeyCodes = [COMMA]; + disabledInteractive = false; } diff --git a/src/material/chips/testing/chip-input-harness.ts b/src/material/chips/testing/chip-input-harness.ts index fbf773ce7930..951f401faf46 100644 --- a/src/material/chips/testing/chip-input-harness.ts +++ b/src/material/chips/testing/chip-input-harness.ts @@ -12,6 +12,7 @@ import { HarnessPredicate, TestKey, } from '@angular/cdk/testing'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {ChipInputHarnessFilters} from './chip-harness-filters'; /** Harness for interacting with a grid's chip input in tests. */ @@ -42,7 +43,14 @@ export class MatChipInputHarness extends ComponentHarness { /** Whether the input is disabled. */ async isDisabled(): Promise { - return (await this.host()).getProperty('disabled'); + const host = await this.host(); + const disabled = await host.getAttribute('disabled'); + + if (disabled !== null) { + return coerceBooleanProperty(disabled); + } + + return (await host.getAttribute('aria-disabled')) === 'true'; } /** Whether the input is required. */ diff --git a/src/material/chips/tokens.ts b/src/material/chips/tokens.ts index 9e70ec83fbf1..9c90f0253a0d 100644 --- a/src/material/chips/tokens.ts +++ b/src/material/chips/tokens.ts @@ -16,6 +16,9 @@ export interface MatChipsDefaultOptions { /** Whether icon indicators should be hidden for single-selection. */ hideSingleSelectionIndicator?: boolean; + + /** Whether the chip input should be interactive while disabled by default. */ + inputDisabledInteractive?: boolean; } /** Injection token to be used to override the default options for the chips module. */