diff --git a/packages/main/cypress/specs/Toolbar.cy.tsx b/packages/main/cypress/specs/Toolbar.cy.tsx
index f4c21fa0ed25..754c7006c0fd 100644
--- a/packages/main/cypress/specs/Toolbar.cy.tsx
+++ b/packages/main/cypress/specs/Toolbar.cy.tsx
@@ -4,7 +4,7 @@ import ToolbarSelect from "../../src/ToolbarSelect.js";
import ToolbarSelectOption from "../../src/ToolbarSelectOption.js";
import ToolbarSeparator from "../../src/ToolbarSeparator.js";
import ToolbarSpacer from "../../src/ToolbarSpacer.js";
-import type ToolbarItem from "../../src/ToolbarItem.js";
+import ToolbarItem from "../../src/ToolbarItem.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";
@@ -575,6 +575,7 @@ describe("ToolbarButton", () => {
cy.get("@toolbar").then($toolbar => {
const toolbar = $toolbar[0] as Toolbar;
const addButton = document.getElementById("add-btn") as ToolbarButton;
+
expect(toolbar.itemsToOverflow.includes(addButton)).to.be.true;
const initialOverflowCount = toolbar.itemsToOverflow.length;
@@ -664,3 +665,140 @@ describe("ToolbarButton", () => {
.should("contain.text", "Decline Item");
});
});
+
+describe("Toolbar Item", () => {
+ it("Should render ui5-toolbar-item with correct properties and not suppress events", () => {
+ // Mount the Toolbar with a ui5-toolbar-item wrapping a web component
+ cy.mount(
+
+
+
+
+
+ );
+
+ // Verify the ui5-toolbar-item has the correct properties
+ cy.get("ui5-toolbar-item").should((item) => {
+ expect(item).to.have.attr("prevent-overflow-closing");
+ expect(item).to.have.attr("overflow-priority", "AlwaysOverflow");
+ });
+
+ // Verify the inner component (ui5-button) is rendered
+ cy.get("ui5-toolbar-item")
+ .find("ui5-button").should((button) => {
+ expect(button).to.exist;
+ expect(button).to.contain.text("User Menu");
+ });
+
+
+ // Attach a click event to the inner button
+ cy.get("ui5-button#innerButton")
+ .then(button => {
+ button.get(0).addEventListener("click", cy.stub().as("buttonClicked"));
+ });
+
+ cy.get('ui5-toolbar') // Select the toolbar
+ .shadow() // Access the shadow DOM of the toolbar
+ .find('.ui5-tb-overflow-btn') // Find the overflow button inside the shadow DOM
+ .realClick();
+
+ cy.get("ui5-toolbar")
+ .shadow()
+ .find("[ui5-popover]")
+ .as("popover")
+
+ cy.get("@popover")
+ .should("have.prop", "open", true);
+
+ // Trigger a click event on the inner button
+ cy.get("ui5-button#innerButton").realClick();
+
+ // Verify the click event was triggered
+ cy.get("@buttonClicked").should("have.been.calledOnce");
+ });
+
+ it("Should respect prevent-overflow-closing property", () => {
+ // Mount the Toolbar with constrained width to force overflow
+ cy.mount(
+
+
+
+
+
+
+
+
+
+
+ );
+
+ // Wait for overflow processing
+ cy.wait(500);
+
+ // Click the overflow button to open the popover
+ cy.get("ui5-toolbar")
+ .shadow()
+ .find(".ui5-tb-overflow-btn")
+ .click();
+
+ // Verify the popover is open
+ cy.get("ui5-toolbar")
+ .shadow()
+ .find(".ui5-overflow-popover")
+ .should("have.prop", "open", true);
+
+ // Click on the item with prevent-overflow-closing
+ cy.get("ui5-toolbar-item[prevent-overflow-closing]")
+ .find("ui5-button")
+ .click();
+
+ // Verify the popover remains open
+ cy.get("ui5-toolbar")
+ .shadow()
+ .find(".ui5-overflow-popover")
+ .should("have.prop", "open", true);
+
+ // Optional: Test that normal items still close the popover
+ cy.get("ui5-toolbar-item:not([prevent-overflow-closing])")
+ .find("ui5-button")
+ .click();
+
+ // Verify the popover closes
+ cy.get("ui5-toolbar")
+ .shadow()
+ .find(".ui5-overflow-popover")
+ .should("have.prop", "open", false);
+ });
+
+ it("Should respect overflow-priority property", () => {
+ // Mount the Toolbar with multiple ui5-toolbar-items
+ cy.mount(
+
+
+
+
+
+
+
+
+ );
+
+ // Verify the overflow-priority property is respected
+ cy.get("ui5-toolbar-item[overflow-priority='AlwaysOverflow']")
+ .should("exist")
+ .should("have.attr", "overflow-priority", "AlwaysOverflow");
+
+ cy.get("ui5-toolbar-item[overflow-priority='NeverOverflow']")
+ .should("exist")
+ .should("have.attr", "overflow-priority", "NeverOverflow");
+
+ // Simulate overflow behavior and ensure high-priority item remains visible
+ cy.viewport(300, 1080); // Simulate a smaller viewport
+ cy.get("ui5-toolbar-item[overflow-priority='NeverOverflow']")
+ .should("be.visible");
+
+ // Ensure low-priority item is hidden or moved to overflow
+ cy.get("ui5-toolbar-item[overflow-priority='AlwaysOverflow']")
+ .should("not.be.visible");
+ });
+})
diff --git a/packages/main/src/Breadcrumbs.ts b/packages/main/src/Breadcrumbs.ts
index bd23b8976380..0695b25f596d 100644
--- a/packages/main/src/Breadcrumbs.ts
+++ b/packages/main/src/Breadcrumbs.ts
@@ -24,6 +24,7 @@ import BreadcrumbsDesign from "./types/BreadcrumbsDesign.js";
import "./BreadcrumbsItem.js";
import type BreadcrumbsItem from "./BreadcrumbsItem.js";
import type BreadcrumbsSeparator from "./types/BreadcrumbsSeparator.js";
+import type { IOverflowToolbarItem } from "./ToolbarItem.js";
import {
BREADCRUMB_ITEM_POS,
@@ -84,6 +85,7 @@ type FocusAdaptor = ITabbable & {
* - [End] - Navigates to the last item.
* @constructor
* @extends UI5Element
+ * @implements {IOverflowToolbarItem}
* @public
* @since 1.0.0-rc.15
*/
@@ -109,7 +111,7 @@ type FocusAdaptor = ITabbable & {
bubbles: true,
cancelable: true,
})
-class Breadcrumbs extends UI5Element {
+class Breadcrumbs extends UI5Element implements IOverflowToolbarItem {
eventDetails!: {
"item-click": BreadcrumbsItemClickEventDetail,
}
@@ -642,6 +644,9 @@ class Breadcrumbs extends UI5Element {
get _cancelButtonText() {
return Breadcrumbs.i18nBundle.getText(BREADCRUMBS_CANCEL_BUTTON);
}
+ get _selfOverflowed() {
+ return true;
+ }
}
Breadcrumbs.define();
diff --git a/packages/main/src/Toolbar.ts b/packages/main/src/Toolbar.ts
index 0a6165bb24e2..143804aa2473 100644
--- a/packages/main/src/Toolbar.ts
+++ b/packages/main/src/Toolbar.ts
@@ -5,7 +5,6 @@ import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
-import type { UI5CustomEvent } from "@ui5/webcomponents-base";
import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
@@ -300,14 +299,30 @@ class Toolbar extends UI5Element {
async onAfterRendering() {
await renderFinished();
-
this.storeItemsWidth();
this.processOverflowLayout();
this.items.forEach(item => {
- item.isOverflowed = this.overflowItems.map(overflowItem => overflowItem).indexOf(item) !== -1;
+ this.addItemsAdditionalProperties(item);
});
}
+ addItemsAdditionalProperties(item: ToolbarItem) {
+ item.isOverflowed = this.overflowItems.indexOf(item) !== -1;
+ const itemWrapper = this.shadowRoot!.querySelector(`#${item._individualSlot}`) as HTMLElement;
+ if (item._selfOverflowed && !item.isOverflowed && itemWrapper) {
+ // We need to set the max-width to the self-overflow element in order ot prevent it from taking all the available space,
+ // since, unlike the other items, it is allowed to grow and shrink
+ // We need to set the max-width to none and its position to absolute to allow the item to grow and measure its width,
+ // then when set, the max-width will be cached and we will set its highest value to not cut it when the Toolbar shrinks it
+ // on rendering and then we resize it manually.
+ itemWrapper.style.maxWidth = `none`;
+ itemWrapper?.classList.add("ui5-tb-self-overflow-grow");
+ item._maxWidth = Math.max(this.getItemWidth(item), item._maxWidth);
+ itemWrapper.style.maxWidth = `${item._maxWidth}px`;
+ itemWrapper?.classList.remove("ui5-tb-self-overflow-grow");
+ }
+ }
+
/**
* Returns if the overflow popup is open.
* @public
@@ -460,16 +475,13 @@ class Toolbar extends UI5Element {
this.popoverOpen = false;
}
- onBeforeClose(e: UI5CustomEvent) {
- e.preventDefault();
- }
-
onOverflowPopoverOpened() {
this.popoverOpen = true;
}
onResize() {
this.closeOverflow();
+ this.storeItemsWidth();
this.processOverflowLayout();
}
diff --git a/packages/main/src/ToolbarButton.ts b/packages/main/src/ToolbarButton.ts
index 4ee3fcc48eb0..e8d315eb1dba 100644
--- a/packages/main/src/ToolbarButton.ts
+++ b/packages/main/src/ToolbarButton.ts
@@ -170,6 +170,8 @@ class ToolbarButton extends ToolbarItem {
@property()
width?: string;
+ _kind = "ToolbarButton";
+
get styles() {
return {
width: this.width,
diff --git a/packages/main/src/ToolbarItem.ts b/packages/main/src/ToolbarItem.ts
index db9f5057e015..fe2f666adb2e 100644
--- a/packages/main/src/ToolbarItem.ts
+++ b/packages/main/src/ToolbarItem.ts
@@ -1,7 +1,11 @@
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
-
+import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
+import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
+import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
+import ToolbarItemTemplate from "./ToolbarItemTemplate.js";
+import ToolbarItemCss from "./generated/themes/ToolbarItem.css.js";
import type ToolbarItemOverflowBehavior from "./types/ToolbarItemOverflowBehavior.js";
type IEventOptions = {
@@ -12,8 +16,22 @@ type ToolbarItemEventDetail = {
targetRef: HTMLElement;
}
+interface IOverflowToolbarItem extends HTMLElement {
+ eventsToCloseOverflow?: string[] | undefined;
+ _selfOverflowed?: boolean | undefined;
+}
+
@event("close-overflow", {
bubbles: true,
+ cancelable: true,
+})
+
+@customElement({
+ tag: "ui5-toolbar-item",
+ languageAware: true,
+ renderer: jsxRenderer,
+ template: ToolbarItemTemplate,
+ styles: ToolbarItemCss,
})
/**
@@ -22,7 +40,6 @@ type ToolbarItemEventDetail = {
* Represents an abstract class for items, used in the `ui5-toolbar`.
* @constructor
* @extends UI5Element
- * @abstract
* @public
* @since 1.17.0
*/
@@ -60,11 +77,108 @@ class ToolbarItem extends UI5Element {
@property({ type: Boolean })
isOverflowed: boolean = false;
+ /**
+ * Defines if the component, wrapped in the toolbar item, has his own overflow mechanism.
+ * @default false
+ * @private
+ * @since 2.19.0
+ */
+ @property({ type: Boolean })
+ _selfOverflowed: boolean = false;
+
+ /**
+ * Defines if the component, wrapped in the toolbar item, should be expanded in the overflow popover.
+ * @default false
+ * @public
+ * @since 2.19.0
+ */
+
+ @property({ type: Boolean })
+ expandInOverflow: boolean = false;
+
_isRendering = true;
+ _maxWidth = 0;
+ fireCloseOverflowRef = this.fireCloseOverflow.bind(this);
+ _kind = "ToolbarItem";
+
+ eventsToCloseOverflow: string[] = [];
+
+ closeOverflowSet = {
+ "ui5-button": ["click"],
+ "ui5-select": ["change"],
+ "ui5-combobox": ["change"],
+ "ui5-multi-combobox": ["change"],
+ "ui5-date-picker": ["change"],
+ "ui5-switch": ["change"],
+ }
+
+ predefinedWrapperSet = {
+ "ui5-button": "ToolbarButton",
+ "ui5-select": "ToolbarSelect",
+ }
+
+ onBeforeRendering(): void {
+ const slottedItem = this.getSlottedNodes("item")[0] as IOverflowToolbarItem;
+ this.checkForWrapper();
+ this.attachCloseOverflowHandlers();
+ this._selfOverflowed = !!slottedItem?._selfOverflowed;
+ }
onAfterRendering(): void {
this._isRendering = false;
}
+
+ onExitDOM(): void {
+ this.detachCloseOverflowHandlers();
+ }
+
+ /**
+ * Wrapped component slot.
+ * @public
+ * @since 2.19.0
+ */
+
+ @slot({
+ "default": true, type: HTMLElement, invalidateOnChildChange: true,
+ })
+ item!: IOverflowToolbarItem[]; // here
+
+ // Method called by ui5-toolbar to inform about the existing toolbar wrapper
+ checkForWrapper() {
+ const tagName = this.item?.[0]?.localName as keyof typeof this.predefinedWrapperSet;
+ if (this._kind === "ToolbarItem"
+ && this.predefinedWrapperSet[tagName]) {
+ // eslint-disable-next-line no-console
+ console.warn(`This UI5 web component has its predefined toolbar wrapper called ${this.predefinedWrapperSet[tagName]}.`);
+ }
+ }
+
+ // We want to close the overflow popover, when closing event is being executed
+ getClosingEvents(): string[] {
+ const ctor = this.getSlottedNodes("item")[0]?.constructor as typeof ToolbarItem;
+ const tagName = ctor?.getMetadata ? ctor.getMetadata().getPureTag() : this.getSlottedNodes("item")[0]?.tagName;
+ return [...(this.closeOverflowSet[tagName as keyof typeof this.closeOverflowSet] || []), ...this.eventsToCloseOverflow];
+ }
+
+ attachCloseOverflowHandlers() {
+ const closingEvents = this.getClosingEvents();
+ closingEvents.forEach(clEvent => {
+ if (!this.preventOverflowClosing) {
+ this.addEventListener(clEvent, this.fireCloseOverflowRef);
+ }
+ });
+ }
+
+ detachCloseOverflowHandlers() {
+ const closingEvents = this.getClosingEvents();
+ closingEvents.forEach(clEvent => {
+ this.removeEventListener(clEvent, this.fireCloseOverflowRef);
+ });
+ }
+
+ fireCloseOverflow() {
+ this.fireDecoratorEvent("close-overflow");
+ }
/**
* Defines if the width of the item should be ignored in calculating the whole width of the toolbar
* @protected
@@ -117,5 +231,8 @@ class ToolbarItem extends UI5Element {
export type {
IEventOptions,
ToolbarItemEventDetail,
+ IOverflowToolbarItem,
};
+ToolbarItem.define();
+
export default ToolbarItem;
diff --git a/packages/main/src/ToolbarItemTemplate.tsx b/packages/main/src/ToolbarItemTemplate.tsx
new file mode 100644
index 000000000000..60bd9f2637b9
--- /dev/null
+++ b/packages/main/src/ToolbarItemTemplate.tsx
@@ -0,0 +1,7 @@
+import type ToolbarItem from "./ToolbarItem.js";
+
+export default function ToolbarItemTemplate(this: ToolbarItem) {
+ return (
+
+ );
+}
diff --git a/packages/main/src/ToolbarSelect.ts b/packages/main/src/ToolbarSelect.ts
index ff3011be84ac..db32447c8a84 100644
--- a/packages/main/src/ToolbarSelect.ts
+++ b/packages/main/src/ToolbarSelect.ts
@@ -163,6 +163,7 @@ class ToolbarSelect extends ToolbarItem {
// Internal value storage, in case the composite select is not rendered on the the assignment happens
_value: string = "";
+ _kind = "ToolbarSelect";
onClick(e: Event): void {
e.stopImmediatePropagation();
diff --git a/packages/main/src/ToolbarSeparator.ts b/packages/main/src/ToolbarSeparator.ts
index d7279d6fecec..8b6326c3508e 100644
--- a/packages/main/src/ToolbarSeparator.ts
+++ b/packages/main/src/ToolbarSeparator.ts
@@ -38,6 +38,7 @@ class ToolbarSeparator extends ToolbarItem {
get isInteractive() {
return false;
}
+ _kind = "ToolbarSeparator";
}
ToolbarSeparator.define();
diff --git a/packages/main/src/ToolbarSpacer.ts b/packages/main/src/ToolbarSpacer.ts
index 40b0cff7aa97..1e30c0e7c8dc 100644
--- a/packages/main/src/ToolbarSpacer.ts
+++ b/packages/main/src/ToolbarSpacer.ts
@@ -48,6 +48,8 @@ class ToolbarSpacer extends ToolbarItem {
get isInteractive() {
return false;
}
+
+ _kind = "ToolbarSpacer";
}
ToolbarSpacer.define();
diff --git a/packages/main/src/ToolbarTemplate.tsx b/packages/main/src/ToolbarTemplate.tsx
index 9f086627b48f..9e406c798f84 100644
--- a/packages/main/src/ToolbarTemplate.tsx
+++ b/packages/main/src/ToolbarTemplate.tsx
@@ -14,15 +14,11 @@ export default function ToolbarTemplate(this: Toolbar) {
aria-label={this.accInfo.root.accessibleName}
>
{this.standardItems.map(item => {
- if ("styles" in item) {
- return (
-
-
-
- );
- }
return (
-
+
);
@@ -56,15 +52,11 @@ export default function ToolbarTemplate(this: Toolbar) {
"ui5-overflow-list": true
}}>
{this.overflowItems.map(item => {
- if (item.isSeparator) {
- return (
-
-
-
- );
- }
+ const separatorClass = item.isSeparator ? " ui5-tb-separator ui5-tb-separator-in-overflow" : "";
+ const selfOverflowClass = item._selfOverflowed ? " ui5-tb-popover-self-overflow" : "";
+ const classes = `ui5-tb-popover-item${separatorClass}${selfOverflowClass}`;
return (
-
+
);
diff --git a/packages/main/src/bundle.esm.ts b/packages/main/src/bundle.esm.ts
index 77c873528352..39061900ac38 100644
--- a/packages/main/src/bundle.esm.ts
+++ b/packages/main/src/bundle.esm.ts
@@ -118,6 +118,7 @@ import Title from "./Title.js";
import Toast from "./Toast.js";
import ToggleButton from "./ToggleButton.js";
import Toolbar from "./Toolbar.js";
+import ToolbarItem from "./ToolbarItem.js";
import ToolbarButton from "./ToolbarButton.js";
import ToolbarSeparator from "./ToolbarSeparator.js";
import ToolbarSpacer from "./ToolbarSpacer.js";
diff --git a/packages/main/src/themes/Toolbar.css b/packages/main/src/themes/Toolbar.css
index 7b1813881459..d5931dcc2beb 100644
--- a/packages/main/src/themes/Toolbar.css
+++ b/packages/main/src/themes/Toolbar.css
@@ -28,13 +28,21 @@
.ui5-tb-item {
flex-shrink: 0;
-}
-
-.ui5-tb-item {
margin-inline-end: var(--_ui5-toolbar-item-margin-right);
margin-inline-start: var(--_ui5-toolbar-item-margin-left);
}
+.ui5-tb-self-overflow {
+ min-width: 2.5rem;
+ flex-shrink: 1;
+ flex-grow: 1;
+
+}
+
+.ui5-tb-self-overflow-grow {
+ position: absolute;
+}
+
/* Last visible element should not have margins.
Last element is: overflow button or tb-item when overflow button is hidden */
.ui5-tb-overflow-btn,
diff --git a/packages/main/src/themes/ToolbarItem.css b/packages/main/src/themes/ToolbarItem.css
new file mode 100644
index 000000000000..cbb74d5b483a
--- /dev/null
+++ b/packages/main/src/themes/ToolbarItem.css
@@ -0,0 +1,8 @@
+/* This style we need for the element inside Popover to not fire the click event on empty space */
+:host([is-overflowed]:not([expand-in-overflow])) {
+ display: inline-block;
+}
+
+:host([expand-in-overflow]) ::slotted(*) {
+ width: 100%;
+}
\ No newline at end of file
diff --git a/packages/main/src/themes/ToolbarPopover.css b/packages/main/src/themes/ToolbarPopover.css
index aba44a2046d9..b11ad5e99025 100644
--- a/packages/main/src/themes/ToolbarPopover.css
+++ b/packages/main/src/themes/ToolbarPopover.css
@@ -6,7 +6,7 @@
display: flex;
flex-direction: column;
justify-content: center;
- align-items: center;
+ align-items: flex-start;
}
.ui5-tb-popover-item {
diff --git a/packages/main/test/pages/Toolbar.html b/packages/main/test/pages/Toolbar.html
index ebdd5f5c6922..9e26be4475fb 100644
--- a/packages/main/test/pages/Toolbar.html
+++ b/packages/main/test/pages/Toolbar.html
@@ -1,4 +1,3 @@
-
diff --git a/packages/main/test/pages/ToolbarItems.html b/packages/main/test/pages/ToolbarItems.html
new file mode 100644
index 000000000000..5499b262b6ab
--- /dev/null
+++ b/packages/main/test/pages/ToolbarItems.html
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
Toolbar
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file