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 + + + + + + + +
+ Standard Toolbar with ToolbarSelect and ToolbarButton +
+
+ + + + + + + + + + + 1 + 2 + 3 + + + + + 123 + +
+ Toolbar with ui5-select and ui5-button wrapped in ui5-toolbar-item + Toolbar with various components +
+
+ + Left 1 (long) + Left 2 + Left 3 + Left 4 + Mid 1 + Mid 2 + Right 1 + Right 4 + + Toolbar with various components +
+
+ + + + + + Simple text + + + + + + + + + + + + + + + + + + Simple title + + + + + +
div with Link and text
+
+ +
+
+ Toolbar with breadcrumbs (self-overflowed) component at the beginning +
+
+ + + + Link1 + Link2 + Link3 + Link4 + Link5 + Link6 + Link7 + Location + + + + + + + + + + +
+ Toolbar with breadcrumbs (self-overflowed) component at the middle +
+
+ + + + + + + + + + Link1 + Link2 + Link3 + Link4 + Link5 + Link6 + Link7 + Location + + + + +
+ Toolbar with Various Form Controls +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + \ No newline at end of file