diff --git a/packages/components/tags/tag-list.component.spec.ts b/packages/components/tags/tag-list.component.spec.ts index 477bec197f..4c6618de50 100644 --- a/packages/components/tags/tag-list.component.spec.ts +++ b/packages/components/tags/tag-list.component.spec.ts @@ -1,16 +1,14 @@ import { animate, style, transition, trigger } from '@angular/animations'; -import { FocusMonitor } from '@angular/cdk/a11y'; +import { CdkMonitorFocus, FocusMonitor } from '@angular/cdk/a11y'; import { Direction, Directionality } from '@angular/cdk/bidi'; import { A } from '@angular/cdk/keycodes'; import { ChangeDetectionStrategy, Component, DebugElement, - ElementRef, NgZone, Provider, QueryList, - Signal, Type, ViewChild, ViewChildren, @@ -69,6 +67,10 @@ const getSelectedTags = (debugElement: DebugElement): HTMLElement[] => { return getTagElements(debugElement).filter((tag) => tag.classList.contains('kbq-selected')); }; +const getTagInputElement = (debugElement: DebugElement): HTMLInputElement => { + return debugElement.query(By.directive(KbqTagInput)).nativeElement; +}; + const getFocusMonitor = () => TestBed.inject(FocusMonitor); @Component({ @@ -119,7 +121,7 @@ export class TestTagList { @Component({ standalone: true, selector: 'test-form-field-tag-list', - imports: [KbqTagsModule, KbqFormFieldModule], + imports: [KbqTagsModule, KbqFormFieldModule, CdkMonitorFocus], template: ` @@ -129,7 +131,7 @@ export class TestTagList { } - + `, @@ -143,10 +145,6 @@ export class TestFormFieldTagList { selected: false })) ); - - readonly tagInputElementRef: Signal> = viewChild.required(KbqTagInput, { - read: ElementRef - }); } describe(KbqTagList.name, () => { @@ -170,10 +168,6 @@ describe(KbqTagList.name, () => { expect(tagListNativeElement.classList).toContain('kbq-tag-list'); }); - xit('height should be 24px', () => { - expect(tagListNativeElement.getBoundingClientRect().height).toBe(24); - }); - it('should not have the aria-selected attribute when is not selectable', () => { testComponent.selectable = false; fixture.detectChanges(); @@ -1326,7 +1320,7 @@ describe(KbqTagList.name, () => { expect(getSelectedTags(debugElement).length).toBe(2); - getFocusMonitor().focusVia(componentInstance.tagInputElementRef().nativeElement, 'mouse'); + getFocusMonitor().focusVia(getTagInputElement(debugElement), 'mouse'); expect(getSelectedTags(debugElement).length).toBe(0); }); @@ -1381,6 +1375,63 @@ describe(KbqTagList.name, () => { expect(componentInstance.selectionChange).toHaveBeenCalledWith(expect.objectContaining({ selected: false })); }); + + it('should focus tag input on container click', () => { + const fixture = createStandaloneComponent(TestFormFieldTagList); + const { debugElement } = fixture; + const input = getTagInputElement(debugElement); + + expect(input.classList).not.toContain('cdk-focused'); + + getTagListElement(debugElement).click(); + + expect(input.classList).toContain('cdk-focused'); + }); + + it('should focus tag input on tag click', async () => { + const fixture = createStandaloneComponent(TestFormFieldTagList); + const { debugElement } = fixture; + const input = getTagInputElement(debugElement); + + expect(input.classList).not.toContain('cdk-focused'); + + getFirstTagElement(debugElement).click(); + + expect(input.classList).toContain('cdk-focused'); + }); + + it('should NOT focus tag input on tag click with ctrl/meta/shift key', async () => { + const fixture = createStandaloneComponent(TestFormFieldTagList); + const { debugElement } = fixture; + const input = getTagInputElement(debugElement); + const tag = getFirstTagElement(debugElement); + + expect(input.classList).not.toContain('cdk-focused'); + + tag.dispatchEvent(new MouseEvent('click', { ctrlKey: true })); + + expect(input.classList).not.toContain('cdk-focused'); + + tag.dispatchEvent(new MouseEvent('click', { metaKey: true })); + + expect(input.classList).not.toContain('cdk-focused'); + + tag.dispatchEvent(new MouseEvent('click', { shiftKey: true })); + + expect(input.classList).not.toContain('cdk-focused'); + }); + + it('should focus tag input on focus', () => { + const fixture = createStandaloneComponent(TestFormFieldTagList); + const { debugElement } = fixture; + const input = getTagInputElement(debugElement); + + expect(input.classList).not.toContain('cdk-focused'); + + getTagListElement(debugElement).focus(); + + expect(input.classList).toContain('cdk-focused'); + }); }); @Component({ diff --git a/packages/components/tags/tag-list.component.ts b/packages/components/tags/tag-list.component.ts index 64a425494d..55277d7087 100644 --- a/packages/components/tags/tag-list.component.ts +++ b/packages/components/tags/tag-list.component.ts @@ -580,35 +580,36 @@ export class KbqTagList * * @docs-private */ - onContainerClick(event: MouseEvent) { - if (!this.originatesFromTag(event)) { - this.focus(); - } + onContainerClick(): void { + this.focus(); } /** - * Focuses the first non-disabled tag in this tag list, or the associated input when there - * are no eligible tags. + * Focuses the tag list. If there is a tag input, focuses that instead. */ focus(): void { - if (this.disabled) { + if (this.disabled) return; + + if (this.tagInput) { + this.focusInput(); + this.stateChanges.next(); + return; } - // TODO: ARIA says this should focus the first `selected` tag if any are selected. (#DS-3740) - // Focus on first element if there's no tagInput inside tag-list - if (this.tagInput && this.tagInput.focused) { - // do nothing - } else if (this.tags.length > 0) { + if (this.tags.length > 0) { this.keyManager.setFirstItemActive(); this.stateChanges.next(); - } else { - this.focusInput(); - this.stateChanges.next(); + + return; } } - /** Attempt to focus an input if we have one. */ + /** + * Focuses the tag input inside the tag list. + * + * @docs-private + */ focusInput() { if (this.tagInput) { this.tagInput.focus(); @@ -849,21 +850,6 @@ export class KbqTagList .subscribe(() => this.propagateTagsChanges()); } - /** Checks whether an event comes from inside a tag element. */ - private originatesFromTag(event: Event): boolean { - let currentElement = event.target as HTMLElement | null; - - while (currentElement && currentElement !== this.elementRef.nativeElement) { - if (this.isTagElement(currentElement)) { - return true; - } - - currentElement = currentElement.parentElement; - } - - return false; - } - /** Checks whether any of the tags is focused. */ private hasFocusedTag() { return this.tags.some((tag) => tag.hasFocus); diff --git a/packages/components/tags/tag.component.spec.ts b/packages/components/tags/tag.component.spec.ts index 02d54865f7..751f2e6dec 100644 --- a/packages/components/tags/tag.component.spec.ts +++ b/packages/components/tags/tag.component.spec.ts @@ -255,7 +255,7 @@ describe(KbqTag.name, () => { tagInstance.disabled = true; fixture.detectChanges(); - const event = dispatchFakeEvent(tagNativeElement, 'mousedown'); + const event = dispatchFakeEvent(tagNativeElement, 'click'); fixture.detectChanges(); @@ -558,7 +558,7 @@ describe(KbqTag.name, () => { const { debugElement } = createComponent(TestTag); const tag = getTagElement(debugElement); - tag.dispatchEvent(new MouseEvent('mousedown', { ctrlKey: true })); + tag.dispatchEvent(new MouseEvent('click', { ctrlKey: true })); expect(isTagSelected(debugElement)).toBeTruthy(); }); @@ -635,7 +635,7 @@ describe(KbqTag.name, () => { const { debugElement } = createComponent(TestTag); const tag = getTagElement(debugElement); - tag.dispatchEvent(new MouseEvent('mousedown', { metaKey: true })); + tag.dispatchEvent(new MouseEvent('click', { metaKey: true })); expect(isTagSelected(debugElement)).toBeTruthy(); }); @@ -644,7 +644,7 @@ describe(KbqTag.name, () => { const { debugElement } = createComponent(TestTag); const tag = getTagElement(debugElement); - tag.dispatchEvent(new MouseEvent('mousedown', { shiftKey: true })); + tag.dispatchEvent(new MouseEvent('click', { shiftKey: true })); expect(isTagSelected(debugElement)).toBeTruthy(); }); @@ -657,7 +657,7 @@ describe(KbqTag.name, () => { componentInstance.selectable.set(false); fixture.detectChanges(); - tag.dispatchEvent(new MouseEvent('mousedown', { ctrlKey: true })); + tag.dispatchEvent(new MouseEvent('click', { ctrlKey: true })); expect(isTagSelected(debugElement)).toBeFalsy(); }); @@ -665,7 +665,7 @@ describe(KbqTag.name, () => { it('should emit KbqTagSelectionChange event on Ctrl + click', () => { const { debugElement, componentInstance } = createComponent(TestTag); - getTagElement(debugElement).dispatchEvent(new MouseEvent('mousedown', { ctrlKey: true })); + getTagElement(debugElement).dispatchEvent(new MouseEvent('click', { ctrlKey: true })); expect(componentInstance.selectionChange).toHaveBeenCalledWith( expect.objectContaining({ selected: true, isUserInput: true }) diff --git a/packages/components/tags/tag.component.ts b/packages/components/tags/tag.component.ts index 52f0a5a456..d976738133 100644 --- a/packages/components/tags/tag.component.ts +++ b/packages/components/tags/tag.component.ts @@ -201,7 +201,7 @@ export class KbqTagEditInput { '[class.kbq-tag_draggable]': 'draggable', '(dblclick)': 'handleDblClick($event)', - '(mousedown)': 'handleMousedown($event)', + '(click)': 'handleClick($event)', '(keydown)': 'handleKeydown($event)' }, changeDetection: ChangeDetectionStrategy.OnPush, @@ -511,12 +511,21 @@ export class KbqTag } /** @docs-private */ - handleMousedown(event: MouseEvent): void { - if (this.disabled || (!this.selectable && !this.editable) || this.editing()) return event.preventDefault(); + handleClick(event: MouseEvent): void { + if (this.disabled || this.editing()) { + event.preventDefault(); - if (hasModifierKey(event, 'metaKey', 'ctrlKey', 'shiftKey') && this.selectable) this.toggleSelected(true); + return; + } - event.stopPropagation(); + if (this.selectable && hasModifierKey(event, 'metaKey', 'ctrlKey', 'shiftKey')) { + this.toggleSelected(true); + + // We should stop event propagation to prevent the tag list from handling the click event. + event.stopPropagation(); + + return; + } } /** @docs-private */ diff --git a/tools/public_api_guard/components/tags.api.md b/tools/public_api_guard/components/tags.api.md index eaebdc14b0..f751b4042a 100644 --- a/tools/public_api_guard/components/tags.api.md +++ b/tools/public_api_guard/components/tags.api.md @@ -73,9 +73,9 @@ export class KbqTag extends KbqColorDirective implements IFocusableOption, OnDes protected readonly editing: WritableSignal; readonly elementRef: ElementRef; focus(): void; + handleClick(event: MouseEvent): void; protected handleDblClick(event: MouseEvent): void; handleKeydown(event: KeyboardEvent): void; - handleMousedown(event: MouseEvent): void; hasFocus: boolean; readonly nativeElement: HTMLElement; // (undocumented) @@ -286,7 +286,7 @@ export class KbqTagList implements KbqFormFieldControl, ControlValueAccesso // (undocumented) ngOnDestroy(): void; onChange: (value: any) => void; - onContainerClick(event: MouseEvent): void; + onContainerClick(): void; onTouched: () => void; // @deprecated orientation: KbqOrientation;