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 78c28964af..3bc10af728 100644 --- a/packages/components/src/components/input-date/input-date.e2e.ts +++ b/packages/components/src/components/input-date/input-date.e2e.ts @@ -1,5 +1,6 @@ import { expect } from '@playwright/test'; import { test } from '@stencil/playwright'; +import type { Iso8601 } from '../../schema'; test.describe('kol-input-date', () => { test.describe('when value is Date object', () => { @@ -60,6 +61,153 @@ test.describe('kol-input-date', () => { }); }); + test.describe('Events', () => { + const testValues = [ + { label: 'ISO String', value: '2020-03-03' as Iso8601 }, + { label: 'Date Object', value: new Date('2020-03-03T03:02:01.099Z') }, + ]; + + testValues.forEach(({ label, value }) => { + test.describe(`when initial value is a ${label}`, () => { + test(`should call onChange with value parameter as ${label}`, async ({ page }) => { + await page.setContent(''); + const inputDate = page.locator('kol-input-date'); + void inputDate.evaluate((element: HTMLKolInputDateElement, date) => { + element._value = date; + }, value); + + const inputDateEventPromise = inputDate.evaluate((element) => { + return new Promise((resolve) => { + (element as HTMLKolSelectElement)._on = { + onChange: (_event, eventValue: unknown) => { + resolve(eventValue as Date | Iso8601); + }, + }; + }); + }); + + 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 call onInput with value parameter as ${label}`, async ({ page }) => { + await page.setContent(''); + const inputDate = page.locator('kol-input-date'); + void inputDate.evaluate((element: HTMLKolInputDateElement, date) => { + element._value = date; + }, value); + + const inputDateEventPromise = inputDate.evaluate((element) => { + return new Promise((resolve) => { + (element as HTMLKolSelectElement)._on = { + onInput: (_event, eventValue: unknown) => { + resolve(eventValue as Date | Iso8601); + }, + }; + }); + }); + + 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 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) => { + element._value = date; + }, value); + + const getValue = await page.locator('kol-input-date').evaluate((element: HTMLKolInputDateElement) => { + return element.getValue(); + }); + + if (value instanceof Date) { + expect(getValue).toBeInstanceOf(Date); + expect((getValue as Date).toISOString().split('T')[0]).toBe(value.toISOString().split('T')[0]); + } else { + expect(getValue).toBe(value); + } + }); + }); + }); + }); + test.describe('when min and max is set', () => { test.describe('for Iso8601-Format', () => { test('should set correct min and max for type date', async ({ page }) => { @@ -202,4 +350,14 @@ test.describe('kol-input-date', () => { }); }); }); + + test.describe('when initial value is null', () => { + test('should set value as empty string', async ({ page }) => { + await page.setContent(''); + await page.locator('kol-input-date').evaluate((element: HTMLKolInputDateElement) => { + element._value = null; + }); + await expect(page.locator('input')).toHaveValue(''); + }); + }); }); diff --git a/packages/components/src/components/input-date/shadow.tsx b/packages/components/src/components/input-date/shadow.tsx index 34d316f6b7..adc37e2f8d 100644 --- a/packages/components/src/components/input-date/shadow.tsx +++ b/packages/components/src/components/input-date/shadow.tsx @@ -42,14 +42,16 @@ export class KolInputDate implements InputDateAPI, FocusableElement { @Element() private readonly host?: HTMLKolInputDateElement; private inputRef?: HTMLInputElement; + @State() private _initialValueType: 'Date' | 'String' | null = null; + private readonly catchRef = (ref?: HTMLInputElement) => { this.inputRef = ref; }; @Method() // eslint-disable-next-line @typescript-eslint/require-await - public async getValue(): Promise { - return this.inputRef?.value; + public async getValue(): Promise { + return this.inputRef && this.remapValue(this.inputRef?.value); } /** @@ -82,6 +84,31 @@ export class KolInputDate implements InputDateAPI, FocusableElement { } } + private setInitialValueType(value: Iso8601 | Date | null) { + if (value instanceof Date) { + this._initialValueType = 'Date'; + } else if (typeof value === 'string') { + this._initialValueType = 'String'; + } else { + this._initialValueType = null; + } + } + private remapValue(newValue: string): Date | string { + return this._initialValueType === 'Date' ? new Date(newValue) : newValue; + } + + private readonly onChange = (event: Event) => { + const newValue = (event.target as HTMLInputElement).value; + const remappedValue = this.remapValue(newValue); + this.controller.onFacade.onChange(event, remappedValue); + }; + + private readonly onInput = (event: Event) => { + const newValue = (event.target as HTMLInputElement).value; + const remappedValue = this.remapValue(newValue); + this.controller.onFacade.onInput(event, true, remappedValue); + }; + private readonly onKeyDown = (event: KeyboardEvent) => { if (event.code === 'Enter' || event.code === 'NumpadEnter') { propagateSubmitEventToForm({ @@ -157,6 +184,8 @@ export class KolInputDate implements InputDateAPI, FocusableElement { value={this.state._value || undefined} {...this.controller.onFacade} onKeyDown={this.onKeyDown} + onChange={this.onChange} + onInput={this.onInput} /> @@ -455,9 +484,11 @@ export class KolInputDate implements InputDateAPI, FocusableElement { deprecatedHint('Date type will be removed in v3. Use `Iso8601` instead.'); } this.controller.validateValueEx(value); + if (value !== undefined) this.setInitialValueType(value); } public componentWillLoad(): void { + if (this._value !== undefined) this.setInitialValueType(this._value); this._alert = this._alert === true; this._touched = this._touched === true; this.controller.componentWillLoad(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd672e0d09..d3813d069a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3566,6 +3566,15 @@ packages: rollup: optional: true + '@rollup/plugin-node-resolve@15.3.0': + resolution: {integrity: sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/plugin-replace@5.0.7': resolution: {integrity: sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==} engines: {node: '>=14.0.0'} @@ -4072,6 +4081,9 @@ packages: '@types/node@22.7.3': resolution: {integrity: sha512-qXKfhXXqGTyBskvWEzJZPUxSslAiLaB6JGP1ic/XTH9ctGgzdgYguuLP1C601aRTSDNlLb0jbKqXjZ48GNraSA==} + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -12712,6 +12724,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-create-class-features-plugin@7.25.4(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-member-expression-to-functions': 7.24.8 + '@babel/helper-optimise-call-expression': 7.24.7 + '@babel/helper-replace-supers': 7.25.0(@babel/core@7.24.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/traverse': 7.25.6 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/helper-create-class-features-plugin@7.25.4(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -16303,6 +16328,10 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/node@22.7.5': + dependencies: + undici-types: 6.19.8 + '@types/normalize-package-data@2.4.4': {} '@types/parse-json@4.0.2': {} @@ -16768,7 +16797,7 @@ snapshots: webpack: 5.95.0(@swc/core@1.5.28)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.95.0) - '@webpack-cli/info@1.5.0(webpack-cli@4.10.0(webpack-dev-server@4.15.2)(webpack@5.91.0))': + '@webpack-cli/info@1.5.0(webpack-cli@4.10.0)': dependencies: envinfo: 7.14.0 webpack-cli: 4.10.0(webpack-dev-server@4.15.2)(webpack@5.91.0) @@ -16778,7 +16807,7 @@ snapshots: webpack: 5.95.0(@swc/core@1.5.28)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.95.0) - '@webpack-cli/serve@1.7.0(webpack-cli@4.10.0(webpack-dev-server@4.15.2)(webpack@5.91.0))(webpack-dev-server@4.15.2(webpack-cli@4.10.0)(webpack@5.91.0))': + '@webpack-cli/serve@1.7.0(webpack-cli@4.10.0)(webpack-dev-server@4.15.2)': dependencies: webpack-cli: 4.10.0(webpack-dev-server@4.15.2)(webpack@5.91.0) optionalDependencies: @@ -20322,7 +20351,7 @@ snapshots: init-package-json@6.0.3: dependencies: '@npmcli/package-json': 5.2.0 - npm-package-arg: 11.0.2 + npm-package-arg: 11.0.3 promzard: 1.0.2 read: 3.0.1 semver: 7.6.3 @@ -21554,7 +21583,7 @@ snapshots: libnpmaccess@8.0.6: dependencies: - npm-package-arg: 11.0.2 + npm-package-arg: 11.0.3 npm-registry-fetch: 17.1.0 transitivePeerDependencies: - supports-color @@ -21563,7 +21592,7 @@ snapshots: dependencies: ci-info: 4.0.0 normalize-package-data: 6.0.2 - npm-package-arg: 11.0.2 + npm-package-arg: 11.0.3 npm-registry-fetch: 17.1.0 proc-log: 4.2.0 semver: 7.6.3 @@ -22557,7 +22586,7 @@ snapshots: minipass: 7.1.2 minipass-fetch: 3.0.5 minizlib: 2.1.2 - npm-package-arg: 11.0.2 + npm-package-arg: 11.0.3 proc-log: 4.2.0 transitivePeerDependencies: - supports-color