Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 65 additions & 14 deletions packages/components/tags/tag-list.component.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -119,7 +121,7 @@ export class TestTagList {
@Component({
standalone: true,
selector: 'test-form-field-tag-list',
imports: [KbqTagsModule, KbqFormFieldModule],
imports: [KbqTagsModule, KbqFormFieldModule, CdkMonitorFocus],
template: `
<kbq-form-field>
<kbq-tag-list #tagList="kbqTagList">
Expand All @@ -129,7 +131,7 @@ export class TestTagList {
</kbq-tag>
}

<input [kbqTagInputFor]="tagList" />
<input cdkMonitorElementFocus [kbqTagInputFor]="tagList" />
</kbq-tag-list>
</kbq-form-field>
`,
Expand All @@ -143,10 +145,6 @@ export class TestFormFieldTagList {
selected: false
}))
);

readonly tagInputElementRef: Signal<ElementRef<HTMLInputElement>> = viewChild.required(KbqTagInput, {
read: ElementRef
});
}

describe(KbqTagList.name, () => {
Expand All @@ -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();
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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({
Expand Down
48 changes: 17 additions & 31 deletions packages/components/tags/tag-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
artembelik marked this conversation as resolved.

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();
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 6 additions & 6 deletions packages/components/tags/tag.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ describe(KbqTag.name, () => {
tagInstance.disabled = true;
fixture.detectChanges();

const event = dispatchFakeEvent(tagNativeElement, 'mousedown');
const event = dispatchFakeEvent(tagNativeElement, 'click');

fixture.detectChanges();

Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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();
});
Expand All @@ -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();
});
Expand All @@ -657,15 +657,15 @@ 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();
});

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 })
Expand Down
19 changes: 14 additions & 5 deletions packages/components/tags/tag.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 */
Expand Down
4 changes: 2 additions & 2 deletions tools/public_api_guard/components/tags.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ export class KbqTag extends KbqColorDirective implements IFocusableOption, OnDes
protected readonly editing: WritableSignal<boolean>;
readonly elementRef: ElementRef<HTMLElement>;
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)
Expand Down Expand Up @@ -286,7 +286,7 @@ export class KbqTagList implements KbqFormFieldControl<any>, ControlValueAccesso
// (undocumented)
ngOnDestroy(): void;
onChange: (value: any) => void;
onContainerClick(event: MouseEvent): void;
onContainerClick(): void;
onTouched: () => void;
// @deprecated
orientation: KbqOrientation;
Expand Down