Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 141 additions & 38 deletions packages/main/cypress/specs/ToolbarSelect.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ describe("Toolbar general interaction", () => {
});

cy.get("@toolbarSelect").then($select => {
$select.get(0).addEventListener("ui5-change", (e) => {
$select.get(0).addEventListener("ui5-change", (_: Event) => {
const input = document.querySelector("[data-testid='selectResult']") as HTMLInputElement;
input.value = "1";
});
Expand Down Expand Up @@ -400,7 +400,6 @@ describe("Toolbar general interaction", () => {

describe("value and label properties", () => {
it("Should verify the initial value of the ToolbarSelect", () => {
// Mount the Toolbar with a ToolbarSelect component
cy.mount(
<Toolbar>
<ToolbarSelect value="Option 2">
Expand All @@ -412,13 +411,16 @@ describe("Toolbar general interaction", () => {
</Toolbar>
);

// Verify the initial value of the ToolbarSelect
cy.get("ui5-select", { includeShadowDom: true })
.should("have.attr", "value", "Option 2");
cy.get("[ui5-toolbar]")
.find("[ui5-toolbar-select]")
.shadow()
.find("[ui5-select]")
.find("[ui5-option]")
.eq(1)
.should("have.attr", "selected");
});

it("Should verify the label slot content", () => {
// Mount the Toolbar with a ToolbarSelect component
cy.mount(
<Toolbar>
<ToolbarSelect value="Option 2">
Expand All @@ -430,14 +432,12 @@ describe("Toolbar general interaction", () => {
</Toolbar>
);

// Verify the label slot content
cy.get("ui5-toolbar-select")
cy.get("[ui5-toolbar-select]")
.find("span[slot='label']")
.should("contain.text", "Select an Option:");
});

it("Should change the value and update the selection", () => {
// Mount the Toolbar with a ToolbarSelect component
cy.mount(
<Toolbar>
<ToolbarSelect value="Option 2">
Expand All @@ -448,20 +448,25 @@ describe("Toolbar general interaction", () => {
</Toolbar>
);

// Change the value of the ToolbarSelect
cy.get("ui5-select", { includeShadowDom: true })
cy.get("[ui5-toolbar]")
.find("[ui5-toolbar-select]")
.shadow()
.find("[ui5-select]")
.realClick()
.find("ui5-option")
.find("[ui5-option]")
.contains("Option 3")
.realClick();

// Verify the updated value of the ToolbarSelect
cy.get("ui5-select", { includeShadowDom: true })
.should("have.attr", "value", "Option 3");
cy.get("[ui5-toolbar]")
.find("[ui5-toolbar-select]")
.shadow()
.find("[ui5-select]")
.find("[ui5-option]")
.eq(2)
.should("have.attr", "selected");
});

it("Should handle a value with no corresponding option", () => {
// Mount the Toolbar with a ToolbarSelect component
cy.mount(
<Toolbar>
<ToolbarSelect value="NonExistentOption">
Expand All @@ -472,13 +477,12 @@ describe("Toolbar general interaction", () => {
</Toolbar>
);

// Verify that no option is selected when the value does not match any option
cy.get("ui5-select", { includeShadowDom: true })
.should("have.attr", "value", "NonExistentOption");
cy.get("[ui5-toolbar-select-option]").eq(0).should("not.have.attr", "selected");
cy.get("[ui5-toolbar-select-option]").eq(1).should("not.have.attr", "selected");
cy.get("[ui5-toolbar-select-option]").eq(2).should("not.have.attr", "selected");
});

it("Should update the value programmatically and reflect the selection", () => {
// Mount the Toolbar with a ToolbarSelect component
cy.mount(
<Toolbar>
<ToolbarSelect value="Option 1">
Expand All @@ -489,12 +493,15 @@ describe("Toolbar general interaction", () => {
</Toolbar>
);

// Update the value programmatically
cy.get("ui5-toolbar-select").invoke("attr", "value", "Option 3");
cy.get("[ui5-toolbar-select]").invoke("attr", "value", "Option 3");

// Verify the updated value and selection
cy.get("ui5-select", { includeShadowDom: true })
.should("have.attr", "value", "Option 3");
cy.get("[ui5-toolbar]")
.find("[ui5-toolbar-select]")
.shadow()
.find("[ui5-select]")
.find("[ui5-option]")
.eq(2)
.should("have.attr", "selected");
});
});

Expand All @@ -518,7 +525,7 @@ describe("Toolbar general interaction", () => {
});

// Verify the toolbar-select is rendered inside the popover
cy.get("ui5-toolbar-select").should("be.visible");
cy.get("[ui5-toolbar-select]").should("be.visible");
});

it("Should update selection when option's selected property is changed programmatically", () => {
Expand All @@ -539,8 +546,8 @@ describe("Toolbar general interaction", () => {
// Attach click handler once the button is in the DOM
cy.get("#btn").then($btn => {
$btn.get(0).addEventListener("click", () => {
const select = document.querySelector("ui5-toolbar-select");
const options = select?.querySelectorAll("ui5-toolbar-select-option");
const select = document.querySelector("[ui5-toolbar-select]");
const options = select?.querySelectorAll("[ui5-toolbar-select-option]");
options?.forEach(opt => {
(opt as ToolbarSelectOption).selected = false;
});
Expand All @@ -565,11 +572,16 @@ describe("Toolbar general interaction", () => {
});

// The rendered select reflects option 2 as the selected value
cy.get("ui5-select", { includeShadowDom: true })
.should("have.attr", "value", "2");
cy.get("[ui5-toolbar]")
.find("[ui5-toolbar-select]")
.shadow()
.find("[ui5-select]")
.find("[ui5-option]")
.eq(1)
.should("have.attr", "selected");
});

it("Should ensure only one option is selected at any time", () => {
it("Should display the last selected option when multiple options are set selected", () => {
cy.mount(
<>
<Toolbar>
Expand All @@ -583,28 +595,119 @@ describe("Toolbar general interaction", () => {
</>
);

// Set up button to attempt selecting multiple options
cy.document().then(doc => {
const btn = doc.getElementById("selectMultiple");
btn?.addEventListener("click", () => {
const opt1 = doc.getElementById("opt1") as ToolbarSelectOption;
const opt2 = doc.getElementById("opt2") as ToolbarSelectOption;
const opt3 = doc.getElementById("opt3") as ToolbarSelectOption;

// Try to select multiple options
opt1.selected = true;
opt2.selected = true;
opt3.selected = true; // This should be the final selection
opt3.selected = true;
});
});

// Click button to attempt multiple selections
cy.get("#selectMultiple").realClick();

// Verify only the last option (opt3) is selected
cy.get("[ui5-toolbar-select-option]").eq(2).should("have.attr", "selected");
// Inner display shows the last selected option (opt3)
cy.get("[ui5-toolbar]")
.find("[ui5-toolbar-select]")
.shadow()
.find("[ui5-select]")
.find("[ui5-option]")
.eq(2)
.should("have.attr", "selected");
});

it("Should clear outer selection when value is set to empty string after render", () => {
cy.mount(
<Toolbar>
<ToolbarSelect value="Option 2">
<ToolbarSelectOption>Option 1</ToolbarSelectOption>
<ToolbarSelectOption>Option 2</ToolbarSelectOption>
<ToolbarSelectOption>Option 3</ToolbarSelectOption>
</ToolbarSelect>
</Toolbar>
);

cy.document().then(doc => {
const select = doc.querySelector("[ui5-toolbar-select]") as ToolbarSelect;
select.value = "";
});

cy.get("[ui5-toolbar-select-option]").eq(0).should("not.have.attr", "selected");
cy.get("[ui5-toolbar-select-option]").eq(1).should("not.have.attr", "selected");
cy.get("ui5-select", { includeShadowDom: true }).should("have.attr", "value", "3");
cy.get("[ui5-toolbar-select-option]").eq(2).should("not.have.attr", "selected");
});

it("Should not let stale _pendingValue override a later programmatic selected change", () => {
cy.mount(
<>
<Toolbar>
<ToolbarSelect value="Option 1">
<ToolbarSelectOption id="prog-opt1">Option 1</ToolbarSelectOption>
<ToolbarSelectOption id="prog-opt2">Option 2</ToolbarSelectOption>
<ToolbarSelectOption id="prog-opt3">Option 3</ToolbarSelectOption>
</ToolbarSelect>
</Toolbar>
<Button id="prog-btn">Select Option 3</Button>
</>
);

cy.get("#prog-btn").then($btn => {
$btn.get(0).addEventListener("click", () => {
const options = document.querySelectorAll("[ui5-toolbar-select-option]");
options.forEach(opt => { (opt as ToolbarSelectOption).selected = false; });
(document.getElementById("prog-opt3") as ToolbarSelectOption).selected = true;
});
});

cy.get("#prog-btn").realClick();

cy.get("[ui5-toolbar-select-option]").eq(2).should("have.attr", "selected");
cy.get("[ui5-toolbar-select-option]").eq(0).should("not.have.attr", "selected");
cy.get("[ui5-toolbar]")
.find("[ui5-toolbar-select]")
.shadow()
.find("[ui5-select]")
.find("[ui5-option]")
.eq(2)
.should("have.attr", "selected");
});

it("Should not let _pendingValue set by value='X' override a later option.selected change before pending is cleared", () => {
cy.mount(
<>
<Toolbar>
<ToolbarSelect id="pending-override" value="Option 1">
<ToolbarSelectOption id="pending-opt1">Option 1</ToolbarSelectOption>
<ToolbarSelectOption id="pending-opt2">Option 2</ToolbarSelectOption>
<ToolbarSelectOption id="pending-opt3">Option 3</ToolbarSelectOption>
</ToolbarSelect>
</Toolbar>
<Button id="pending-btn">Select Option 2 by selected</Button>
</>
);

cy.get("#pending-btn").then($btn => {
$btn.get(0).addEventListener("click", () => {
const options = document.querySelectorAll("[ui5-toolbar-select-option]");
options.forEach(opt => { (opt as ToolbarSelectOption).selected = false; });
(document.getElementById("pending-opt2") as ToolbarSelectOption).selected = true;
});
});

cy.get("#pending-btn").realClick();

cy.get("[ui5-toolbar-select-option]").eq(1).should("have.attr", "selected");
cy.get("[ui5-toolbar-select-option]").eq(0).should("not.have.attr", "selected");
cy.get("[ui5-toolbar]")
.find("[ui5-toolbar-select]")
.shadow()
.find("[ui5-select]")
.find("[ui5-option]")
.eq(1)
.should("have.attr", "selected");
});
});
45 changes: 39 additions & 6 deletions packages/main/src/ToolbarSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,22 +154,36 @@ class ToolbarSelect extends ToolbarItemBase {
*/
@property()
set value(newValue: string) {
if (this.select && this.select.value !== newValue) {
this.select.value = newValue;
if (this.options.length) {
// Options are available: resolve immediately by setting selected on the matching outer option.
// Empty string clears all selections.
this.options.forEach(option => {
option.selected = newValue !== "" && (option.value === newValue || option.textContent?.trim() === newValue);
});
this._pendingValue = "";
this._hasPendingValue = false;
} else {
// Options not yet available (pre-render): stage for onBeforeRendering to resolve.
this._pendingValue = newValue;
this._hasPendingValue = true;
}
this._value = newValue;
}

get value(): string | undefined {
return this.select ? this.select.value : this._value;
const selectedOption = this.options.find(o => o.selected);
return selectedOption?.value || selectedOption?.textContent?.trim() || "";
}

get select(): Select | null {
return this.shadowRoot!.querySelector<Select>("[ui5-select]");
}

// Internal value storage, in case the composite select is not rendered on the the assignment happens
_value: string = "";
// Staging buffer for value= assignments that arrive before options are available.
_pendingValue: string = "";
_hasPendingValue: boolean = false;

// Computed in onBeforeRendering: index of the last selected option (-1 = none)
_lastSelectedIndex: number = -1;

onClick(e: Event): void {
e.stopImmediatePropagation();
Expand All @@ -195,6 +209,23 @@ class ToolbarSelect extends ToolbarItemBase {
}
}

onBeforeRendering(): void {
super.onBeforeRendering();

// Resolve a pending value= assignment now that options are available.
if (this._hasPendingValue && this.options.length) {
const pending = this._pendingValue;
this.options.forEach(option => {
option.selected = pending !== "" && (option.value === pending || option.textContent?.trim() === pending);
});
this._pendingValue = "";
this._hasPendingValue = false;
}

// Last selected wins — mirrors Select._applyAutoSelection behaviour.
this._lastSelectedIndex = this.options.reduce((last, option, index) => (option.selected ? index : last), -1);
}

onChange(e: CustomEvent<SelectChangeEventDetail>): void {
e.stopImmediatePropagation();
const selectedOptionIndex = Number(e.detail.selectedOption?.getAttribute("data-ui5-external-action-item-index"));
Expand All @@ -208,6 +239,8 @@ class ToolbarSelect extends ToolbarItemBase {
}

_syncOptions(selectedOptionIndex: number): void {
this._pendingValue = "";
this._hasPendingValue = false;
this.options.forEach((option: ToolbarSelectOption, index: number) => {
option.selected = index === selectedOptionIndex;
});
Expand Down
Loading
Loading