From 7a8c73b6e95e35f16b57c093cc2dcf90882c1138 Mon Sep 17 00:00:00 2001 From: Stefan Dietz Date: Thu, 7 Nov 2024 16:14:12 +0100 Subject: [PATCH 01/75] Fix linting for Playwright config Refs: #6987 --- packages/components/tsconfig.node.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/tsconfig.node.json b/packages/components/tsconfig.node.json index 8eb7529506..eb6c00008a 100644 --- a/packages/components/tsconfig.node.json +++ b/packages/components/tsconfig.node.json @@ -19,8 +19,8 @@ "typeRoots": ["node_modules/@types"], "strict": true, "skipLibCheck": true, - "resolveJsonModule": true, + "resolveJsonModule": true }, - "include": ["stencil.config.ts", ".eslintrc.js"], + "include": [".eslintrc.js", "playwright.config.ts", "stencil.config.ts"], "exclude": ["node_modules"] } From 67a15f48af231952583811694523d0cdc11aee3a Mon Sep 17 00:00:00 2001 From: Stefan Dietz Date: Fri, 8 Nov 2024 19:55:14 +0100 Subject: [PATCH 02/75] Implement some events and tests Refs: #6987 --- .../src/components/button/button.e2e.ts | 16 ++++++ .../src/components/button/component.tsx | 10 +++- .../components/input-date/input-date.e2e.ts | 35 +++++++++++++ .../src/components/input-date/shadow.tsx | 42 ++++++++++----- .../components/input-text/input-text.e2e.ts | 30 +++++++++++ .../src/components/input-text/shadow.tsx | 52 ++++++++++++++----- .../src/components/split-button/shadow.tsx | 7 +++ 7 files changed, 165 insertions(+), 27 deletions(-) diff --git a/packages/components/src/components/button/button.e2e.ts b/packages/components/src/components/button/button.e2e.ts index 4ef24e855d..aeaec7d695 100644 --- a/packages/components/src/components/button/button.e2e.ts +++ b/packages/components/src/components/button/button.e2e.ts @@ -7,4 +7,20 @@ test.describe('kol-button', () => { const kolButton = page.locator('kol-button'); await expect(kolButton).toContainText('Test Button Element'); }); + + test.describe('DOM events', () => { + ['click', 'mousedown'].forEach((event) => { + test(`should emit ${event} when internal button emits ${event}`, async ({ page }) => { + await page.setContent(''); + const eventPromise = page.locator('kol-button').evaluate(async (element, event) => { + return new Promise((resolve) => { + element.addEventListener(event, resolve); + }); + }, event); + await page.waitForChanges(); + await page.locator('button').dispatchEvent(event); + await expect(eventPromise).resolves.toBeTruthy(); + }); + }); + }); }); diff --git a/packages/components/src/components/button/component.tsx b/packages/components/src/components/button/component.tsx index 1bc2d5d9cf..efdcafb07d 100644 --- a/packages/components/src/components/button/component.tsx +++ b/packages/components/src/components/button/component.tsx @@ -99,6 +99,14 @@ export class KolButtonWc implements ButtonAPI, FocusableElement { this.state._on?.onClick(event, this.state._value); } } + + this.host?.dispatchEvent(new Event('click', { bubbles: true, composed: true })); + }; + + private readonly onMouseDown = (event: MouseEvent) => { + stopPropagation(event); + this.state?._on?.onMouseDown?.(event); + this.host?.dispatchEvent(new Event('mousedown', { bubbles: true, composed: true })); }; public render(): JSX.Element { @@ -126,8 +134,8 @@ export class KolButtonWc implements ButtonAPI, FocusableElement { disabled={this.state._disabled} id={this.state._id} name={this.state._name} - {...this.state._on} onClick={this.onClick} + onMouseDown={this.onMouseDown} role={this.state._role} tabIndex={this.state._tabIndex} type={this.state._type} diff --git a/packages/components/src/components/input-date/input-date.e2e.ts b/packages/components/src/components/input-date/input-date.e2e.ts index 3bc10af728..e86b86d2e9 100644 --- a/packages/components/src/components/input-date/input-date.e2e.ts +++ b/packages/components/src/components/input-date/input-date.e2e.ts @@ -204,6 +204,25 @@ test.describe('kol-input-date', () => { expect(getValue).toBe(value); } }); + + test(`should reflect the correct _value property as ${label} on the web component`, async ({ page }) => { + await page.setContent(''); + await page.locator('kol-input-date').evaluate((element: HTMLKolInputDateElement, date) => { + element._value = date; // set the initial value + }, value); + + const NEW_DATE = '2021-03-03'; + await page.locator('input').fill(NEW_DATE); + + const valueDomProperty = await page.locator('kol-input-date').evaluate((element: HTMLKolInputDateElement) => element._value); + + if (value instanceof Date) { + expect(valueDomProperty).toBeInstanceOf(Date); + expect((valueDomProperty as Date).toISOString().split('T')[0]).toBe(new Date(NEW_DATE).toISOString().split('T')[0]); + } else { + expect(valueDomProperty).toBe(NEW_DATE); + } + }); }); }); }); @@ -360,4 +379,20 @@ test.describe('kol-input-date', () => { await expect(page.locator('input')).toHaveValue(''); }); }); + + test.describe('DOM events', () => { + ['click', 'focus', 'blur', 'input', 'change'].forEach((event) => { + test(`should emit ${event} when internal input emits ${event}`, async ({ page }) => { + await page.setContent(''); + const eventPromise = page.locator('kol-input-date').evaluate(async (element, event) => { + return new Promise((resolve) => { + element.addEventListener(event, resolve); + }); + }, event); + await page.waitForChanges(); + await page.locator('input').dispatchEvent(event); + await expect(eventPromise).resolves.toBeTruthy(); + }); + }); + }); }); diff --git a/packages/components/src/components/input-date/shadow.tsx b/packages/components/src/components/input-date/shadow.tsx index 8eb59351bb..95147a9bc4 100644 --- a/packages/components/src/components/input-date/shadow.tsx +++ b/packages/components/src/components/input-date/shadow.tsx @@ -95,19 +95,43 @@ export class KolInputDate implements InputDateAPI, FocusableElement { this._initialValueType = null; } } - private remapValue(newValue: string): Date | string { - return this._initialValueType === 'Date' ? new Date(newValue) : newValue; + private remapValue(newValue: string): Date | Iso8601 { + return this._initialValueType === 'Date' ? new Date(newValue) : (newValue as Iso8601); } + private emitEvent(type: string): void { + this.host?.dispatchEvent(new Event(type, { bubbles: true, composed: true })); + } + + private readonly onBlur = (event: Event) => { + this.emitEvent('blur'); + this.controller.onFacade.onBlur(event); + this.inputHasFocus = false; + }; + + private readonly onClick = (event: Event) => { + this.emitEvent('click'); + this.controller.onFacade.onClick(event); + }; + + private readonly onFocus = (event: Event) => { + this.emitEvent('focus'); + this.controller.onFacade.onFocus(event); + this.inputHasFocus = true; + }; + private readonly onChange = (event: Event) => { const newValue = (event.target as HTMLInputElement).value; const remappedValue = this.remapValue(newValue); + this.emitEvent('change'); this.controller.onFacade.onChange(event, remappedValue); }; private readonly onInput = (event: Event) => { const newValue = (event.target as HTMLInputElement).value; const remappedValue = this.remapValue(newValue); + this._value = remappedValue; + this.emitEvent('input'); this.controller.onFacade.onInput(event, true, remappedValue); }; @@ -185,16 +209,10 @@ export class KolInputDate implements InputDateAPI, FocusableElement { spellcheck="false" type={this.state._type} value={this.state._value || undefined} - {...this.controller.onFacade} + onBlur={this.onBlur} + onClick={this.onClick} + onFocus={this.onFocus} onKeyDown={this.onKeyDown} - onBlur={(event) => { - this.controller.onFacade.onBlur(event); - this.inputHasFocus = false; - }} - onFocus={(event) => { - this.controller.onFacade.onFocus(event); - this.inputHasFocus = true; - }} onChange={this.onChange} onInput={this.onInput} /> @@ -349,7 +367,7 @@ export class KolInputDate implements InputDateAPI, FocusableElement { /** * Defines the value of the input. */ - @Prop({ mutable: true }) public _value?: Iso8601 | Date | null; + @Prop({ mutable: true, reflect: true }) public _value?: Iso8601 | Date | null; @State() public state: InputDateStates = { _autoComplete: 'off', diff --git a/packages/components/src/components/input-text/input-text.e2e.ts b/packages/components/src/components/input-text/input-text.e2e.ts index fff447b9a0..077e4a4114 100644 --- a/packages/components/src/components/input-text/input-text.e2e.ts +++ b/packages/components/src/components/input-text/input-text.e2e.ts @@ -16,4 +16,34 @@ test.describe('kol-input-text', () => { await kolButton.click(); }); }); + + test(`should reflect the _value property on the web component`, async ({ page }) => { + const value = 'Lorem Ipsum'; + await page.setContent(''); + await page.locator('kol-input-text').evaluate((element: HTMLKolInputTextElement, text) => { + element._value = text; // set the initial value + }, value); + const NEW_INPUT = 'Dolor Sit Amet'; + await page.locator('input').fill(NEW_INPUT); + + const valueDomProperty = await page.locator('kol-input-text').evaluate((element: HTMLKolInputTextElement) => element._value); + + expect(valueDomProperty).toBe(NEW_INPUT); + }); + + test.describe('DOM events', () => { + ['click', 'focus', 'blur', 'input', 'change'].forEach((event) => { + test(`should emit ${event} when internal input emits ${event}`, async ({ page }) => { + await page.setContent(''); + const eventPromise = page.locator('kol-input-text').evaluate(async (element, event) => { + return new Promise((resolve) => { + element.addEventListener(event, resolve); + }); + }, event); + await page.waitForChanges(); + await page.locator('input').dispatchEvent(event); + await expect(eventPromise).resolves.toBeTruthy(); + }); + }); + }); }); diff --git a/packages/components/src/components/input-text/shadow.tsx b/packages/components/src/components/input-text/shadow.tsx index 1530b87de0..f42c0fb4a6 100644 --- a/packages/components/src/components/input-text/shadow.tsx +++ b/packages/components/src/components/input-text/shadow.tsx @@ -46,19 +46,49 @@ export class KolInputText implements InputTextAPI, FocusableElement { private inputRef?: HTMLInputElement; private oldValue?: string; + private emitEvent(type: string): void { + this.host?.dispatchEvent(new Event(type, { bubbles: true, composed: true })); + } + private readonly catchRef = (ref?: HTMLInputElement) => { this.inputRef = ref; }; + private readonly onBlur = (event: FocusEvent) => { + this.emitEvent('blur'); + this.controller.onFacade.onBlur(event); + this.inputHasFocus = false; + }; + private readonly onChange = (event: Event) => { - if (this.oldValue !== this.inputRef?.value) { - this.oldValue = this.inputRef?.value; - this.controller.onFacade.onChange(event); + const value = this.inputRef?.value; + + if (this.oldValue !== value) { + this.oldValue = value; } + + this.emitEvent('change'); + this.controller.onFacade.onChange(event); + }; + + private readonly onClick = (event: MouseEvent) => { + this.emitEvent('click'); + this.controller.onFacade.onClick(event); + }; + + private readonly onFocus = (event: FocusEvent) => { + this.emitEvent('focus'); + this.controller.onFacade.onFocus(event); + this.inputHasFocus = true; }; private readonly onInput = (event: InputEvent) => { - setState(this, '_currentLength', (event.target as HTMLInputElement).value.length); + const value = this.inputRef?.value ?? ''; + setState(this, '_currentLength', value.length); + + this._value = value; + this.emitEvent('input'); + this.controller.onFacade.onInput(event); }; @@ -166,18 +196,12 @@ export class KolInputText implements InputTextAPI, FocusableElement { spellcheck="false" type={this.state._type} value={this.state._value as string} - {...this.controller.onFacade} + onBlur={this.onBlur} onChange={this.onChange} + onClick={this.onClick} + onFocus={this.onFocus} onInput={this.onInput} onKeyDown={this.onKeyDown} - onFocus={(event) => { - this.controller.onFacade.onFocus(event); - this.inputHasFocus = true; - }} - onBlur={(event) => { - this.controller.onFacade.onBlur(event); - this.inputHasFocus = false; - }} /> @@ -336,7 +360,7 @@ export class KolInputText implements InputTextAPI, FocusableElement { /** * Defines the value of the input. */ - @Prop({ mutable: true }) public _value?: string; + @Prop({ mutable: true, reflect: true }) public _value?: string; @State() public state: InputTextStates = { _autoComplete: 'off', diff --git a/packages/components/src/components/split-button/shadow.tsx b/packages/components/src/components/split-button/shadow.tsx index fece3357ed..f0c623056a 100644 --- a/packages/components/src/components/split-button/shadow.tsx +++ b/packages/components/src/components/split-button/shadow.tsx @@ -15,6 +15,7 @@ import type { TooltipAlignPropType, } from '../../schema'; import type { JSX } from '@stencil/core'; +import { Listen } from '@stencil/core'; import { Component, h, Host, Method, Prop, State } from '@stencil/core'; import { translate } from '../../i18n'; @@ -33,6 +34,12 @@ import { KolButtonWcTag, KolPopoverWcTag } from '../../core/component-names'; }, }) export class KolSplitButton implements SplitButtonProps /*, SplitButtonAPI*/ { + @Listen('click') + handleHostClick(event: MouseEvent) { + // Stop propagation to avoid the popover being close immediately because of the body click event + event.stopPropagation(); + } + private readonly clickButtonHandler = { onClick: (e: MouseEvent) => { if (typeof this._on?.onClick === 'function') { From de31f906dfdc6438b2525eb86776e46dfd52769c Mon Sep 17 00:00:00 2001 From: Stefan Dietz Date: Fri, 8 Nov 2024 20:07:38 +0100 Subject: [PATCH 03/75] Add callback tests for button Refs: #6987 --- .../src/components/button/button.e2e.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/components/src/components/button/button.e2e.ts b/packages/components/src/components/button/button.e2e.ts index aeaec7d695..c52d23a638 100644 --- a/packages/components/src/components/button/button.e2e.ts +++ b/packages/components/src/components/button/button.e2e.ts @@ -8,6 +8,29 @@ test.describe('kol-button', () => { await expect(kolButton).toContainText('Test Button Element'); }); + test.describe('Callbacks', () => { + ['onClick', 'onMouseDown'].forEach((callbackName) => { + test(`should call ${callbackName} when internal button emits`, async ({ page }) => { + await page.setContent(''); + const kolButton = page.locator('kol-button'); + + const eventPromise = kolButton.evaluate((element: HTMLKolButtonElement, callbackName) => { + return new Promise((resolve) => { + element._on = { + [callbackName]: () => { + resolve(); + }, + }; + }); + }, callbackName); + await page.waitForChanges(); + + await page.locator('button').click(); + await expect(eventPromise).resolves.toBeUndefined(); + }); + }); + }); + test.describe('DOM events', () => { ['click', 'mousedown'].forEach((event) => { test(`should emit ${event} when internal button emits ${event}`, async ({ page }) => { From 76b1423f6494949cbc72ea2f4200abfb4e83498d Mon Sep 17 00:00:00 2001 From: Stefan Dietz Date: Wed, 20 Nov 2024 10:35:34 +0100 Subject: [PATCH 04/75] Add callback tests for KolInputDate and KolInputText Refs: #6987 --- .../components/input-date/input-date.e2e.ts | 34 +++++++++++++++++++ .../components/input-text/input-text.e2e.ts | 34 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/packages/components/src/components/input-date/input-date.e2e.ts b/packages/components/src/components/input-date/input-date.e2e.ts index e86b86d2e9..b58a5db18f 100644 --- a/packages/components/src/components/input-date/input-date.e2e.ts +++ b/packages/components/src/components/input-date/input-date.e2e.ts @@ -380,6 +380,40 @@ test.describe('kol-input-date', () => { }); }); + test.describe('Callbacks', () => { + [ + ['click', 'onClick'], + ['focus', 'onFocus'], + ['blur', 'onBlur'], + ['input', 'onInput', '2024-11-19'], + ['change', 'onChange', '2024-11-19'], + ].forEach(([eventName, callbackName, testValue]) => { + test(`should call ${callbackName} when internal input emits`, async ({ page }) => { + await page.setContent(''); + const kolInputDate = page.locator('kol-input-date'); + const input = page.locator('input'); + + const eventPromise = kolInputDate.evaluate((element: HTMLKolInputDateElement, callbackName) => { + return new Promise((resolve) => { + element._on = { + [callbackName]: (_event: InputEvent, value?: Date | Iso8601) => { + resolve(value); + }, + }; + }); + }, callbackName); + await page.waitForChanges(); + + if (testValue) { + await input.fill(testValue); + } + await input.dispatchEvent(eventName); + + await expect(eventPromise).resolves.toBe(testValue); + }); + }); + }); + test.describe('DOM events', () => { ['click', 'focus', 'blur', 'input', 'change'].forEach((event) => { test(`should emit ${event} when internal input emits ${event}`, async ({ page }) => { diff --git a/packages/components/src/components/input-text/input-text.e2e.ts b/packages/components/src/components/input-text/input-text.e2e.ts index 077e4a4114..fb00e8d1d0 100644 --- a/packages/components/src/components/input-text/input-text.e2e.ts +++ b/packages/components/src/components/input-text/input-text.e2e.ts @@ -31,6 +31,40 @@ test.describe('kol-input-text', () => { expect(valueDomProperty).toBe(NEW_INPUT); }); + test.describe('Callbacks', () => { + [ + ['click', 'onClick'], + ['focus', 'onFocus'], + ['blur', 'onBlur'], + ['input', 'onInput', 'Test Input'], + ['change', 'onChange', 'Test Input'], + ].forEach(([eventName, callbackName, testValue]) => { + test(`should call ${callbackName} when internal input emits`, async ({ page }) => { + await page.setContent(''); + const kolInputText = page.locator('kol-input-text'); + const input = page.locator('input'); + + const eventPromise = kolInputText.evaluate((element: HTMLKolInputTextElement, callbackName) => { + return new Promise((resolve) => { + element._on = { + [callbackName]: (_event: InputEvent, value?: string) => { + resolve(value); + }, + }; + }); + }, callbackName); + await page.waitForChanges(); + + if (testValue) { + await input.fill(testValue); + } + await input.dispatchEvent(eventName); + + await expect(eventPromise).resolves.toBe(testValue); + }); + }); + }); + test.describe('DOM events', () => { ['click', 'focus', 'blur', 'input', 'change'].forEach((event) => { test(`should emit ${event} when internal input emits ${event}`, async ({ page }) => { From b86e33ea732ef3310a6382bac5498d42cc703f19 Mon Sep 17 00:00:00 2001 From: Stefan Dietz Date: Fri, 22 Nov 2024 13:46:19 +0100 Subject: [PATCH 05/75] Remove redundant date tests Refs: #6987 --- .../components/input-date/input-date.e2e.ts | 94 ------------------- 1 file changed, 94 deletions(-) diff --git a/packages/components/src/components/input-date/input-date.e2e.ts b/packages/components/src/components/input-date/input-date.e2e.ts index b58a5db18f..f682dc38b6 100644 --- a/packages/components/src/components/input-date/input-date.e2e.ts +++ b/packages/components/src/components/input-date/input-date.e2e.ts @@ -127,66 +127,6 @@ test.describe('kol-input-date', () => { } }); - test(`should trigger custom "kol-change" DOM event with ${label} as event detail`, async ({ page }) => { - await page.setContent(''); - const inputDate = page.locator('kol-input-date'); - - await inputDate.evaluate((element: HTMLKolInputDateElement, date) => { - element._value = date; - }, value); - - const inputDateEventPromise = inputDate.evaluate((element: HTMLKolInputDateElement) => { - return new Promise((resolve) => { - element.addEventListener('kol-change', (e: Event) => { - const eventValue: Date | Iso8601 = (e as CustomEvent).detail; - resolve(eventValue); - }); - }); - }); - - await page.waitForChanges(); - await page.locator('input').dispatchEvent('change'); - - const eventDetail = await inputDateEventPromise; - - if (value instanceof Date) { - expect(eventDetail).toBeInstanceOf(Date); - expect((eventDetail as Date).toISOString().split('T')[0]).toBe(value.toISOString().split('T')[0]); - } else { - expect(eventDetail).toBe(value); - } - }); - - test(`should trigger custom "kol-input" DOM event with ${label} as event detail`, async ({ page }) => { - await page.setContent(''); - const inputDate = page.locator('kol-input-date'); - - await inputDate.evaluate((element: HTMLKolInputDateElement, date) => { - element._value = date; - }, value); - - const inputDateEventPromise = inputDate.evaluate((element: HTMLKolInputDateElement) => { - return new Promise((resolve) => { - element.addEventListener('kol-input', (e: Event) => { - const eventValue: Date | Iso8601 = (e as CustomEvent).detail; - resolve(eventValue); - }); - }); - }); - - await page.waitForChanges(); - await page.locator('input').dispatchEvent('input'); - - const eventDetail = await inputDateEventPromise; - - if (value instanceof Date) { - expect(eventDetail).toBeInstanceOf(Date); - expect((eventDetail as Date).toISOString().split('T')[0]).toBe(value.toISOString().split('T')[0]); - } else { - expect(eventDetail).toBe(value); - } - }); - test(`should return the correct value for getValue() as ${label}`, async ({ page }) => { await page.setContent(''); await page.locator('kol-input-date').evaluate((element: HTMLKolInputDateElement, date) => { @@ -380,40 +320,6 @@ test.describe('kol-input-date', () => { }); }); - test.describe('Callbacks', () => { - [ - ['click', 'onClick'], - ['focus', 'onFocus'], - ['blur', 'onBlur'], - ['input', 'onInput', '2024-11-19'], - ['change', 'onChange', '2024-11-19'], - ].forEach(([eventName, callbackName, testValue]) => { - test(`should call ${callbackName} when internal input emits`, async ({ page }) => { - await page.setContent(''); - const kolInputDate = page.locator('kol-input-date'); - const input = page.locator('input'); - - const eventPromise = kolInputDate.evaluate((element: HTMLKolInputDateElement, callbackName) => { - return new Promise((resolve) => { - element._on = { - [callbackName]: (_event: InputEvent, value?: Date | Iso8601) => { - resolve(value); - }, - }; - }); - }, callbackName); - await page.waitForChanges(); - - if (testValue) { - await input.fill(testValue); - } - await input.dispatchEvent(eventName); - - await expect(eventPromise).resolves.toBe(testValue); - }); - }); - }); - test.describe('DOM events', () => { ['click', 'focus', 'blur', 'input', 'change'].forEach((event) => { test(`should emit ${event} when internal input emits ${event}`, async ({ page }) => { From 87b802d451ceb396012f3217abe40856a8e5fd6e Mon Sep 17 00:00:00 2001 From: Stefan Dietz Date: Fri, 22 Nov 2024 14:05:46 +0100 Subject: [PATCH 06/75] Consolidate input tests Refs: #6987 --- .../components/input-date/input-date.e2e.ts | 17 +---- .../components/input-text/input-text.e2e.ts | 67 ++----------------- packages/components/src/e2e/index.ts | 3 + .../components/src/e2e/input-callbacks.ts | 40 +++++++++++ .../components/src/e2e/input-dom-events.ts | 22 ++++++ .../src/e2e/input-value-reflection.ts | 15 +++++ 6 files changed, 86 insertions(+), 78 deletions(-) create mode 100644 packages/components/src/e2e/index.ts create mode 100644 packages/components/src/e2e/input-callbacks.ts create mode 100644 packages/components/src/e2e/input-dom-events.ts create mode 100644 packages/components/src/e2e/input-value-reflection.ts diff --git a/packages/components/src/components/input-date/input-date.e2e.ts b/packages/components/src/components/input-date/input-date.e2e.ts index f682dc38b6..ff70754166 100644 --- a/packages/components/src/components/input-date/input-date.e2e.ts +++ b/packages/components/src/components/input-date/input-date.e2e.ts @@ -1,6 +1,7 @@ import { expect } from '@playwright/test'; import { test } from '@stencil/playwright'; import type { Iso8601 } from '../../schema'; +import { testInputDomEvents } from '../../e2e'; test.describe('kol-input-date', () => { test.describe('when value is Date object', () => { @@ -320,19 +321,5 @@ test.describe('kol-input-date', () => { }); }); - test.describe('DOM events', () => { - ['click', 'focus', 'blur', 'input', 'change'].forEach((event) => { - test(`should emit ${event} when internal input emits ${event}`, async ({ page }) => { - await page.setContent(''); - const eventPromise = page.locator('kol-input-date').evaluate(async (element, event) => { - return new Promise((resolve) => { - element.addEventListener(event, resolve); - }); - }, event); - await page.waitForChanges(); - await page.locator('input').dispatchEvent(event); - await expect(eventPromise).resolves.toBeTruthy(); - }); - }); - }); + testInputDomEvents('kol-input-date'); }); diff --git a/packages/components/src/components/input-text/input-text.e2e.ts b/packages/components/src/components/input-text/input-text.e2e.ts index fb00e8d1d0..43dc8b2ff0 100644 --- a/packages/components/src/components/input-text/input-text.e2e.ts +++ b/packages/components/src/components/input-text/input-text.e2e.ts @@ -1,5 +1,6 @@ import { expect } from '@playwright/test'; import { test } from '@stencil/playwright'; +import { testInputCallbacks, testInputDomEvents, testInputValueReflection } from '../../e2e'; test.describe('kol-input-text', () => { test.describe('smart-button', () => { @@ -17,67 +18,7 @@ test.describe('kol-input-text', () => { }); }); - test(`should reflect the _value property on the web component`, async ({ page }) => { - const value = 'Lorem Ipsum'; - await page.setContent(''); - await page.locator('kol-input-text').evaluate((element: HTMLKolInputTextElement, text) => { - element._value = text; // set the initial value - }, value); - const NEW_INPUT = 'Dolor Sit Amet'; - await page.locator('input').fill(NEW_INPUT); - - const valueDomProperty = await page.locator('kol-input-text').evaluate((element: HTMLKolInputTextElement) => element._value); - - expect(valueDomProperty).toBe(NEW_INPUT); - }); - - test.describe('Callbacks', () => { - [ - ['click', 'onClick'], - ['focus', 'onFocus'], - ['blur', 'onBlur'], - ['input', 'onInput', 'Test Input'], - ['change', 'onChange', 'Test Input'], - ].forEach(([eventName, callbackName, testValue]) => { - test(`should call ${callbackName} when internal input emits`, async ({ page }) => { - await page.setContent(''); - const kolInputText = page.locator('kol-input-text'); - const input = page.locator('input'); - - const eventPromise = kolInputText.evaluate((element: HTMLKolInputTextElement, callbackName) => { - return new Promise((resolve) => { - element._on = { - [callbackName]: (_event: InputEvent, value?: string) => { - resolve(value); - }, - }; - }); - }, callbackName); - await page.waitForChanges(); - - if (testValue) { - await input.fill(testValue); - } - await input.dispatchEvent(eventName); - - await expect(eventPromise).resolves.toBe(testValue); - }); - }); - }); - - test.describe('DOM events', () => { - ['click', 'focus', 'blur', 'input', 'change'].forEach((event) => { - test(`should emit ${event} when internal input emits ${event}`, async ({ page }) => { - await page.setContent(''); - const eventPromise = page.locator('kol-input-text').evaluate(async (element, event) => { - return new Promise((resolve) => { - element.addEventListener(event, resolve); - }); - }, event); - await page.waitForChanges(); - await page.locator('input').dispatchEvent(event); - await expect(eventPromise).resolves.toBeTruthy(); - }); - }); - }); + testInputValueReflection('kol-input-text', 'Hello World'); + testInputCallbacks('kol-input-text'); + testInputDomEvents('kol-input-text'); }); diff --git a/packages/components/src/e2e/index.ts b/packages/components/src/e2e/index.ts new file mode 100644 index 0000000000..41d0edde5a --- /dev/null +++ b/packages/components/src/e2e/index.ts @@ -0,0 +1,3 @@ +export * from './input-callbacks'; +export * from './input-dom-events'; +export * from './input-value-reflection'; diff --git a/packages/components/src/e2e/input-callbacks.ts b/packages/components/src/e2e/input-callbacks.ts new file mode 100644 index 0000000000..93b8c13782 --- /dev/null +++ b/packages/components/src/e2e/input-callbacks.ts @@ -0,0 +1,40 @@ +import { test } from '@stencil/playwright'; +import { expect } from '@playwright/test'; + +const testInputCallbacks = (componentName: string) => { + test.describe('Callbacks', () => { + [ + ['click', 'onClick'], + ['focus', 'onFocus'], + ['blur', 'onBlur'], + ['input', 'onInput', 'Test Input'], + ['change', 'onChange', 'Test Input'], + ].forEach(([eventName, callbackName, testValue]) => { + test(`should call ${callbackName} when internal input emits`, async ({ page }) => { + await page.setContent(`<${componentName} _label="Input">`); + const component = page.locator(componentName); + const input = page.locator('input'); + + const eventPromise = component.evaluate((element: HTMLKolInputTextElement, callbackName) => { + return new Promise((resolve) => { + element._on = { + [callbackName]: (_event: InputEvent, value?: string) => { + resolve(value); + }, + }; + }); + }, callbackName); + await page.waitForChanges(); + + if (testValue) { + await input.fill(testValue); + } + await input.dispatchEvent(eventName); + + await expect(eventPromise).resolves.toBe(testValue); + }); + }); + }); +}; + +export { testInputCallbacks }; diff --git a/packages/components/src/e2e/input-dom-events.ts b/packages/components/src/e2e/input-dom-events.ts new file mode 100644 index 0000000000..1c5c46ecc3 --- /dev/null +++ b/packages/components/src/e2e/input-dom-events.ts @@ -0,0 +1,22 @@ +import { test } from '@stencil/playwright'; +import { expect } from '@playwright/test'; + +const testInputDomEvents = (componentName: string) => { + test.describe('DOM events', () => { + ['click', 'focus', 'blur', 'input', 'change'].forEach((event) => { + test(`should emit ${event} when internal input emits ${event}`, async ({ page }) => { + await page.setContent(`<${componentName} _label="Input">`); + const eventPromise = page.locator(componentName).evaluate(async (element, event) => { + return new Promise((resolve) => { + element.addEventListener(event, resolve); + }); + }, event); + await page.waitForChanges(); + await page.locator('input').dispatchEvent(event); + await expect(eventPromise).resolves.toBeTruthy(); + }); + }); + }); +}; + +export { testInputDomEvents }; diff --git a/packages/components/src/e2e/input-value-reflection.ts b/packages/components/src/e2e/input-value-reflection.ts new file mode 100644 index 0000000000..692f606a73 --- /dev/null +++ b/packages/components/src/e2e/input-value-reflection.ts @@ -0,0 +1,15 @@ +import { test } from '@stencil/playwright'; +import { expect } from '@playwright/test'; + +const testInputValueReflection = (componentName: string, inputValue: string) => { + test(`should reflect the _value property on the web component`, async ({ page }) => { + await page.setContent(`<${componentName} _label="Input">`); + await page.locator('input').fill(inputValue); + + const valueDomProperty = await page.locator(componentName).evaluate((element: HTMLKolInputTextElement) => element._value); + + expect(valueDomProperty).toBe(inputValue); + }); +}; + +export { testInputValueReflection }; From d6e95e3de08cfdce43e945df5ecaf8933c185ffd Mon Sep 17 00:00:00 2001 From: Stefan Dietz Date: Fri, 22 Nov 2024 16:32:44 +0100 Subject: [PATCH 07/75] Consolidate inputs event handling Refs: #6987 --- .../@deprecated/input/controller.ts | 18 +++++++----- .../components/input-color/input-color.e2e.ts | 11 ++++++++ .../src/components/input-color/shadow.tsx | 28 +++++++++++++------ .../src/components/input-date/shadow.tsx | 15 +--------- .../src/components/input-text/shadow.tsx | 15 +--------- .../components/src/e2e/input-callbacks.ts | 6 ++-- 6 files changed, 46 insertions(+), 47 deletions(-) create mode 100644 packages/components/src/components/input-color/input-color.e2e.ts diff --git a/packages/components/src/components/@deprecated/input/controller.ts b/packages/components/src/components/@deprecated/input/controller.ts index 272b5d10dc..b0a11e36cb 100644 --- a/packages/components/src/components/@deprecated/input/controller.ts +++ b/packages/components/src/components/@deprecated/input/controller.ts @@ -26,14 +26,14 @@ import { validateHideLabel, validateLabelWithExpertSlot, validateMsg, + validateShortKey, validateTabIndex, validateTooltipAlign, watchBoolean, watchString, - validateShortKey, } from '../../../schema'; -import { stopPropagation, tryToDispatchKoliBriEvent } from '../../../utils/events'; +import { stopPropagation } from '../../../utils/events'; import { ControlledInputController } from '../../input-adapter-leanup/controller'; import type { Props as AdapterProps } from '../../input-adapter-leanup/types'; @@ -173,12 +173,16 @@ export class InputController extends ControlledInputController implements Watche validateAccessAndShortKey(this.component._accessKey, this.component._shortKey); } + private emitEvent(type: string): void { + this.host?.dispatchEvent(new Event(type, { bubbles: true, composed: true })); + } + protected onBlur(event: Event): void { this.component._touched = true; // Event handling + this.emitEvent('blur'); stopPropagation(event); - tryToDispatchKoliBriEvent('blur', this.host); // Callback if (typeof this.component._on?.onBlur === 'function') { @@ -194,7 +198,7 @@ export class InputController extends ControlledInputController implements Watche value = value ?? (event.target as HTMLInputElement).value; // Event handling - tryToDispatchKoliBriEvent('change', this.host, value); + this.emitEvent('change'); // Callback if (typeof this.component._on?.onChange === 'function') { @@ -223,8 +227,8 @@ export class InputController extends ControlledInputController implements Watche value = value ?? (event.target as HTMLInputElement).value; // Event handling + this.emitEvent('input'); stopPropagation(event); - tryToDispatchKoliBriEvent('input', this.host, value); // Static form handling if (shouldSetFormAssociatedValue) { @@ -239,8 +243,8 @@ export class InputController extends ControlledInputController implements Watche protected onClick(event: Event): void { // Event handling + this.emitEvent('click'); stopPropagation(event); - tryToDispatchKoliBriEvent('click', this.host); // Callback if (typeof this.component._on?.onClick === 'function') { @@ -250,8 +254,8 @@ export class InputController extends ControlledInputController implements Watche protected onFocus(event: Event): void { // Event handling + this.emitEvent('focus'); stopPropagation(event); - tryToDispatchKoliBriEvent('focus', this.host); // Callback if (typeof this.component._on?.onFocus === 'function') { diff --git a/packages/components/src/components/input-color/input-color.e2e.ts b/packages/components/src/components/input-color/input-color.e2e.ts new file mode 100644 index 0000000000..be73f561be --- /dev/null +++ b/packages/components/src/components/input-color/input-color.e2e.ts @@ -0,0 +1,11 @@ +import { test } from '@stencil/playwright'; +import { testInputCallbacks, testInputDomEvents, testInputValueReflection } from '../../e2e'; + +const COMPONENT_NAME = 'kol-input-color'; +const TEST_VALUE = '#cc006e'; + +test.describe(COMPONENT_NAME, () => { + testInputValueReflection(COMPONENT_NAME, TEST_VALUE); + testInputCallbacks(COMPONENT_NAME, TEST_VALUE); + testInputDomEvents(COMPONENT_NAME); +}); diff --git a/packages/components/src/components/input-color/shadow.tsx b/packages/components/src/components/input-color/shadow.tsx index 00bf563deb..aa07664764 100644 --- a/packages/components/src/components/input-color/shadow.tsx +++ b/packages/components/src/components/input-color/shadow.tsx @@ -47,6 +47,21 @@ export class KolInputColor implements InputColorAPI, FocusableElement { this.inputRef = ref; }; + private readonly onBlur = (event: FocusEvent) => { + this.controller.onFacade.onBlur(event); + this.inputHasFocus = false; + }; + + private readonly onFocus = (event: FocusEvent) => { + this.controller.onFacade.onFocus(event); + this.inputHasFocus = true; + }; + + private readonly onInput = (event: InputEvent) => { + this._value = this.inputRef?.value ?? ''; + this.controller.onFacade.onInput(event); + }; + @Method() // eslint-disable-next-line @typescript-eslint/require-await public async getValue(): Promise { @@ -130,14 +145,9 @@ export class KolInputColor implements InputColorAPI, FocusableElement { type="color" value={this.state._value as string} {...this.controller.onFacade} - onFocus={(event) => { - this.controller.onFacade.onFocus(event); - this.inputHasFocus = true; - }} - onBlur={(event) => { - this.controller.onFacade.onBlur(event); - this.inputHasFocus = false; - }} + onBlur={this.onBlur} + onFocus={this.onFocus} + onInput={this.onInput} /> @@ -263,7 +273,7 @@ export class KolInputColor implements InputColorAPI, FocusableElement { /** * Defines the value of the input. */ - @Prop() public _value?: string; + @Prop({ reflect: true }) public _value?: string; @State() public state: InputColorStates = { _autoComplete: 'off', diff --git a/packages/components/src/components/input-date/shadow.tsx b/packages/components/src/components/input-date/shadow.tsx index d879995db7..d0057da6f8 100644 --- a/packages/components/src/components/input-date/shadow.tsx +++ b/packages/components/src/components/input-date/shadow.tsx @@ -102,23 +102,12 @@ export class KolInputDate implements InputDateAPI, FocusableElement { return this._initialValueType === 'Date' ? new Date(newValue) : (newValue as Iso8601); } - private emitEvent(type: string): void { - this.host?.dispatchEvent(new Event(type, { bubbles: true, composed: true })); - } - private readonly onBlur = (event: Event) => { - this.emitEvent('blur'); this.controller.onFacade.onBlur(event); this.inputHasFocus = false; }; - private readonly onClick = (event: Event) => { - this.emitEvent('click'); - this.controller.onFacade.onClick(event); - }; - private readonly onFocus = (event: Event) => { - this.emitEvent('focus'); this.controller.onFacade.onFocus(event); this.inputHasFocus = true; }; @@ -126,7 +115,6 @@ export class KolInputDate implements InputDateAPI, FocusableElement { private readonly onChange = (event: Event) => { const newValue = (event.target as HTMLInputElement).value; const remappedValue = this.remapValue(newValue); - this.emitEvent('change'); this.controller.onFacade.onChange(event, remappedValue); }; @@ -134,7 +122,6 @@ export class KolInputDate implements InputDateAPI, FocusableElement { const newValue = (event.target as HTMLInputElement).value; const remappedValue = this.remapValue(newValue); this._value = remappedValue; - this.emitEvent('input'); this.controller.onFacade.onInput(event, true, remappedValue); }; @@ -213,8 +200,8 @@ export class KolInputDate implements InputDateAPI, FocusableElement { spellcheck="false" type={this.state._type} value={this.state._value || undefined} + {...this.controller.onFacade} onBlur={this.onBlur} - onClick={this.onClick} onFocus={this.onFocus} onKeyDown={this.onKeyDown} onChange={this.onChange} diff --git a/packages/components/src/components/input-text/shadow.tsx b/packages/components/src/components/input-text/shadow.tsx index 13548f9915..6a99852377 100644 --- a/packages/components/src/components/input-text/shadow.tsx +++ b/packages/components/src/components/input-text/shadow.tsx @@ -48,16 +48,11 @@ export class KolInputText implements InputTextAPI, FocusableElement { private inputRef?: HTMLInputElement; private oldValue?: string; - private emitEvent(type: string): void { - this.host?.dispatchEvent(new Event(type, { bubbles: true, composed: true })); - } - private readonly catchRef = (ref?: HTMLInputElement) => { this.inputRef = ref; }; private readonly onBlur = (event: FocusEvent) => { - this.emitEvent('blur'); this.controller.onFacade.onBlur(event); this.inputHasFocus = false; }; @@ -69,17 +64,10 @@ export class KolInputText implements InputTextAPI, FocusableElement { this.oldValue = value; } - this.emitEvent('change'); this.controller.onFacade.onChange(event); }; - private readonly onClick = (event: MouseEvent) => { - this.emitEvent('click'); - this.controller.onFacade.onClick(event); - }; - private readonly onFocus = (event: FocusEvent) => { - this.emitEvent('focus'); this.controller.onFacade.onFocus(event); this.inputHasFocus = true; }; @@ -89,7 +77,6 @@ export class KolInputText implements InputTextAPI, FocusableElement { setState(this, '_currentLength', value.length); this._value = value; - this.emitEvent('input'); this.controller.onFacade.onInput(event); }; @@ -199,9 +186,9 @@ export class KolInputText implements InputTextAPI, FocusableElement { spellcheck="false" type={this.state._type} value={this.state._value as string} + {...this.controller.onFacade} onBlur={this.onBlur} onChange={this.onChange} - onClick={this.onClick} onFocus={this.onFocus} onInput={this.onInput} onKeyDown={this.onKeyDown} diff --git a/packages/components/src/e2e/input-callbacks.ts b/packages/components/src/e2e/input-callbacks.ts index 93b8c13782..52001c40f1 100644 --- a/packages/components/src/e2e/input-callbacks.ts +++ b/packages/components/src/e2e/input-callbacks.ts @@ -1,14 +1,14 @@ import { test } from '@stencil/playwright'; import { expect } from '@playwright/test'; -const testInputCallbacks = (componentName: string) => { +const testInputCallbacks = (componentName: string, testValue: string = 'Test Input') => { test.describe('Callbacks', () => { [ ['click', 'onClick'], ['focus', 'onFocus'], ['blur', 'onBlur'], - ['input', 'onInput', 'Test Input'], - ['change', 'onChange', 'Test Input'], + ['input', 'onInput', testValue], + ['change', 'onChange', testValue], ].forEach(([eventName, callbackName, testValue]) => { test(`should call ${callbackName} when internal input emits`, async ({ page }) => { await page.setContent(`<${componentName} _label="Input">`); From 3a514437b0e8649e6d9f6f2991edfa7bacfa30a5 Mon Sep 17 00:00:00 2001 From: Stefan Dietz Date: Fri, 22 Nov 2024 16:39:50 +0100 Subject: [PATCH 08/75] Update snapshots Refs: #6987 --- .../test/__snapshots__/snapshot.spec.tsx.snap | 56 +++++++++---------- .../test/__snapshots__/snapshot.spec.tsx.snap | 26 ++++----- .../test/__snapshots__/snapshot.spec.tsx.snap | 28 +++++----- 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/packages/components/src/components/input-color/test/__snapshots__/snapshot.spec.tsx.snap b/packages/components/src/components/input-color/test/__snapshots__/snapshot.spec.tsx.snap index c049415028..6acddadf84 100644 --- a/packages/components/src/components/input-color/test/__snapshots__/snapshot.spec.tsx.snap +++ b/packages/components/src/components/input-color/test/__snapshots__/snapshot.spec.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`kol-input-color should render with _label="Label" _name="field" _placeholder="Hier steht ein Platzhaltertext" _value="#FFF" _accessKey="V" 1`] = ` - +