diff --git a/packages/main/cypress/specs/Toolbar.cy.tsx b/packages/main/cypress/specs/Toolbar.cy.tsx index f4c21fa0ed25..719dba32be02 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"; @@ -123,7 +123,7 @@ describe("Toolbar general interaction", () => { .should("exist", "hidden class attached to tb button, meaning it's not shown as expected"); }); - it("Should call event handlers on abstract item", () => { + it("Should call event handlers on item", () => { cy.mount( @@ -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,4 @@ describe("ToolbarButton", () => { .should("contain.text", "Decline Item"); }); }); + diff --git a/packages/main/cypress/specs/ToolbarItem.cy.tsx b/packages/main/cypress/specs/ToolbarItem.cy.tsx new file mode 100644 index 000000000000..595408c1381a --- /dev/null +++ b/packages/main/cypress/specs/ToolbarItem.cy.tsx @@ -0,0 +1,767 @@ +import Toolbar from "../../src/Toolbar.js"; +import ToolbarItem from "../../src/ToolbarItem.js"; +import Button from "../../src/Button.js"; +import Switch from "../../src/Switch.js"; +import MultiComboBox from "../../src/MultiComboBox.js"; +import MultiComboBoxItem from "../../src/MultiComboBoxItem.js"; +import ComboBox from "../../src/ComboBox.js"; +import ComboBoxItem from "../../src/ComboBoxItem.js"; +import DatePicker from "../../src/DatePicker.js"; +import Select from "../../src/Select.js"; +import Option from "../../src/Option.js"; + +describe("Toolbar Item Properties", () => { + 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 => { + // Add your event logic here + }); + }); +}); + +describe("Toolbar Item Closing Events - closeOverflowSet functionality", () => { + it("Should close overflow popover when ui5-button 'click' event is fired", () => { + cy.viewport(300, 600); + + cy.mount( + + + + + + ); + + // Open overflow popover + cy.get("#toolbar-button-close") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Verify popover is open + cy.get("#toolbar-button-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + + // Click the button inside the overflow + cy.get("[ui5-toolbar-item]") + .find("[ui5-button]") + .realClick(); + + // Verify popover is closed + cy.get("#toolbar-button-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", false); + }); + + it("Should close overflow popover when ui5-select 'change' event is fired", () => { + cy.viewport(300, 600); + + cy.mount( + + + + + + ); + + // Open overflow popover + cy.get("#toolbar-select-close") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Verify popover is open + cy.get("#toolbar-select-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + + // Open the select and choose an option + cy.get("[ui5-toolbar-item]") + .find("[ui5-select]") + .realClick(); + + // Select a different option + cy.get("[ui5-option]") + .eq(1) + .realClick(); + + cy.wait(500); // Wait for the change event to propagate and popover to close + + // Verify popover is closed after select change + cy.get("#toolbar-select-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", false); + }); + + it("Should close overflow popover when ui5-combobox 'change' event is fired", () => { + cy.viewport(300, 600); + + cy.mount( + + + + + + + + + + ); + + // Open overflow popover + cy.get("#toolbar-combobox-close") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Verify popover is open + cy.get("#toolbar-combobox-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + + // Open the combobox by clicking the dropdown arrow icon + cy.get("[ui5-toolbar-item]") + .find("[ui5-combobox]") + .shadow() + .find("[ui5-icon]") + .realClick(); + + // Wait for combobox popover to open and select an item + cy.get("[ui5-combobox]") + .find("[ui5-cb-item]") + .first() + .realClick(); + + // Verify popover is closed after combobox change + cy.get("#toolbar-combobox-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", false); + }); + + it("Should close overflow popover when ui5-multi-combobox 'selection-change' event is fired", () => { + cy.viewport(300, 600); + + cy.mount( + + + + + + + + + + ); + + // Open overflow popover + cy.get("#toolbar-multi-combobox-close") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Verify popover is open + cy.get("#toolbar-multi-combobox-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + + // Open the multi-combobox + cy.get("[ui5-toolbar-item]") + .find("[ui5-multi-combobox]") + .shadow() + .find("[ui5-icon]") + .realClick(); + + // Check an item - this triggers selection-change event + cy.get("[ui5-multi-combobox]") + .find("[ui5-mcb-item]") + .first() + .realClick(); + + // Verify toolbar popover is closed after multi-combobox selection-change + cy.get("#toolbar-multi-combobox-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", false); + }); + + it("Should close overflow popover when ui5-date-picker 'change' event is fired", () => { + cy.viewport(300, 600); + + cy.mount( + + + + + + ); + + // Open overflow popover + cy.get("#toolbar-datepicker-close") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Verify popover is open + cy.get("#toolbar-datepicker-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + + // Open date picker + cy.get("[ui5-toolbar-item]") + .find("[ui5-date-picker]") + .shadow() + .find("[ui5-icon]") + .realClick(); + + // Select today's date by clicking on today cell + cy.get("[ui5-calendar]", { includeShadowDom: true }) + .find("[ui5-daypicker]", { includeShadowDom: true }) + .shadow() + .find(".ui5-dp-item--now") + .realClick(); + + // Verify popover is closed after date picker change + cy.get("#toolbar-datepicker-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", false); + }); + + it("Should close overflow popover when ui5-switch 'change' event is fired", () => { + cy.viewport(300, 600); + + cy.mount( + + + + + + ); + + // Open overflow popover + cy.get("#toolbar-switch-close") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Verify popover is open + cy.get("#toolbar-switch-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + + // Toggle the switch using realClick + cy.get("[ui5-toolbar-item]") + .find("[ui5-switch]") + .shadow() + .find(".ui5-switch-root") + .realClick(); + + // Verify popover is closed after switch change + cy.get("#toolbar-switch-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", false); + }); + + it("Should NOT close overflow popover when preventOverflowClosing is set", () => { + cy.viewport(300, 600); + + cy.mount( + + + + + + ); + + // Open overflow popover + cy.get("#toolbar-prevent-close") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Verify popover is open + cy.get("#toolbar-prevent-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + + // Click the button inside the overflow + cy.get("[ui5-toolbar-item]") + .find("[ui5-button]") + .realClick(); + + // Verify popover is still open + cy.get("#toolbar-prevent-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + }); + + it("Should NOT close overflow popover when preventOverflowClosing is set on ui5-switch", () => { + cy.viewport(300, 600); + + cy.mount( + + + + + + ); + + // Open overflow popover + cy.get("#toolbar-switch-prevent-close") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Verify popover is open + cy.get("#toolbar-switch-prevent-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + + // Toggle the switch + cy.get("[ui5-toolbar-item]") + .find("[ui5-switch]") + .realClick(); + + // Verify popover is still open since preventOverflowClosing is set + cy.get("#toolbar-switch-prevent-close") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + }); + + it("Should fire close-overflow event when closing event is triggered", () => { + cy.viewport(300, 600); + + cy.mount( + + + + + + ); + + // Add event listener for close-overflow + cy.get("#testToolbarItem") + .then($item => { + $item.get(0).addEventListener("close-overflow", cy.stub().as("closeOverflowStub")); + }); + + // Open overflow popover + cy.get("#toolbar-close-event") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Click the button to trigger closing + cy.get("[ui5-toolbar-item]") + .find("[ui5-button]") + .realClick(); + + // Verify close-overflow event was fired + cy.get("@closeOverflowStub") + .should("have.been.calledOnce"); + }); + + it("Should handle multiple components with different closing events in toolbar", () => { + cy.viewport(200, 600); + + cy.mount( + + + + + + + + + ); + + // Open overflow popover + cy.get("#toolbar-multiple-components") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Verify popover is open + cy.get("#toolbar-multiple-components") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + + // Toggle the switch (fires 'change' event) + cy.get("[ui5-toolbar-item]") + .find("[ui5-switch]") + .shadow() + .find(".ui5-switch-root") + .realClick(); + + // Verify popover is closed + cy.get("#toolbar-multiple-components") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", false); + + // Re-open overflow popover + cy.get("#toolbar-multiple-components") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Verify popover is open again + cy.get("#toolbar-multiple-components") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + + // Click the button (fires 'click' event) + cy.get("[ui5-toolbar-item]") + .find("[ui5-button]") + .realClick(); + + // Verify popover is closed + cy.get("#toolbar-multiple-components") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", false); + }); + + it("Should return correct closing events via getClosingEvents for button", () => { + cy.mount( + + + + + + ); + + cy.get("#buttonToolbarItem") + .then($item => { + const toolbarItem = $item.get(0) as ToolbarItem; + const closingEvents = toolbarItem.getClosingEvents(); + expect(closingEvents).to.include("click"); + }); + }); + + it("Should return correct closing events via getClosingEvents for select", () => { + cy.mount( + + + + + + ); + + cy.get("#selectToolbarItem") + .then($item => { + const toolbarItem = $item.get(0) as ToolbarItem; + const closingEvents = toolbarItem.getClosingEvents(); + expect(closingEvents).to.include("change"); + }); + }); + + it("Should return correct closing events via getClosingEvents for switch", () => { + cy.mount( + + + + + + ); + + cy.get("#switchToolbarItem") + .then($item => { + const toolbarItem = $item.get(0) as ToolbarItem; + const closingEvents = toolbarItem.getClosingEvents(); + expect(closingEvents).to.include("change"); + }); + }); +}); + +/** + * Custom component that implements IOverflowToolbarItem with overflowCloseEvents + * This simulates a component that defines its own custom closing events + */ +class CustomOverflowComponent extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot!.innerHTML = ` + + `; + } + + // Implements IOverflowToolbarItem.overflowCloseEvents + get overflowCloseEvents(): string[] { + return ["custom-action", "custom-change"]; + } + + fireCustomAction() { + this.dispatchEvent(new CustomEvent("custom-action", { bubbles: true })); + } + + fireCustomChange() { + this.dispatchEvent(new CustomEvent("custom-change", { bubbles: true })); + } +} + +// Register the custom element if not already registered +if (!customElements.get("custom-overflow-component")) { + customElements.define("custom-overflow-component", CustomOverflowComponent); +} + +/** + * Custom component that implements IOverflowToolbarItem with additional custom events + * Used to test combining predefined closeOverflowSet events with component's overflowCloseEvents + */ +class CustomButtonWithExtraEvents extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot!.innerHTML = ``; + } + + get overflowCloseEvents(): string[] { + return ["extra-event"]; + } + + fireExtraEvent() { + this.dispatchEvent(new CustomEvent("extra-event", { bubbles: true })); + } +} + +// Register the custom element if not already registered +if (!customElements.get("custom-button-extra")) { + customElements.define("custom-button-extra", CustomButtonWithExtraEvents); +} + +// @ts-ignore - Custom element JSX type +const CustomOverflowComponentRenderer = (props: { id: string }) => ; + +describe("Toolbar Item Closing Events - overflowCloseEvents functionality (IOverflowToolbarItem)", () => { + it("Should close overflow popover when custom component fires 'custom-action' event from overflowCloseEvents", () => { + cy.viewport(300, 600); + + cy.mount( + + + + + + ); + + // Open overflow popover + cy.get("#toolbar-custom-action") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Verify popover is open + cy.get("#toolbar-custom-action") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + + // Fire the custom-action event from the component + cy.get("#customComponent1") + .then($el => { + const component = $el.get(0) as CustomOverflowComponent; + component.fireCustomAction(); + }); + + // Verify popover is closed + cy.get("#toolbar-custom-action") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", false); + }); + + it("Should close overflow popover when custom component fires 'custom-change' event from overflowCloseEvents", () => { + cy.viewport(300, 600); + + cy.mount( + + + + + + ); + + // Open overflow popover + cy.get("#toolbar-custom-change") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Verify popover is open + cy.get("#toolbar-custom-change") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + + // Fire the custom-change event from the component + cy.get("#customComponent2") + .then($el => { + const component = $el.get(0) as CustomOverflowComponent; + component.fireCustomChange(); + }); + + // Verify popover is closed + cy.get("#toolbar-custom-change") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", false); + }); + + it("Should return custom closing events via getClosingEvents for component with overflowCloseEvents", () => { + cy.mount( + + + + + + ); + + cy.get("#customToolbarItem") + .then($item => { + const toolbarItem = $item.get(0) as ToolbarItem; + const closingEvents = toolbarItem.getClosingEvents(); + expect(closingEvents).to.include("custom-action"); + expect(closingEvents).to.include("custom-change"); + }); + }); + + it("Should NOT close overflow popover with custom events when preventOverflowClosing is set", () => { + cy.viewport(300, 600); + + cy.mount( + + + + + + ); + + // Open overflow popover + cy.get("#toolbar-custom-prevent") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Verify popover is open + cy.get("#toolbar-custom-prevent") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + + // Fire the custom-action event from the component + cy.get("#customComponent4") + .then($el => { + const component = $el.get(0) as CustomOverflowComponent; + component.fireCustomAction(); + }); + + // Verify popover is still open since preventOverflowClosing is set + cy.get("#toolbar-custom-prevent") + .shadow() + .find("[ui5-popover]") + .should("have.prop", "open", true); + }); + + it("Should fire close-overflow event when custom overflowCloseEvents event is triggered", () => { + cy.viewport(300, 600); + + cy.mount( + + + + + + ); + + // Add event listener for close-overflow + cy.get("#customEventItem") + .then($item => { + $item.get(0).addEventListener("close-overflow", cy.stub().as("closeOverflowStub")); + }); + + // Open overflow popover + cy.get("#toolbar-custom-event-fire") + .shadow() + .find(".ui5-tb-overflow-btn") + .realClick(); + + // Fire the custom-action event from the component + cy.get("#customComponent5") + .then($el => { + const component = $el.get(0) as CustomOverflowComponent; + component.fireCustomAction(); + }); + + // Verify close-overflow event was fired + cy.get("@closeOverflowStub") + .should("have.been.calledOnce"); + }); + + it("Should combine predefined closeOverflowSet events with component's overflowCloseEvents", () => { + // @ts-ignore - Custom element JSX type + const CustomButtonExtraRenderer = (props: { id: string }) => ; + + cy.mount( + + + + + + ); + + cy.get("#combinedEventsItem") + .then($item => { + const toolbarItem = $item.get(0) as ToolbarItem; + const closingEvents = toolbarItem.getClosingEvents(); + // Should include the custom overflowCloseEvents + expect(closingEvents).to.include("extra-event"); + }); + }); +}); \ No newline at end of file diff --git a/packages/main/src/Breadcrumbs.ts b/packages/main/src/Breadcrumbs.ts index 989092db4a80..8a4680a3a59e 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 hasOverflow() { + return true; + } } Breadcrumbs.define(); diff --git a/packages/main/src/Toolbar.ts b/packages/main/src/Toolbar.ts index 39740f4f2415..e2d43aa74297 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"; @@ -299,14 +298,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.hasOverflow && !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 @@ -459,16 +474,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/ToolbarItem.ts b/packages/main/src/ToolbarItem.ts index db9f5057e015..3beb0a6660cd 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,26 @@ type ToolbarItemEventDetail = { targetRef: HTMLElement; } +interface IOverflowToolbarItem extends HTMLElement { + overflowCloseEvents?: string[] | undefined; + hasOverflow?: boolean | undefined; +} +/** + * Fired when the overflow popover is closed. + * @public + * @since 1.17.0 + */ @event("close-overflow", { bubbles: true, + cancelable: true, +}) + +@customElement({ + tag: "ui5-toolbar-item", + languageAware: true, + renderer: jsxRenderer, + template: ToolbarItemTemplate, + styles: ToolbarItemCss, }) /** @@ -22,8 +44,8 @@ type ToolbarItemEventDetail = { * Represents an abstract class for items, used in the `ui5-toolbar`. * @constructor * @extends UI5Element - * @abstract * @public + * @experimental This module is experimental and its API might change significantly in future. * @since 1.17.0 */ class ToolbarItem extends UI5Element { @@ -61,10 +83,97 @@ class ToolbarItem extends UI5Element { isOverflowed: boolean = false; _isRendering = true; + _maxWidth = 0; + _wrapperChecked = false; + fireCloseOverflowRef = this.fireCloseOverflow.bind(this); + + closeOverflowSet = { + "ui5-button": ["click"], + "ui5-select": ["change"], + "ui5-combobox": ["change"], + "ui5-multi-combobox": ["selection-change"], + "ui5-date-picker": ["change"], + "ui5-switch": ["change"], + } + + predefinedWrapperSet = { + "ui5-button": "ToolbarButton", + "ui5-select": "ToolbarSelect", + } + + onBeforeRendering(): void { + this.checkForWrapper(); + this.attachCloseOverflowHandlers(); + } onAfterRendering(): void { this._isRendering = false; } + + onExitDOM(): void { + this.detachCloseOverflowHandlers(); + } + + /** + * Wrapped component slot. + * @public + * @since 2.20.0 + */ + + @slot({ + "default": true, type: HTMLElement, invalidateOnChildChange: true, + }) + item!: IOverflowToolbarItem[]; + + // Method called by ui5-toolbar to inform about the existing toolbar wrapper + checkForWrapper() { + if (this._wrapperChecked) { + return; + } + this._wrapperChecked = true; + + const tagName = this.itemTagName as keyof typeof this.predefinedWrapperSet; + const ctor = this.constructor as typeof UI5Element; + const wrapperName = ctor?.getMetadata ? ctor.getMetadata().getPureTag() : this.tagName; + if (wrapperName === "ui5-toolbar-item" + && 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 item = Array.isArray(this.item) ? this.item[0] : this.item; + + const closeEvents = this.closeOverflowSet[this.itemTagName as keyof typeof this.closeOverflowSet] || []; + if (!item) { + return [...closeEvents]; + } + const overflowCloseEvents = Array.isArray(item.overflowCloseEvents) ? item.overflowCloseEvents : []; + + return [...closeEvents, ...overflowCloseEvents]; + } + + 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 @@ -92,6 +201,15 @@ class ToolbarItem extends UI5Element { return true; } + get itemTagName() { + const ctor = this.getSlottedNodes("item")[0]?.constructor as typeof UI5Element; + return ctor?.getMetadata ? ctor.getMetadata().getPureTag() : this.getSlottedNodes("item")[0]?.tagName; + } + + get hasOverflow(): boolean { + return this.item[0]?.hasOverflow ?? false; + } + /** * Returns if the item is separator. * @protected @@ -117,5 +235,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/ToolbarTemplate.tsx b/packages/main/src/ToolbarTemplate.tsx index 9f086627b48f..232bb74a172e 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,12 @@ export default function ToolbarTemplate(this: Toolbar) { "ui5-overflow-list": true }}> {this.overflowItems.map(item => { - if (item.isSeparator) { - return ( -
- -
- ); - } return ( -
+
); diff --git a/packages/main/src/bundle.esm.ts b/packages/main/src/bundle.esm.ts index 2cbd6114839f..59508d9020e7 100644 --- a/packages/main/src/bundle.esm.ts +++ b/packages/main/src/bundle.esm.ts @@ -119,6 +119,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..1c61f4911d62 --- /dev/null +++ b/packages/main/src/themes/ToolbarItem.css @@ -0,0 +1,8 @@ +:host() { + display: inline-block; + height: 100%; +} + +:host([is-overflowed]) ::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..31c810c2789c --- /dev/null +++ b/packages/main/test/pages/ToolbarItems.html @@ -0,0 +1,194 @@ + + + + + + + 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 diff --git a/packages/website/docs/_components_pages/main/Toolbar/Toolbar.mdx b/packages/website/docs/_components_pages/main/Toolbar/Toolbar.mdx index 44c3551eab17..dd2cd194baa1 100644 --- a/packages/website/docs/_components_pages/main/Toolbar/Toolbar.mdx +++ b/packages/website/docs/_components_pages/main/Toolbar/Toolbar.mdx @@ -7,6 +7,8 @@ import AlwaysOverflowingItems from "../../../_samples/main/Toolbar/AlwaysOverflo import NeverOverflowingItems from "../../../_samples/main/Toolbar/NeverOverflowingItems/NeverOverflowingItems.md"; import SpacerAndSeparator from "../../../_samples/main/Toolbar/SpacerAndSeparator/SpacerAndSeparator.md"; import ItemsAlignment from "../../../_samples/main/Toolbar/ItemsAlignment/ItemsAlignment.md"; +import ToolbarItem from "../../../_samples/main/Toolbar/ToolbarItem/ToolbarItem.md"; + <%COMPONENT_OVERVIEW%> @@ -34,3 +36,6 @@ To force items staying always visible and never overflow, set "overflow-priority You can align items to the Start, or to the End via the "align-content" property +### ToolbarItem +ToolbarItem wrapper used to add any component to Toolbar + diff --git a/packages/website/docs/_components_pages/main/Toolbar/ToolbarItem.mdx b/packages/website/docs/_components_pages/main/Toolbar/ToolbarItem.mdx new file mode 100644 index 000000000000..e4267eefb27a --- /dev/null +++ b/packages/website/docs/_components_pages/main/Toolbar/ToolbarItem.mdx @@ -0,0 +1,8 @@ +--- +slug: ../../ToolbarItem +sidebar_class_name: newComponentBadge +--- + +<%COMPONENT_OVERVIEW%> + +<%COMPONENT_METADATA%> \ No newline at end of file diff --git a/packages/website/docs/_samples/main/Toolbar/ToolbarItem/ToolbarItem.md b/packages/website/docs/_samples/main/Toolbar/ToolbarItem/ToolbarItem.md new file mode 100644 index 000000000000..ffccbf6dd13e --- /dev/null +++ b/packages/website/docs/_samples/main/Toolbar/ToolbarItem/ToolbarItem.md @@ -0,0 +1,4 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; + + \ No newline at end of file diff --git a/packages/website/docs/_samples/main/Toolbar/ToolbarItem/main.js b/packages/website/docs/_samples/main/Toolbar/ToolbarItem/main.js new file mode 100644 index 000000000000..1a6881f7a597 --- /dev/null +++ b/packages/website/docs/_samples/main/Toolbar/ToolbarItem/main.js @@ -0,0 +1,9 @@ +import "@ui5/webcomponents/dist/Toolbar.js"; // For ui5-toolbar +import "@ui5/webcomponents/dist/ToolbarItem.js"; // For ui5-toolbar-item +import "@ui5/webcomponents/dist/ComboBox.js"; // For ui5-combobox +import "@ui5/webcomponents/dist/ComboBoxItem.js"; // For ui5-combobox-item +import "@ui5/webcomponents/dist/MultiComboBox.js"; // For ui5-multi-combobox +import "@ui5/webcomponents/dist/MultiComboBoxItem.js"; // For ui5-mcb-item +import "@ui5/webcomponents/dist/DatePicker.js"; // For ui5-datepicker +import "@ui5/webcomponents/dist/Switch.js"; // For ui5-switch +import "@ui5/webcomponents/dist/Popover.js"; // For ui5-popover \ No newline at end of file diff --git a/packages/website/docs/_samples/main/Toolbar/ToolbarItem/sample.html b/packages/website/docs/_samples/main/Toolbar/ToolbarItem/sample.html new file mode 100644 index 000000000000..c111722bcf34 --- /dev/null +++ b/packages/website/docs/_samples/main/Toolbar/ToolbarItem/sample.html @@ -0,0 +1,69 @@ + + + + + + + Sample + + + + + + + +
+ + + +
+
+ + + +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + \ No newline at end of file