From 620759f632a4d50208b1e15cad0204bbef4da000 Mon Sep 17 00:00:00 2001 From: Nikola Anachkov Date: Fri, 14 Nov 2025 13:04:38 +0200 Subject: [PATCH] fix(ui5-toolbar-select): sync value when option selected property changes programmatically --- .../main/cypress/specs/ToolbarSelect.cy.tsx | 52 +++++++++++++++++-- packages/main/src/ToolbarSelect.ts | 47 ++++++++++++++--- 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/packages/main/cypress/specs/ToolbarSelect.cy.tsx b/packages/main/cypress/specs/ToolbarSelect.cy.tsx index 3da2eb1a5b6a..912973538ce2 100644 --- a/packages/main/cypress/specs/ToolbarSelect.cy.tsx +++ b/packages/main/cypress/specs/ToolbarSelect.cy.tsx @@ -1,6 +1,7 @@ import Toolbar from "../../src/Toolbar.js"; import ToolbarSelect from "../../src/ToolbarSelect.js"; import ToolbarSelectOption from "../../src/ToolbarSelectOption.js"; +import Button from "../../src/Button.js"; describe("Toolbar general interaction", () => { it("Should render the select with the correct attributes", () => { @@ -264,10 +265,10 @@ describe("Toolbar general interaction", () => { cy.mount( - 1 - 2 - 3 - + 1 + 2 + 3 + ); cy.viewport(220, 1080); // Set a small viewport width to trigger overflow @@ -282,4 +283,47 @@ describe("Toolbar general interaction", () => { // Verify the toolbar-select is rendered inside the popover cy.get("ui5-toolbar-select").should("be.visible"); }); + + it("Should update ToolbarSelect value when option selected property changes via button click", () => { + cy.mount( + <> + + + Option 1 + Option 2 + + + + + ); + + // Wait for component to render + cy.wait(500); + + // Initial check - Option 1 should be selected + cy.get("[ui5-toolbar]") + .find("[ui5-toolbar-select]") + .should("have.prop", "value", "Option 1"); + + // Set up button click handler + cy.get("[ui5-button]").then($btn => { + $btn.get(0).addEventListener("click", () => { + const opt1 = document.getElementById("opt1") as ToolbarSelectOption; + const opt2 = document.getElementById("opt2") as ToolbarSelectOption; + opt1.selected = false; + opt2.selected = true; + }); + }); + + // Click the button + cy.get("[ui5-button]").realClick(); + + // Wait for update + cy.wait(200); + + // Verify the ToolbarSelect value property updated + cy.get("[ui5-toolbar]") + .find("[ui5-toolbar-select]") + .should("have.prop", "value", "Option 2"); + }); }); \ No newline at end of file diff --git a/packages/main/src/ToolbarSelect.ts b/packages/main/src/ToolbarSelect.ts index 2e8451f9973d..cb5ccdbe1b0b 100644 --- a/packages/main/src/ToolbarSelect.ts +++ b/packages/main/src/ToolbarSelect.ts @@ -4,6 +4,7 @@ import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import type ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; +import type { ChangeInfo } from "@ui5/webcomponents-base/dist/UI5Element.js"; import ToolbarSelectCss from "./generated/themes/ToolbarSelect.css.js"; import type Select from "./Select.js"; @@ -91,6 +92,7 @@ class ToolbarSelect extends ToolbarItem { @slot({ "default": true, type: HTMLElement, + invalidateOnChildChange: true, }) options!: Array; @@ -146,14 +148,19 @@ class ToolbarSelect extends ToolbarItem { */ @property() set value(newValue: string) { - if (this.select && this.select.value !== newValue) { - this.select.value = newValue; - } this._value = newValue; + const selectElement = this.select; + if (selectElement && selectElement.value !== newValue) { + selectElement.value = newValue; + } } get value(): string | undefined { - return this.select ? this.select.value : this._value; + // Always return _value if it's set, as it represents the source of truth + if (this._value !== undefined && this._value !== "") { + return this._value; + } + return this.select ? this.select.value : undefined; } get select(): Select | null { @@ -163,6 +170,30 @@ class ToolbarSelect extends ToolbarItem { // Internal value storage, in case the composite select is not rendered on the the assignment happens _value: string = ""; + onInvalidation(changeInfo: ChangeInfo) { + // When a child ToolbarSelectOption's selected property changes, update the value + if (changeInfo.reason === "childchange") { + const selectedOption = this.options.find(option => option.selected); + if (selectedOption) { + const newValue = selectedOption.textContent || ""; + // Update both internal value and the select component's value + this._value = newValue; + // Cache the select reference to avoid multiple DOM queries + const selectElement = this.select; + if (selectElement && selectElement.value !== newValue) { + selectElement.value = newValue; + } + } else { + // If no option is selected, clear the value + this._value = ""; + const selectElement = this.select; + if (selectElement) { + selectElement.value = ""; + } + } + } + } + onClick(e: Event): void { e.stopImmediatePropagation(); const prevented = !this.fireDecoratorEvent("click", { targetRef: e.target as HTMLElement }); @@ -189,12 +220,16 @@ class ToolbarSelect extends ToolbarItem { onChange(e: CustomEvent): void { e.stopImmediatePropagation(); + + // Update internal value BEFORE firing the change event + // so that when event listeners read this.value, they get the updated value + this._value = e.detail.selectedOption?.textContent || ""; + this._syncOptions(e.detail.selectedOption); + const prevented = !this.fireDecoratorEvent("change", { ...e.detail, targetRef: e.target as HTMLElement }); if (!prevented) { this.fireDecoratorEvent("close-overflow"); } - - this._syncOptions(e.detail.selectedOption); } _syncOptions(selectedOption: HTMLElement): void {