From 07327be95e161b3947082bb07b1df3a9c52adf91 Mon Sep 17 00:00:00 2001 From: Konnor Rogers Date: Fri, 13 Jun 2025 13:19:47 -0400 Subject: [PATCH] input / change always bubble now (#1057) * make sure relayNativeEvent is synchronous * prettier * changelog entry * fix event --- .../docs/docs/resources/changelog.md | 3 +- .../src/components/checkbox/checkbox.ts | 4 +- .../color-picker/color-picker.test.ts | 1 + .../components/color-picker/color-picker.ts | 74 +++++++++++++------ .../webawesome/src/components/input/input.ts | 12 ++- .../src/components/radio-group/radio-group.ts | 12 ++- .../src/components/rating/rating.ts | 12 ++- .../src/components/select/select.ts | 8 +- .../src/components/slider/slider.ts | 31 ++++++-- .../src/components/switch/switch.ts | 17 +++-- .../src/components/textarea/textarea.ts | 5 +- 11 files changed, 125 insertions(+), 54 deletions(-) diff --git a/packages/webawesome/docs/docs/resources/changelog.md b/packages/webawesome/docs/docs/resources/changelog.md index 34299d20c..d8398fcb1 100644 --- a/packages/webawesome/docs/docs/resources/changelog.md +++ b/packages/webawesome/docs/docs/resources/changelog.md @@ -14,6 +14,7 @@ During the alpha period, things might break! We take breaking changes very serio ## Next +- 🚨 BREAKING: `input` and `change` events on form controls like `` now are always set to `bubble` and `compose`. - 🚨 BREAKING: Greatly simplified how native styles work and removed redundant utilities - Removed `.wa-button`, `.wa-callout` classes - Removed `themes/native/*.css` files; use `native.css` to opt into native styles @@ -377,4 +378,4 @@ Here's a list of some of the things that have changed since Shoelace v2. For que Did we miss something? [Let us know!](https://github.com/shoelace-style/webawesome-alpha/discussions) -Are you coming from Shoelace? [The 2.x changelog can be found here.](https://shoelace.style/resources/changelog/) +Are you coming from Shoelace? [The 2.x changelog can be found here.](https://shoelace.style/resources/changelog/) \ No newline at end of file diff --git a/packages/webawesome/src/components/checkbox/checkbox.ts b/packages/webawesome/src/components/checkbox/checkbox.ts index 7ee8e2f0a..8b9b2790d 100644 --- a/packages/webawesome/src/components/checkbox/checkbox.ts +++ b/packages/webawesome/src/components/checkbox/checkbox.ts @@ -132,7 +132,9 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement { this.hasInteracted = true; this.checked = !this.checked; this.indeterminate = false; - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } @watch('defaultChecked') diff --git a/packages/webawesome/src/components/color-picker/color-picker.test.ts b/packages/webawesome/src/components/color-picker/color-picker.test.ts index e46672d9c..d88d0f47e 100644 --- a/packages/webawesome/src/components/color-picker/color-picker.test.ts +++ b/packages/webawesome/src/components/color-picker/color-picker.test.ts @@ -300,6 +300,7 @@ describe('', () => { await sendKeys({ type: 'fc0' }); // type in a color input.blur(); // commit changes by blurring the field await el.updateComplete; + await aTimeout(1); expect(changeHandler).to.have.been.calledOnce; expect(inputHandler).to.have.been.calledOnce; diff --git a/packages/webawesome/src/components/color-picker/color-picker.ts b/packages/webawesome/src/components/color-picker/color-picker.ts index b85a6c325..90aeb55f4 100644 --- a/packages/webawesome/src/components/color-picker/color-picker.ts +++ b/packages/webawesome/src/components/color-picker/color-picker.ts @@ -275,8 +275,11 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement { const nextIndex = (formats.indexOf(this.format) + 1) % formats.length; this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl' | 'hsv'; this.setColor(this.value || ''); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); - this.dispatchEvent(new InputEvent('input')); + + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + }); } private handleAlphaDrag(event: PointerEvent) { @@ -296,13 +299,18 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement { if (this.value !== currentValue) { currentValue = this.value; - this.dispatchEvent(new InputEvent('input')); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + }); } }, onStop: () => { if (this.value !== initialValue) { initialValue = this.value; - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } }, initialEvent: event, @@ -326,13 +334,17 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement { if (this.value !== currentValue) { currentValue = this.value; - this.dispatchEvent(new InputEvent('input')); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input')); + }); } }, onStop: () => { if (this.value !== initialValue) { initialValue = this.value; - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } }, initialEvent: event, @@ -359,14 +371,18 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement { if (this.value !== currentValue) { currentValue = this.value; - this.dispatchEvent(new InputEvent('input')); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + }); } }, onStop: () => { this.isDraggingGridHandle = false; if (this.value !== initialValue) { initialValue = this.value; - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } }, initialEvent: event, @@ -402,8 +418,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement { } if (this.value !== oldValue) { - this.dispatchEvent(new InputEvent('input')); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } } @@ -436,8 +454,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement { } if (this.value !== oldValue) { - this.dispatchEvent(new InputEvent('input')); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } } @@ -470,8 +490,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement { } if (this.value !== oldValue) { - this.dispatchEvent(new InputEvent('input')); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } } @@ -490,8 +512,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement { } if (this.value !== oldValue) { - this.dispatchEvent(new InputEvent('input')); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } } @@ -511,8 +535,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement { this.input.value = this.value; if (this.value !== oldValue) { - this.dispatchEvent(new InputEvent('input')); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } setTimeout(() => this.input.select()); @@ -696,8 +722,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement { this.setColor(colorSelectionResult.sRGBHex); if (this.value !== oldValue) { - this.dispatchEvent(new InputEvent('input')); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } }) .catch(() => { @@ -712,8 +740,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement { this.setColor(color); if (this.value !== oldValue) { - this.dispatchEvent(new InputEvent('input')); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } } } diff --git a/packages/webawesome/src/components/input/input.ts b/packages/webawesome/src/components/input/input.ts index 264ef780d..c4a3b1994 100644 --- a/packages/webawesome/src/components/input/input.ts +++ b/packages/webawesome/src/components/input/input.ts @@ -223,8 +223,9 @@ export default class WaInput extends WebAwesomeFormAssociatedElement { @property({ attribute: 'with-hint', type: Boolean }) withHint = false; private handleChange(event: Event) { - this.relayNativeEvent(event, { bubbles: true, composed: true }); this.value = this.input.value; + + this.relayNativeEvent(event, { bubbles: true, composed: true }); } private handleClearClick(event: MouseEvent) { @@ -232,9 +233,12 @@ export default class WaInput extends WebAwesomeFormAssociatedElement { if (this.value !== '') { this.value = ''; - this.dispatchEvent(new WaClearEvent()); - this.dispatchEvent(new InputEvent('input')); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + + this.updateComplete.then(() => { + this.dispatchEvent(new WaClearEvent()); + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } this.input.focus(); diff --git a/packages/webawesome/src/components/radio-group/radio-group.ts b/packages/webawesome/src/components/radio-group/radio-group.ts index e118c75ce..b99ccb53f 100644 --- a/packages/webawesome/src/components/radio-group/radio-group.ts +++ b/packages/webawesome/src/components/radio-group/radio-group.ts @@ -171,8 +171,10 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement { } if (this.value !== oldValue) { - this.dispatchEvent(new InputEvent('input')); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } }; @@ -274,8 +276,10 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement { } if (this.value !== oldValue) { - this.dispatchEvent(new InputEvent('input')); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } event.preventDefault(); diff --git a/packages/webawesome/src/components/rating/rating.ts b/packages/webawesome/src/components/rating/rating.ts index e918289b3..005c24180 100644 --- a/packages/webawesome/src/components/rating/rating.ts +++ b/packages/webawesome/src/components/rating/rating.ts @@ -101,7 +101,9 @@ export default class WaRating extends WebAwesomeElement { } this.setValue(this.getValueFromMousePosition(event)); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } private setValue(newValue: number) { @@ -145,7 +147,9 @@ export default class WaRating extends WebAwesomeElement { } if (this.value !== oldValue) { - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } } @@ -178,7 +182,9 @@ export default class WaRating extends WebAwesomeElement { private handleTouchEnd(event: TouchEvent) { this.isHovering = false; this.setValue(this.hoverValue); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); // Prevent click on mobile devices event.preventDefault(); diff --git a/packages/webawesome/src/components/select/select.ts b/packages/webawesome/src/components/select/select.ts index 860c71335..18998413e 100644 --- a/packages/webawesome/src/components/select/select.ts +++ b/packages/webawesome/src/components/select/select.ts @@ -381,7 +381,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement { // Emit after updating this.updateComplete.then(() => { - this.dispatchEvent(new InputEvent('input')); + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); }); @@ -511,7 +511,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement { // Emit after update this.updateComplete.then(() => { this.dispatchEvent(new WaClearEvent()); - this.dispatchEvent(new InputEvent('input')); + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); }); } @@ -542,7 +542,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement { if (this.value !== oldValue) { // Emit after updating this.updateComplete.then(() => { - this.dispatchEvent(new InputEvent('input')); + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); }); } @@ -600,7 +600,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement { // Emit after updating this.updateComplete.then(() => { - this.dispatchEvent(new InputEvent('input')); + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); }); } diff --git a/packages/webawesome/src/components/slider/slider.ts b/packages/webawesome/src/components/slider/slider.ts index b0cc5dc4b..cff84d151 100644 --- a/packages/webawesome/src/components/slider/slider.ts +++ b/packages/webawesome/src/components/slider/slider.ts @@ -228,7 +228,9 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement { }, stop: () => { if (this.minValue !== this.valueWhenDraggingStarted) { - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); this.hasInteracted = true; } this.hideRangeTooltips(); @@ -251,7 +253,9 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement { }, stop: () => { if (this.maxValue !== this.valueWhenDraggingStarted) { - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); this.hasInteracted = true; } this.hideRangeTooltips(); @@ -321,7 +325,9 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement { if (this.activeThumb) { const currentValue = this.activeThumb === 'min' ? this.minValue : this.maxValue; if (currentValue !== this.valueWhenDraggingStarted) { - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); this.hasInteracted = true; } } @@ -346,7 +352,10 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement { }, stop: () => { if (this.value !== this.valueWhenDraggingStarted) { - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); + this.hasInteracted = true; } this.hideTooltip(); @@ -602,8 +611,10 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement { } // Dispatch events - this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); this.hasInteracted = true; } @@ -625,7 +636,9 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement { // Dispatch input events when the value changes by dragging if (this.value !== oldValue) { - this.dispatchEvent(new InputEvent('input')); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + }); } } @@ -658,8 +671,10 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement { // Dispatch input events if (oldValue !== (thumb === 'min' ? this.minValue : this.maxValue)) { - this.dispatchEvent(new InputEvent('input')); this.updateFormValue(); + this.updateComplete.then(() => { + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + }); } } diff --git a/packages/webawesome/src/components/switch/switch.ts b/packages/webawesome/src/components/switch/switch.ts index 086714d28..1297f3db2 100644 --- a/packages/webawesome/src/components/switch/switch.ts +++ b/packages/webawesome/src/components/switch/switch.ts @@ -117,22 +117,29 @@ export default class WaSwitch extends WebAwesomeFormAssociatedElement { private handleClick() { this.hasInteracted = true; this.checked = !this.checked; - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + }); } private handleKeyDown(event: KeyboardEvent) { if (event.key === 'ArrowLeft') { event.preventDefault(); this.checked = false; - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); - this.dispatchEvent(new InputEvent('input')); + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + }); } if (event.key === 'ArrowRight') { event.preventDefault(); this.checked = true; - this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); - this.dispatchEvent(new InputEvent('input')); + + this.updateComplete.then(() => { + this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + }); } } diff --git a/packages/webawesome/src/components/textarea/textarea.ts b/packages/webawesome/src/components/textarea/textarea.ts index 812aab7b2..27b920b12 100644 --- a/packages/webawesome/src/components/textarea/textarea.ts +++ b/packages/webawesome/src/components/textarea/textarea.ts @@ -206,13 +206,14 @@ export default class WaTextarea extends WebAwesomeFormAssociatedElement { this.valueHasChanged = true; this.value = this.input.value; this.setTextareaDimensions(); - this.relayNativeEvent(event, { bubbles: true, composed: true }); this.checkValidity(); + this.relayNativeEvent(event, { bubbles: true, composed: true }); } - private handleInput() { + private handleInput(event: InputEvent) { this.valueHasChanged = true; this.value = this.input.value; + this.relayNativeEvent(event, { bubbles: true, composed: true }); } private setTextareaDimensions() {