From 653334f28b40491ffce0bbb4589d01b2ce05eb43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 08:44:19 +0000 Subject: [PATCH 1/3] Initial plan From 768d50ef46c90b801a9891f1aef4f596232ed0c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 08:55:21 +0000 Subject: [PATCH 2/3] Implement Safari onChange fix with focus/blur tracking Co-authored-by: iOvergaard <752371+iOvergaard@users.noreply.github.com> --- packages/uui-input/lib/uui-input.element.ts | 19 ++++- packages/uui-input/lib/uui-input.test.ts | 81 +++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/packages/uui-input/lib/uui-input.element.ts b/packages/uui-input/lib/uui-input.element.ts index 80654db80..d81844490 100644 --- a/packages/uui-input/lib/uui-input.element.ts +++ b/packages/uui-input/lib/uui-input.element.ts @@ -226,6 +226,7 @@ export class UUIInputElement extends UUIFormControlMixin( _input!: HTMLInputElement; private _type: InputType = 'text'; + private _valueOnFocus: string = ''; constructor() { super(); @@ -316,6 +317,20 @@ export class UUIInputElement extends UUIFormControlMixin( this.dispatchEvent(new UUIInputEvent(UUIInputEvent.CHANGE)); } + protected onFocus() { + // Store the value when the input receives focus + this._valueOnFocus = this.value as string; + } + + protected onBlur() { + // Check if the value has changed since focus + // This ensures change events are fired on Safari even when native change events don't fire + if (this._valueOnFocus !== this.value) { + this.pristine = false; + this.dispatchEvent(new UUIInputEvent(UUIInputEvent.CHANGE)); + } + } + protected renderPrepend() { return html``; } @@ -359,7 +374,9 @@ export class UUIInputElement extends UUIFormControlMixin( ?readonly=${this.readonly} tabindex=${ifDefined(this.tabIndex)} @input=${this.onInput} - @change=${this.onChange} />`; + @change=${this.onChange} + @focus=${this.onFocus} + @blur=${this.onBlur} />`; } private renderAutoWidthBackground() { diff --git a/packages/uui-input/lib/uui-input.test.ts b/packages/uui-input/lib/uui-input.test.ts index 8118f5f08..3e1f79ffe 100644 --- a/packages/uui-input/lib/uui-input.test.ts +++ b/packages/uui-input/lib/uui-input.test.ts @@ -150,6 +150,87 @@ describe('UuiInputElement', () => { expect(innerEvent.type).to.equal(UUIInputEvent.CHANGE); expect(innerEvent!.target).to.equal(innerElement); }); + + it('emits a change event when value changes and input loses focus (Safari fix)', async () => { + // Simulate Safari behavior where native change event doesn't fire + const listener = oneEvent(element, UUIInputEvent.CHANGE); + + // Simulate user focusing the input + input.dispatchEvent(new Event('focus')); + await elementUpdated(element); + + // Simulate user typing + input.value = 'new value'; + input.dispatchEvent(new Event('input')); + await elementUpdated(element); + + // Simulate user blurring the input (without native change event) + input.dispatchEvent(new Event('blur')); + + const event = await listener; + expect(event).to.exist; + expect(event.type).to.equal(UUIInputEvent.CHANGE); + expect(element.value).to.equal('new value'); + }); + + it('does not emit change event on blur if value has not changed', async () => { + let changeEventCount = 0; + element.addEventListener(UUIInputEvent.CHANGE, () => { + changeEventCount++; + }); + + // Simulate user focusing the input + input.dispatchEvent(new Event('focus')); + await elementUpdated(element); + + // Don't change the value + + // Simulate user blurring the input + input.dispatchEvent(new Event('blur')); + await elementUpdated(element); + + // Wait a bit to ensure no event is fired + await sleep(50); + + expect(changeEventCount).to.equal(0); + }); + + it('handles multiple focus/blur cycles correctly', async () => { + let changeEventCount = 0; + element.addEventListener(UUIInputEvent.CHANGE, () => { + changeEventCount++; + }); + + // First cycle - change value + input.dispatchEvent(new Event('focus')); + await elementUpdated(element); + input.value = 'value1'; + input.dispatchEvent(new Event('input')); + await elementUpdated(element); + input.dispatchEvent(new Event('blur')); + await elementUpdated(element); + + expect(changeEventCount).to.equal(1); + + // Second cycle - no change + input.dispatchEvent(new Event('focus')); + await elementUpdated(element); + input.dispatchEvent(new Event('blur')); + await elementUpdated(element); + + expect(changeEventCount).to.equal(1); + + // Third cycle - change value again + input.dispatchEvent(new Event('focus')); + await elementUpdated(element); + input.value = 'value2'; + input.dispatchEvent(new Event('input')); + await elementUpdated(element); + input.dispatchEvent(new Event('blur')); + await elementUpdated(element); + + expect(changeEventCount).to.equal(2); + }); }); }); From 9733949d9bb09b344b4cb8d1c2b46461f4072001 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 08:58:20 +0000 Subject: [PATCH 3/3] Fix duplicate change events and improve type safety Co-authored-by: iOvergaard <752371+iOvergaard@users.noreply.github.com> --- packages/uui-input/lib/uui-input.element.ts | 11 +++++++-- packages/uui-input/lib/uui-input.test.ts | 26 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/uui-input/lib/uui-input.element.ts b/packages/uui-input/lib/uui-input.element.ts index d81844490..1b8efbfe7 100644 --- a/packages/uui-input/lib/uui-input.element.ts +++ b/packages/uui-input/lib/uui-input.element.ts @@ -227,6 +227,7 @@ export class UUIInputElement extends UUIFormControlMixin( private _type: InputType = 'text'; private _valueOnFocus: string = ''; + private _changeEventFiredSinceFocus: boolean = false; constructor() { super(); @@ -313,19 +314,25 @@ export class UUIInputElement extends UUIFormControlMixin( protected onChange(e: Event) { e.stopPropagation(); this.pristine = false; + this._changeEventFiredSinceFocus = true; this.dispatchEvent(new UUIInputEvent(UUIInputEvent.CHANGE)); } protected onFocus() { // Store the value when the input receives focus - this._valueOnFocus = this.value as string; + this._valueOnFocus = String(this.value || ''); + this._changeEventFiredSinceFocus = false; } protected onBlur() { // Check if the value has changed since focus // This ensures change events are fired on Safari even when native change events don't fire - if (this._valueOnFocus !== this.value) { + // Only fire if the native change event hasn't already fired to avoid duplicates + if ( + !this._changeEventFiredSinceFocus && + this._valueOnFocus !== this.value + ) { this.pristine = false; this.dispatchEvent(new UUIInputEvent(UUIInputEvent.CHANGE)); } diff --git a/packages/uui-input/lib/uui-input.test.ts b/packages/uui-input/lib/uui-input.test.ts index 3e1f79ffe..e053a1f38 100644 --- a/packages/uui-input/lib/uui-input.test.ts +++ b/packages/uui-input/lib/uui-input.test.ts @@ -231,6 +231,32 @@ describe('UuiInputElement', () => { expect(changeEventCount).to.equal(2); }); + + it('does not emit duplicate change events when native change fires (non-Safari browsers)', async () => { + let changeEventCount = 0; + element.addEventListener(UUIInputEvent.CHANGE, () => { + changeEventCount++; + }); + + // Simulate typical browser behavior where native change event fires + input.dispatchEvent(new Event('focus')); + await elementUpdated(element); + + input.value = 'new value'; + input.dispatchEvent(new Event('input')); + await elementUpdated(element); + + // Native change event fires + input.dispatchEvent(new Event('change')); + await elementUpdated(element); + + // Blur event fires after change + input.dispatchEvent(new Event('blur')); + await elementUpdated(element); + + // Should only fire ONE change event (from native change, not from blur) + expect(changeEventCount).to.equal(1); + }); }); });