Skip to content
Draft
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
159 changes: 154 additions & 5 deletions packages/main/cypress/specs/Toolbar.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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";
Expand Down Expand Up @@ -46,6 +47,157 @@ 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 into and out of overflow button with single arrow press", () => {
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("ArrowRight");
cy.get("#overflow-arrow-toolbar")
.then($toolbar => {
const toolbar = $toolbar[0] as Toolbar & {
_lastFocusedItem?: ToolbarItem | HTMLElement;
};
const firstToolbarItem = $toolbar.find("[ui5-toolbar-button][text='One Long']")[0] as ToolbarItem;
expect(toolbar._lastFocusedItem).to.equal(firstToolbarItem);
});
});

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 scroll the page when pressing Up/Down inside the toolbar", () => {
cy.mount(
<div style={{ height: "2000px" }}>
<Toolbar id="scroll-toolbar" style={{ marginTop: "100px" }}>
<ToolbarButton text="Button"></ToolbarButton>
</Toolbar>
</div>
);

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

cy.window().its("scrollY").as("scrollBefore");
cy.realPress("ArrowDown");
cy.window().its("scrollY").then(scrollAfter => {
cy.get("@scrollBefore").should("equal", scrollAfter);
});
});

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 @@ -482,11 +634,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]")
.shadow()
.find(".ui5-tb-item")
.eq(3)
// Verify the focus shifts to the last visible toolbar button
cy.get("[ui5-toolbar-button][text='Button 5']")
.should("be.focused");
});

Expand Down
22 changes: 22 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 { ToolbarMovementInfo } from "./ToolbarItemBase.js";

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

getToolbarMovementInfo(): ToolbarMovementInfo | 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 caretIndex = input.selectionStart ?? 0;
const valueLength = input.value?.length ?? 0;

return {
currentIndex: caretIndex,
itemCount: valueLength + 1,
};
}

/**
* Returns a reference to the native input element
* @protected
Expand Down
1 change: 0 additions & 1 deletion packages/main/src/RadioButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ let activeRadio: RadioButton;
* @public
* @csspart outer-ring - Used to style the outer ring of the `ui5-radio-button`.
* @csspart inner-ring - Used to style the inner ring of the `ui5-radio-button`.
* @csspart root - Used to style the root DOM element of the component.
*/
@customElement({
tag: "ui5-radio-button",
Expand Down
23 changes: 23 additions & 0 deletions packages/main/src/TextArea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { isEscape } from "@ui5/webcomponents-base/dist/Keys.js";
import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js";
import type Popover from "./Popover.js";
import type InputComposition from "./features/InputComposition.js";
import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js";
import type { ToolbarMovementInfo } from "./ToolbarItemBase.js";

import TextAreaTemplate from "./TextAreaTemplate.js";

Expand Down Expand Up @@ -436,6 +438,27 @@ class TextArea extends UI5Element implements IFormInputElement {
return this.getDomRef()!.querySelector<HTMLTextAreaElement>("textarea")!;
}

getToolbarMovementInfo(): ToolbarMovementInfo | undefined {
const textArea = this.getDomRef()?.querySelector<HTMLTextAreaElement>("textarea");
if (!textArea) {
return undefined;
}

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

const caretIndex = textArea.selectionStart ?? 0;
const valueLength = textArea.value?.length ?? 0;

return {
currentIndex: caretIndex,
itemCount: valueLength + 1,
};
}

_onkeydown(e: KeyboardEvent) {
this._keyDown = true;

Expand Down
Loading
Loading