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;