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
281 changes: 278 additions & 3 deletions packages/main/cypress/specs/Toolbar.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import ToolbarSelectOption from "../../src/ToolbarSelectOption.js";
import ToolbarSeparator from "../../src/ToolbarSeparator.js";
import ToolbarSpacer from "../../src/ToolbarSpacer.js";
import ToolbarItem from "../../src/ToolbarItem.js";
import CheckBox from "../../src/CheckBox.js";
import add from "@ui5/webcomponents-icons/dist/add.js";
import decline from "@ui5/webcomponents-icons/dist/decline.js";
import employee from "@ui5/webcomponents-icons/dist/employee.js";
import Button from "../../src/Button.js";
import Dialog from "../../src/Dialog.js";
import Input from "../../src/Input.js";

describe("Toolbar general interaction", () => {
it("Should not return null upon calling getDomRef for all direct child items", () => {
Expand Down Expand Up @@ -46,6 +48,256 @@ describe("Toolbar general interaction", () => {
});
});

it("Should move focus inside checkbox group and leave group on boundary with single arrow press", () => {
cy.mount(
<Toolbar id="checkbox-group-toolbar">
<ToolbarItem>
<CheckBox text="Checkbox 1"></CheckBox>
<CheckBox text="Checkbox 2" checked></CheckBox>
<CheckBox text="Checkbox 3"></CheckBox>
</ToolbarItem>
<ToolbarButton text="After group"></ToolbarButton>
</Toolbar>
);

cy.get("[ui5-checkbox][text='Checkbox 1']")
.realClick()
.should("be.focused");

cy.realPress("ArrowRight");
cy.get("[ui5-checkbox][text='Checkbox 2']")
.should("be.focused");

cy.realPress("ArrowRight");
cy.get("[ui5-checkbox][text='Checkbox 3']")
.should("be.focused");

cy.realPress("ArrowRight");
cy.get("[ui5-toolbar-button][text='After group']")
.should("be.focused");
});

it("Should navigate from overflow button to last visible toolbar item with ArrowLeft", () => {
cy.viewport(320, 1080);

cy.mount(
<Toolbar id="overflow-arrow-toolbar" style={{ width: "220px" }}>
<ToolbarButton text="One Long"></ToolbarButton>
<ToolbarButton text="Two Long"></ToolbarButton>
<ToolbarButton text="Three Long"></ToolbarButton>
<ToolbarButton text="Four Long"></ToolbarButton>
<ToolbarButton text="Five Long"></ToolbarButton>
</Toolbar>
);

cy.get("#overflow-arrow-toolbar")
.shadow()
.find(".ui5-tb-overflow-btn")
.should("not.have.class", "ui5-tb-overflow-btn-hidden");

cy.get("#overflow-arrow-toolbar")
.then($toolbar => {
const toolbar = $toolbar[0] as Toolbar & {
_setCurrentItem: (item: ToolbarItem | HTMLElement) => void;
overflowButtonDOM: HTMLElement;
};
toolbar._setCurrentItem(toolbar.overflowButtonDOM);
toolbar.overflowButtonDOM.focus();
});

cy.realPress("ArrowLeft");

// After ArrowLeft from overflow button, focus lands on the last visible toolbar item
cy.get("#overflow-arrow-toolbar")
.then($toolbar => {
const toolbar = $toolbar[0] as Toolbar & {
_lastFocusedItem?: ToolbarItem | HTMLElement;
_getNavigableItems: () => ToolbarItem[];
};
const navigableItems = toolbar._getNavigableItems();
const expectedLastItem = navigableItems.at(-1);
expect(toolbar._lastFocusedItem).to.equal(expectedLastItem);
});
});

it("Should navigate between toolbar items with Left/Right arrow keys", () => {
cy.mount(
<Toolbar id="arrow-nav-toolbar">
<ToolbarButton text="First"></ToolbarButton>
<ToolbarButton text="Second"></ToolbarButton>
<ToolbarButton text="Third"></ToolbarButton>
</Toolbar>
);

cy.get("[ui5-toolbar-button][text='First']").realClick().should("be.focused");

cy.realPress("ArrowRight");
cy.get("[ui5-toolbar-button][text='Second']").should("be.focused");

cy.realPress("ArrowRight");
cy.get("[ui5-toolbar-button][text='Third']").should("be.focused");

cy.realPress("ArrowLeft");
cy.get("[ui5-toolbar-button][text='Second']").should("be.focused");
});

it("Should move focus to first/last item with Home/End keys", () => {
cy.mount(
<Toolbar id="home-end-toolbar">
<ToolbarButton text="First"></ToolbarButton>
<ToolbarButton text="Second"></ToolbarButton>
<ToolbarButton text="Last"></ToolbarButton>
</Toolbar>
);

cy.get("[ui5-toolbar-button][text='Second']").realClick().should("be.focused");

cy.realPress("End");
cy.get("[ui5-toolbar-button][text='Last']").should("be.focused");

cy.realPress("Home");
cy.get("[ui5-toolbar-button][text='First']").should("be.focused");
});


it("Should not wrap on ArrowRight at last item or ArrowLeft at first item", () => {
cy.mount(
<Toolbar id="no-wrap-toolbar">
<ToolbarButton text="First"></ToolbarButton>
<ToolbarButton text="Second"></ToolbarButton>
<ToolbarButton text="Third"></ToolbarButton>
</Toolbar>
);

// ArrowLeft on first item does nothing
cy.get("[ui5-toolbar-button][text='First']").realClick().should("be.focused");
cy.realPress("ArrowLeft");
cy.get("[ui5-toolbar-button][text='First']").should("be.focused");

// ArrowRight on last item does nothing
cy.get("[ui5-toolbar-button][text='Third']").realClick().should("be.focused");
cy.realPress("ArrowRight");
cy.get("[ui5-toolbar-button][text='Third']").should("be.focused");
});

it("Should allow Tab to navigate between toolbar items (all items have tabIndex=0)", () => {
cy.mount(
<Toolbar id="tab-index-toolbar">
<ToolbarButton text="A"></ToolbarButton>
<ToolbarButton text="B"></ToolbarButton>
<ToolbarButton text="C"></ToolbarButton>
</Toolbar>
);

cy.get("[ui5-toolbar-button][text='A']").realClick().should("be.focused");

cy.realPress("Tab");
cy.get("[ui5-toolbar-button][text='B']").should("be.focused");

cy.realPress("Tab");
cy.get("[ui5-toolbar-button][text='C']").should("be.focused");
});

it("Should respect Input caret position when deciding arrow navigation", () => {
cy.mount(
<Toolbar id="input-caret-toolbar">
<ToolbarButton text="Before"></ToolbarButton>
<ToolbarItem>
<Input id="toolbar-input" value="hello"></Input>
</ToolbarItem>
<ToolbarButton text="After"></ToolbarButton>
</Toolbar>
);

// Focus the input and move caret to start
cy.get("#toolbar-input").realClick();
cy.get("#toolbar-input").realPress("Home");

// ArrowLeft at caret=0 (atLeftEnd) → toolbar moves to previous item
cy.realPress("ArrowLeft");
cy.get("[ui5-toolbar-button][text='Before']").should("be.focused");

// Now focus input and move caret to end
cy.get("#toolbar-input").realClick();
cy.get("#toolbar-input").realPress("End");

// ArrowRight at caret=end (atRightEnd) → toolbar moves to next item
cy.realPress("ArrowRight");
cy.get("[ui5-toolbar-button][text='After']").should("be.focused");
});

it("Should keep focus within Input while caret is not at boundary", () => {
cy.mount(
<Toolbar id="input-mid-caret-toolbar">
<ToolbarButton text="Before"></ToolbarButton>
<ToolbarItem>
<Input id="mid-toolbar-input" value="hello"></Input>
</ToolbarItem>
<ToolbarButton text="After"></ToolbarButton>
</Toolbar>
);

cy.get("#mid-toolbar-input").realClick();

// Home key is NOT intercepted by toolbar when an Input has focus (caret-aware items)
cy.realPress("Home");
cy.get("#mid-toolbar-input").should("be.focused");

// Set caret to position 2 (mid-string) via DOM to avoid End key exiting to last item
cy.get("#mid-toolbar-input").then($input => {
const ui5Input = $input[0] as Input & { nativeInput: HTMLInputElement | null };
const native = ui5Input.nativeInput;
if (native) {
native.focus();
native.setSelectionRange(2, 2);
}
});

// ArrowRight with caret mid-string → stays in input, toolbar does not navigate
cy.realPress("ArrowRight");
cy.get("#mid-toolbar-input").should("be.focused");

// Move caret back to mid
cy.get("#mid-toolbar-input").then($input => {
const ui5Input = $input[0] as Input & { nativeInput: HTMLInputElement | null };
const native = ui5Input.nativeInput;
if (native) {
native.focus();
native.setSelectionRange(2, 2);
}
});

// ArrowLeft with caret mid-string → stays in input
cy.realPress("ArrowLeft");
cy.get("#mid-toolbar-input").should("be.focused");
});

it("Should focus first overflow item when overflow popover opens", () => {
cy.mount(
<Toolbar id="overflow-focus-toolbar">
<ToolbarButton text="One" overflow-priority="AlwaysOverflow"></ToolbarButton>
<ToolbarButton text="Two" overflow-priority="AlwaysOverflow"></ToolbarButton>
<ToolbarButton text="Three" overflow-priority="AlwaysOverflow"></ToolbarButton>
</Toolbar>
);

cy.wait(500);

cy.get("[ui5-toolbar]")
.shadow()
.find(".ui5-tb-overflow-btn")
.should("not.have.class", "ui5-tb-overflow-btn-hidden")
.realClick();

cy.get("[ui5-toolbar]")
.shadow()
.find(".ui5-overflow-popover")
.should("have.attr", "open", "open");

cy.get("[ui5-toolbar-button][text='One']")
.should("be.focused");
});

it("shouldn't have toolbar button as popover opener when there is spacer before last toolbar item", () => {
cy.mount(
<Toolbar id="otb_spacer">
Expand Down Expand Up @@ -162,6 +414,30 @@ describe("Toolbar general interaction", () => {
.should("exist", "hidden class attached to tb button, meaning it's not shown as expected");
});

it("Should handle toolbar-select with width larger than the toolbar", () => {
cy.mount(
<div style="width: 200px;">
<Toolbar id="toolbar-wide-select">
<ToolbarSelect width="400px">
<ToolbarSelectOption>Option 1</ToolbarSelectOption>
<ToolbarSelectOption>Option 2</ToolbarSelectOption>
</ToolbarSelect>
</Toolbar>
</div>
);

// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);

cy.get("#toolbar-wide-select")
.shadow()
.find(".ui5-tb-overflow-btn")
.should("not.have.class", "ui5-tb-overflow-btn-hidden");

cy.get("[ui5-toolbar-select]")
.should("have.prop", "isOverflowed", true);
});

it("Should call event handlers on item", () => {
cy.mount(
<Toolbar>
Expand Down Expand Up @@ -482,9 +758,8 @@ describe("Toolbar general interaction", () => {
// Resize the viewport to make the overflow button disappear
cy.viewport(800, 1080);

// Verify the focus shifts to the last interactive element outside the overflow popover
cy.get("[ui5-toolbar-button]")
.last()
// Verify the focus shifts to the last visible toolbar button
cy.get("[ui5-toolbar-button][text='Button 5']")
.should("be.focused");
});

Expand Down
24 changes: 12 additions & 12 deletions packages/main/cypress/specs/ToolbarSelect.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,17 +299,16 @@ describe("Toolbar general interaction", () => {
</>
);

// Set up button click handler
cy.get("#btn").then($btn => {
$btn.get(0).addEventListener("ui5-click", () => {
// First, deselect all options
const select = document.querySelector("ui5-toolbar-select");
// Attach click handler via document to avoid jQuery wrapping issues
cy.document().then(doc => {
const btn = doc.getElementById("btn");
btn?.addEventListener("click", () => {
const select = doc.querySelector("ui5-toolbar-select");
const options = select?.querySelectorAll("ui5-toolbar-select-option");
options?.forEach(opt => {
(opt as ToolbarSelectOption).selected = false;
});
// Then select option 2
const opt2 = document.getElementById("opt2") as ToolbarSelectOption;
const opt2 = doc.getElementById("opt2") as ToolbarSelectOption;
opt2.selected = true;
});
});
Expand Down Expand Up @@ -360,11 +359,12 @@ describe("Toolbar general interaction", () => {
);

// Set up button to attempt selecting multiple options
cy.get("#selectMultiple").then($btn => {
$btn.get(0).addEventListener("ui5-click", () => {
const opt1 = document.getElementById("opt1") as ToolbarSelectOption;
const opt2 = document.getElementById("opt2") as ToolbarSelectOption;
const opt3 = document.getElementById("opt3") as ToolbarSelectOption;
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;
Expand Down
19 changes: 19 additions & 0 deletions packages/main/src/Input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import InputType from "./types/InputType.js";
import type Popover from "./Popover.js";
import type Icon from "./Icon.js";
import type { IIcon } from "./Icon.js";
import type { ToolbarArrowNavState } from "./ToolbarItemBase.js";

// Templates
import InputTemplate from "./InputTemplate.js";
Expand Down Expand Up @@ -1660,6 +1661,24 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
return this.nativeInput;
}

getArrowNavState(): ToolbarArrowNavState | undefined {
const input = this.getInputDOMRefSync();
if (!input) {
return undefined;
}

const active = getActiveElement() as HTMLElement | null;
const isInputFocused = !!active && (active === input || input.contains(active));
if (!isInputFocused) {
return undefined;
}

const caret = input.selectionStart ?? 0;
const len = input.value?.length ?? 0;

return { atLeftEnd: caret === 0, atRightEnd: caret >= len };
}

/**
* Returns a reference to the native input element
* @protected
Expand Down
Loading
Loading