diff --git a/.changeset/large-cherries-attend.md b/.changeset/large-cherries-attend.md new file mode 100644 index 00000000000..d5e9e3dde26 --- /dev/null +++ b/.changeset/large-cherries-attend.md @@ -0,0 +1,7 @@ +--- +"@siemens/ix-angular": minor +"@siemens/ix": minor +"@siemens/ix-vue": minor +--- + +Add filter cleared event to ix-categroy-filter. diff --git a/packages/angular/src/components.ts b/packages/angular/src/components.ts index 9bdf692545d..ebf9c076bf5 100644 --- a/packages/angular/src/components.ts +++ b/packages/angular/src/components.ts @@ -364,7 +364,7 @@ export class IxCategoryFilter { constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { c.detach(); this.el = r.nativeElement; - proxyOutputs(this, this.el, ['categoryChanged', 'inputChanged', 'filterChanged']); + proxyOutputs(this, this.el, ['categoryChanged', 'inputChanged', 'filterChanged', 'filterCleared']); } } @@ -385,6 +385,10 @@ export declare interface IxCategoryFilter extends Components.IxCategoryFilter { * Event dispatched whenever the filter state changes. */ filterChanged: EventEmitter>; + /** + * Event dispatched whenever the filter gets cleared. + */ + filterCleared: EventEmitter>; } diff --git a/packages/core/component-doc.json b/packages/core/component-doc.json index acb8809047a..c46111d19e5 100644 --- a/packages/core/component-doc.json +++ b/packages/core/component-doc.json @@ -2490,6 +2490,20 @@ "docs": "Event dispatched whenever the filter state changes.", "docsTags": [] }, + { + "event": "filterCleared", + "detail": "void", + "bubbles": true, + "complexType": { + "original": "void", + "resolved": "void", + "references": {} + }, + "cancelable": true, + "composed": true, + "docs": "Event dispatched whenever the filter gets cleared.", + "docsTags": [] + }, { "event": "inputChanged", "detail": "InputState", diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index b2f7c07df19..fda110ca33e 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -3630,6 +3630,7 @@ declare global { "categoryChanged": string; "inputChanged": InputState; "filterChanged": FilterState; + "filterCleared": void; } interface HTMLIxCategoryFilterElement extends Components.IxCategoryFilter, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLIxCategoryFilterElement, ev: IxCategoryFilterCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -5450,6 +5451,10 @@ declare namespace LocalJSX { * Event dispatched whenever the filter state changes. */ "onFilterChanged"?: (event: IxCategoryFilterCustomEvent) => void; + /** + * Event dispatched whenever the filter gets cleared. + */ + "onFilterCleared"?: (event: IxCategoryFilterCustomEvent) => void; /** * Event dispatched whenever the text input changes. */ diff --git a/packages/core/src/components/category-filter/category-filter.tsx b/packages/core/src/components/category-filter/category-filter.tsx index 1e844e784e5..bf1d502b702 100644 --- a/packages/core/src/components/category-filter/category-filter.tsx +++ b/packages/core/src/components/category-filter/category-filter.tsx @@ -167,6 +167,11 @@ export class CategoryFilter { */ @Event() filterChanged!: EventEmitter; + /** + * Event dispatched whenever the filter gets cleared. + */ + @Event() filterCleared!: EventEmitter; + get dropdown() { return this.hostElement.shadowRoot!.querySelector('ix-dropdown'); } @@ -213,7 +218,8 @@ export class CategoryFilter { this.formKeyDownListener = addDisposableEventListener( this.formElement, 'keydown', - () => this.handleFormElementKeyDown + ((e: KeyboardEvent) => + this.handleFormElementKeyDown(e)) as EventListener ); this.preventDefaultListener = addDisposableEventListener( @@ -233,7 +239,7 @@ export class CategoryFilter { this.inputKeyDownListener = addDisposableEventListener( this.textInput.current, 'keydown', - () => this.handleInputElementKeyDown + ((e: KeyboardEvent) => this.handleInputElementKeyDown(e)) as EventListener ); this.focusInListener = addDisposableEventListener( @@ -354,24 +360,35 @@ export class CategoryFilter { } } + private focusElement(selector: string): boolean { + const item = this.hostElement.shadowRoot!.querySelector(selector); + if (item instanceof HTMLElement) { + item.focus(); + return true; + } + return false; + } + + private onArrowDown(e: KeyboardEvent) { + const baseSelector = `.category-item-${ + this.category !== '' ? 'value' : 'id' + }`; + const fallbackSelector = '.category-item'; + + if (this.focusElement(baseSelector)) { + e.stopPropagation(); + return; + } + + if (this.suggestions?.length && this.focusElement(fallbackSelector)) { + e.stopPropagation(); + } + } + private handleInputElementKeyDown(e: KeyboardEvent) { switch (e.code) { case 'ArrowDown': { - const selector = `.category-item-${ - this.category !== '' ? 'value' : 'id' - }`; - let item = this.hostElement.shadowRoot!.querySelector(selector); - - if (item instanceof HTMLElement) { - item.focus(); - e.stopPropagation(); - } else if (this.suggestions?.length) { - item = this.hostElement.shadowRoot!.querySelector('.category-item'); - if (item instanceof HTMLElement) { - item.focus(); - e.stopPropagation(); - } - } + this.onArrowDown(e); break; } @@ -393,7 +410,10 @@ export class CategoryFilter { case 'Enter': case 'NumpadEnter': - this.addToken(this.inputValue, this.category); + this.addToken( + this.inputValue, + this.category || this.ID_CUSTOM_FILTER_VALUE + ); e.preventDefault(); break; } @@ -482,6 +502,12 @@ export class CategoryFilter { } private resetFilter(e: Event) { + const { defaultPrevented } = this.filterCleared.emit(); + + if (defaultPrevented) { + return; + } + e.stopPropagation(); this.closeDropdown(); this.filterTokens = []; @@ -489,6 +515,7 @@ export class CategoryFilter { this.category = ''; this.categoryChanged.emit(undefined); } + this.emitFilterEvent(); } diff --git a/packages/core/src/components/category-filter/test/category-filter.ct.ts b/packages/core/src/components/category-filter/test/category-filter.ct.ts index 079395f06ed..cb171129c8d 100644 --- a/packages/core/src/components/category-filter/test/category-filter.ct.ts +++ b/packages/core/src/components/category-filter/test/category-filter.ct.ts @@ -38,20 +38,33 @@ test.describe('category-preview test', () => { }); }); - test('clear category-preview', async ({ page }) => { + test('add token', async ({ page }) => { + const token = 'Test'; await page.waitForSelector('ix-category-filter'); - await page.locator('input').first().click(); - await page.locator('.category-item').first().click(); + const input = await page.locator('input').first(); + await input.click(); + await input.fill(token); + await page.keyboard.press('Enter'); + const chip = await page.locator('ix-filter-chip').first(); + await expect(chip).toContainText(token); + }); - const categoryPreviewPromise = page.evaluate(() => { - return new Promise((resolve) => { - function onCategoryChanged(event) { - resolve(event.detail); - } + test('clear category-preview', async ({ page }) => { + const categoryFilter = page.locator('ix-category-filter'); + await categoryFilter.locator('input').first().click(); + await categoryFilter.locator('.category-item').first().click(); - document.addEventListener('categoryChanged', onCategoryChanged); - }); - }); + const categoryPreviewPromise = categoryFilter.evaluate( + (element: HTMLIxCategoryFilterElement) => { + return new Promise((resolve) => { + function onCategoryChanged(event: CustomEvent) { + resolve(event.detail); + } + + element.addEventListener('categoryChanged', onCategoryChanged); + }); + } + ); await page.locator('ix-icon-button').first().click(); const categoryPreview = await categoryPreviewPromise; diff --git a/packages/vue/src/components.ts b/packages/vue/src/components.ts index 0d5ce4fada8..3d141370751 100644 --- a/packages/vue/src/components.ts +++ b/packages/vue/src/components.ts @@ -247,7 +247,8 @@ export const IxCategoryFilter = /*@__PURE__*/ defineContainer