diff --git a/docs/internal/Toolbar.md b/docs/internal/Toolbar.md index 9f4a69215b91..7861d7e64b2f 100644 --- a/docs/internal/Toolbar.md +++ b/docs/internal/Toolbar.md @@ -1,6 +1,6 @@ -# Creating a web component abstract item to be used inside Toolbar +# Creating a web component toolbar item to be used inside Toolbar -*This section explains how to build abstract items in order to be compatible with UI5 Toolbar.* +*This section explains how to build toolbar items in order to be compatible with UI5 Toolbar.* *It will guide you through the process of how we created `ui5-toolbar-button`, to be compatible with `ui5-toolbar`. Currently developed items can be used without those efforts. They are:* 1. ui5-toolbar-button @@ -8,13 +8,8 @@ compatible with `ui5-toolbar`. Currently developed items can be used without tho 3. ui5-toolbar-separator 4. ui5-toolbar-spacer -## Abstract items -  -### Why are abstract items needed? -  -When the toolbar renders its slotted items within a popover in the static area, simply relocating the actual DOM nodes within its slots can lead to reference issues, causing the slotted nodes to lose their parent reference (e.g., the toolbar). This is the reason why the toolbar must operate with abstract items. Abstract items are not rendered directly within the DOM; instead, they function as data used by the toolbar to produce corresponding physical web components. On the other hand, useful modifications detected by the toolbar on the physical items are synchronised with the abstract ones. (see step [Events](#events)) -  -The `ui5-toolbar` is a composite web component, that slots different UI5 components, designing them as abstract items. They can contain +## Toolbar Items +The `ui5-toolbar` is a composite web component, that slots different UI5 components, designing them as toolbar items. They can contain properties, slots and events, and they can match the API of already existing component. In order to be suitable for usage inside `ui5-toolbar`, each component should adhere to following guidelines: @@ -25,17 +20,19 @@ In order to be suitable for usage inside `ui5-toolbar`, each component should ad ToolbarButton.ts ``` -2. The new component needs to implement two template files with name of the following type: +2. The new component needs to implement template file with name of the following type: ```javascript -ToolbarButton.hbs and ToolbarPopoverButton.hbs +ToolbarButton.hbs ``` -3. It needs to implement **customElement** decorator, which is good to contain custom tag name: +3. It needs to implement **customElement** decorator, which is good to contain custom tag name, template and renderer: ```javascript @customElement({ - tag: "ui5-toolbar-button" + tag: "ui5-toolbar-button", + template: ToolbarButtonTemplate, + renderer: jsxRenderer, }) ``` @@ -45,29 +42,7 @@ ToolbarButton.hbs and ToolbarPopoverButton.hbs class ToolbarButton extends ToolbarItem ``` -5. Inside the module there should be two template getters: for toolbar and popover representation. - -```javascript -static get toolbarTemplate() { - return ToolbarButtonTemplate; -} - -static get toolbarPopoverTemplate() { - return ToolbarPopoverButtonTemplate; -} -``` - -6. After the class declaration there should be a registry call for the item inside the toolbar. **registerToolbarItem** helper should be added as a dependency. - -```javascript -import { registerToolbarItem } from "./ToolbarRegistry.js"; -``` - -```javascript -registerToolbarItem(ToolbarButton); -``` - -7. In the templates there should be mapping of the properties that need to be used in the component inside Toolbar. +5. In the templates there should be mapping of the properties that need to be used in the component inside Toolbar. Inside ToolbarButton.ts:   @@ -91,14 +66,13 @@ Inside ToolbarButtonTemplate.hbs:   {{this.text}} ``` -8. The new component's DOM root element needs to have `"ui5-tb-item"` CSS class in order to get default styles for item (margins etc.). -9. The new class needs to be added to the bundle file in the corresponding library. +6. The new class needs to be added to the bundle file in the corresponding library. Inside bundle.common.js: ```javascript import ToolbarButton from "./dist/ToolbarButton.js"; ``` -10. Use your newly created component inside the ui5-toolbar like this: +7. Use your newly created component inside the ui5-toolbar like this: ```html diff --git a/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx b/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx index 367c465ad948..4aa5e9d41c8b 100644 --- a/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx +++ b/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx @@ -298,9 +298,11 @@ describe("Events", () => { settings.get(0).addEventListener("before-close", cy.stub().as("beforeClosed")); }); - cy.get("@settings").shadow().find("[ui5-toolbar]").shadow() - .find("[ui5-button]") + cy.get("@settings").shadow().find("[ui5-toolbar]") + .find("[ui5-toolbar-button]") .first() + .shadow() + .find("[ui5-button]") .as("closeButton"); cy.get("@closeButton") .click(); @@ -320,9 +322,11 @@ describe("Events", () => { settings.get(0).addEventListener("close", cy.stub().as("closed")); }); - cy.get("@settings").shadow().find("[ui5-toolbar]").shadow() - .find("[ui5-button]") + cy.get("@settings").shadow().find("[ui5-toolbar]") + .find("[ui5-toolbar-button]") .first() + .shadow() + .find("[ui5-button]") .as("closeButton"); cy.get("@closeButton") .click(); @@ -487,9 +491,11 @@ describe("Responsiveness", () => { }); cy.get("@settings").shadow().find("[ui5-dialog]").should("exist"); cy.get("@settings").shadow().find("[ui5-toolbar]").should("exist"); - cy.get("@settings").shadow().find("[ui5-toolbar]").shadow() - .find("[ui5-button]") + cy.get("@settings").shadow().find("[ui5-toolbar]") + .find("[ui5-toolbar-button]") .first() + .shadow() + .find("[ui5-button]") .as("closeButton"); cy.get("@closeButton") .click(); @@ -523,9 +529,11 @@ describe("Responsiveness", () => { .find("[ui5-li]") .as("item"); cy.get("@item").should("have.attr", "type", "Navigation"); - cy.get("@settings").shadow().find("[ui5-toolbar]").shadow() - .find("[ui5-button]") + cy.get("@settings").shadow().find("[ui5-toolbar]") + .find("[ui5-toolbar-button]") .first() + .shadow() + .find("[ui5-button]") .as("closeButton"); cy.get("@closeButton") .click(); diff --git a/packages/fiori/test/specs/DynamicPage.spec.js b/packages/fiori/test/specs/DynamicPage.spec.js index e3fa40b14c57..7db0717fc01c 100644 --- a/packages/fiori/test/specs/DynamicPage.spec.js +++ b/packages/fiori/test/specs/DynamicPage.spec.js @@ -153,7 +153,7 @@ describe("Page general interaction", () => { it("allows toggle the footer", async () => { const footer = await browser.$("#page").shadow$(".ui5-dynamic-page-footer"); - const toggleFooterButton = await browser.$("#actionsToolbar").shadow$("#toggleFooterBtn"); + const toggleFooterButton = await browser.$("#actionsToolbar").$("#toggleFooterBtn"); assert.ok(await footer.isDisplayedInViewport(), "Footer should be visible."); diff --git a/packages/main/cypress/specs/Toolbar.cy.tsx b/packages/main/cypress/specs/Toolbar.cy.tsx index 8104e92345d7..bc4858f2787c 100644 --- a/packages/main/cypress/specs/Toolbar.cy.tsx +++ b/packages/main/cypress/specs/Toolbar.cy.tsx @@ -133,12 +133,15 @@ describe("Toolbar general interaction", () => { ); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(500); + cy.get("ui5-toolbar-button[text='Button 1']") .then(button => { button.get(0).addEventListener("click", cy.stub().as("clicked")); }); - cy.get("ui5-button", { includeShadowDom: true }).contains("Button 1") + cy.get("ui5-button", { includeShadowDom: true }).contains("Button 1") .click(); cy.get("@clicked") @@ -169,6 +172,77 @@ describe("Toolbar general interaction", () => { cy.get("@closed") .should("have.been.calledOnce"); }); + + it("Should move button with alwaysOverflow priority to overflow popover", async () => { + + cy.mount( + + + + + ); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(500); + + const otb = cy.get("#otb_d"); + + cy.get("otb") + .shadow() + .find(".ui5-tb-overflow-btn") + .click(); + const overflowButton = otb.shadow().find(".ui5-tb-overflow-btn"); + + cy.get("#otb_d") + .shadow() + .find(".ui5-overflow-popover") + .should("have.attr", "open", "true"); + overflowButton.click(); + cy.wait(500); + + cy.get("@popover") + .find(".ui5-tb-popover-item") + .should("have.length", 2); + + cy.get("@popover") + .find(`[stable-dom-ref="tb-button-employee-d"]`) + .should("have.class", "ui5-tb-popover-item"); + }); + + it("Should properly prevent the closing of the overflow menu when preventClosing = true", () => { + cy.mount( +
+ + + + + + + test + + + +
+ ) + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(1000); + + cy.get("#testEventpreventClosing-toolbar") + .shadow() + .find(".ui5-tb-overflow-btn") + .click(); + cy.get("[ui5-toolbar-select]") + .shadow() + .find("[ui5-select]") + .click(); + + cy.get("#testEventpreventClosing-toolbar") + .shadow() + .find(".ui5-overflow-popover") + .should("have.attr", "open", "open"); + }); + }); describe("Accessibility", () => { @@ -185,6 +259,7 @@ describe("Accessibility", () => { ); + cy.wait(1000); cy.get("ui5-toolbar") .shadow() @@ -192,3 +267,101 @@ describe("Accessibility", () => { .should("have.attr", "accessible-name", "Available Values"); }); }); + +//ToolbarSelect +describe("Toolbar Select", () => { + it("Should render the select with the correct attributes inside the popover", () => { + cy.mount( +
+ + + 1 + 2 + 3 + + + + + 1 + 2 + 3 + + +
+ ); + + const otb = cy.get("#otb_e").as("otb"); + + cy.get("@otb") + .shadow() + .find(".ui5-tb-overflow-btn") + .click(); + const overflowButton = otb.shadow().find(".ui5-tb-overflow-btn"); + + cy.get("@otb") + .shadow() + .find(".ui5-overflow-popover").as("popover") + .should("have.attr", "open", "open"); + overflowButton.click(); + cy.wait(500); + + cy.get("@otb") + .find("#toolbar-select") + .should("have.attr", "value-state", "Critical") + + .should("have.attr", "accessible-name", "Add") + + .should("have.attr", "accessible-name-ref", "title") + + cy.get("@otb") + .find(".custom-class") + .should("have.attr", "disabled", "disabled"); + + }); + + //ToolbarButton + it("Should render the button with the correct text inside the popover", async () => { + cy.viewport(200, 1080); + + cy.get("#otb_d").within(() => { + cy.get(".ui5-tb-overflow-btn").click(); + cy.get("ui5-popover").shadow().within(() => { + cy.get("ui5-toolbar-button").shadow().within(() => { + cy.get("ui5-button").then($button => { + expect($button).to.have.text("Back"); + expect($button).to.have.attr("design", "Emphasized"); + expect($button).to.have.attr("disabled", "true"); + expect($button).to.have.attr("icon", "sap-icon://add"); + expect($button).to.have.attr("end-icon", "sap-icon://employee"); + expect($button).to.have.attr("tooltip", "Add"); + }); + }); + }); + }); + }); + + it ("Should render the button with the correct accessible name inside the popover", async () => { + cy.viewport(100, 1080); + + cy.get("#otb_d").within(() => { + cy.get(".ui5-tb-overflow-btn").click(); + cy.get("ui5-popover").shadow().within(() => { + cy.get("ui5-button[accessible-name]").then($button => { + expect($button).to.have.attr("accessible-name", "Add"); + expect($button).to.have.attr("accessible-name-ref", "btn"); + }); + }); + }); + }); + + it("Should render the button with the correct accessibilityAttributes inside the popover", async () => { + cy.viewport(100, 1080); + + cy.get("#otb_d").within(() => { + cy.get(".ui5-tb-overflow-btn").click(); + cy.get("ui5-popover").shadow().within(() => { + cy.get("ui5-button[accessible-name]").invoke("prop", "accessibilityAttributes").should("have.property", "expanded", "true"); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/main/src/Toolbar.ts b/packages/main/src/Toolbar.ts index f3e54d108498..d502526d6685 100644 --- a/packages/main/src/Toolbar.ts +++ b/packages/main/src/Toolbar.ts @@ -32,11 +32,6 @@ import ToolbarItemOverflowBehavior from "./types/ToolbarItemOverflowBehavior.js" import type ToolbarItem from "./ToolbarItem.js"; import type ToolbarSeparator from "./ToolbarSeparator.js"; -import { - getRegisteredToolbarItem, - getRegisteredStyles, -} from "./ToolbarRegistry.js"; - import type Button from "./Button.js"; import type Popover from "./Popover.js"; @@ -158,7 +153,9 @@ class Toolbar extends UI5Element { * **Note:** Currently only `ui5-toolbar-button`, `ui5-toolbar-select`, `ui5-toolbar-separator` and `ui5-toolbar-spacer` are allowed here. * @public */ - @slot({ "default": true, type: HTMLElement, invalidateOnChildChange: true }) + @slot({ + "default": true, type: HTMLElement, invalidateOnChildChange: true, individualSlots: true, + }) items!: Array _onResize!: ResizeObserverCallback; @@ -171,11 +168,9 @@ class Toolbar extends UI5Element { ITEMS_WIDTH_MAP: Map = new Map(); static get styles() { - const styles = getRegisteredStyles(); return [ ToolbarCss, ToolbarPopoverCss, - ...styles, ]; } @@ -311,6 +306,9 @@ class Toolbar extends UI5Element { this.storeItemsWidth(); this.processOverflowLayout(); + this.getItemsInfo(this.items).forEach(item => { + item.context._isOverflowed = this.overflowItems.map(overflowItem => overflowItem.context).indexOf(item.context) !== -1; + }); } /** @@ -502,16 +500,7 @@ class Toolbar extends UI5Element { getItemsInfo(items: Array) { return items.map((item: ToolbarItem) => { - const ctor = item.constructor as typeof ToolbarItem; - const ElementClass = getRegisteredToolbarItem(ctor.getMetadata().getPureTag()); - - if (!ElementClass) { - return null; - } - const toolbarItem = { - toolbarTemplate: ElementClass.toolbarTemplate, - toolbarPopoverTemplate: ElementClass.toolbarPopoverTemplate, context: item, }; @@ -526,11 +515,11 @@ class Toolbar extends UI5Element { } const id: string = item._id; // Measure rendered width for spacers with width, and for normal items - const renderedItem = this.getRegisteredToolbarItemByID(id); + const renderedItem = this.shadowRoot!.querySelector(`#${item.slot}`); let itemWidth = 0; - if (renderedItem) { + if (renderedItem && renderedItem.offsetWidth) { const ItemCSSStyleSet = getComputedStyle(renderedItem); itemWidth = renderedItem.offsetWidth + parsePxValue(ItemCSSStyleSet, "margin-inline-end") + parsePxValue(ItemCSSStyleSet, "margin-inline-start"); diff --git a/packages/main/src/ToolbarButton.ts b/packages/main/src/ToolbarButton.ts index 191857312379..b3e2f539ac3a 100644 --- a/packages/main/src/ToolbarButton.ts +++ b/packages/main/src/ToolbarButton.ts @@ -1,3 +1,4 @@ +import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; @@ -6,11 +7,6 @@ import type ButtonDesign from "./types/ButtonDesign.js"; import ToolbarItem from "./ToolbarItem.js"; import ToolbarButtonTemplate from "./ToolbarButtonTemplate.js"; -import ToolbarPopoverButtonTemplate from "./ToolbarPopoverButtonTemplate.js"; - -import ToolbarButtonPopoverCss from "./generated/themes/ToolbarButtonPopover.css.js"; - -import { registerToolbarItem } from "./ToolbarRegistry.js"; type ToolbarButtonAccessibilityAttributes = ButtonAccessibilityAttributes; @@ -31,7 +27,8 @@ type ToolbarButtonAccessibilityAttributes = ButtonAccessibilityAttributes; */ @customElement({ tag: "ui5-toolbar-button", - styles: ToolbarButtonPopoverCss, + template: ToolbarButtonTemplate, + renderer: jsxRenderer, }) /** @@ -175,14 +172,6 @@ class ToolbarButton extends ToolbarItem { return true; } - static get toolbarTemplate() { - return ToolbarButtonTemplate; - } - - static get toolbarPopoverTemplate() { - return ToolbarPopoverButtonTemplate; - } - onClick(e: Event) { e.stopImmediatePropagation(); const prevented = !this.fireDecoratorEvent("click", { targetRef: e.target as HTMLElement }); @@ -190,9 +179,11 @@ class ToolbarButton extends ToolbarItem { this.fireDecoratorEvent("close-overflow"); } } -} -registerToolbarItem(ToolbarButton); + get class() { + return `${this._isOverflowed ? "ui5-tb-popover-item" : "ui5-tb-item"} ui5-tb-button`; + } +} ToolbarButton.define(); diff --git a/packages/main/src/ToolbarButtonTemplate.tsx b/packages/main/src/ToolbarButtonTemplate.tsx index aadcab2f8d7a..5d1b625595c8 100644 --- a/packages/main/src/ToolbarButtonTemplate.tsx +++ b/packages/main/src/ToolbarButtonTemplate.tsx @@ -4,11 +4,11 @@ import Button from "./Button.js"; export default function ToolbarButtonTemplate(this: ToolbarButton) { return ( - ); -} diff --git a/packages/main/src/ToolbarPopoverSelectTemplate.tsx b/packages/main/src/ToolbarPopoverSelectTemplate.tsx deleted file mode 100644 index 2e897caf4c76..000000000000 --- a/packages/main/src/ToolbarPopoverSelectTemplate.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type ToolbarSelect from "./ToolbarSelect.js"; -import Select from "./Select.js"; -import Option from "./Option.js"; - -export default function ToolbarPopoverSelectTemplate(this: ToolbarSelect) { - return ( - - ); -} diff --git a/packages/main/src/ToolbarPopoverSeparatorTemplate.tsx b/packages/main/src/ToolbarPopoverSeparatorTemplate.tsx deleted file mode 100644 index 9098f211a12a..000000000000 --- a/packages/main/src/ToolbarPopoverSeparatorTemplate.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import type ToolbarSeparator from "./ToolbarSeparator.js"; - -export default function ToolbarPopoverSeparator(this: ToolbarSeparator) { - return ( -
- ); -} diff --git a/packages/main/src/ToolbarPopoverTemplate.tsx b/packages/main/src/ToolbarPopoverTemplate.tsx deleted file mode 100644 index a6d3ead173cd..000000000000 --- a/packages/main/src/ToolbarPopoverTemplate.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type Toolbar from "./Toolbar.js"; -import Popover from "./Popover.js"; - -export default function ToolbarPopoverTemplate(this: Toolbar) { - return ( - -
- { this.overflowItems.map(item => ( - item.toolbarPopoverTemplate.call(item.context) - ))} -
-
- ); -} diff --git a/packages/main/src/ToolbarRegistry.ts b/packages/main/src/ToolbarRegistry.ts deleted file mode 100644 index 0339efbd578e..000000000000 --- a/packages/main/src/ToolbarRegistry.ts +++ /dev/null @@ -1,27 +0,0 @@ -import getSharedResource from "@ui5/webcomponents-base/dist/getSharedResource.js"; - -import type ToolbarItem from "./ToolbarItem.js"; - -const registry = getSharedResource>("ToolbarItem.registry", new Map()); - -const registerToolbarItem = (ElementClass: typeof ToolbarItem) => { - registry.set(ElementClass.getMetadata().getPureTag(), ElementClass); -}; - -const getRegisteredToolbarItem = (name: string) => { - if (!registry.has(name)) { - throw new Error(`No template found for ${name}`); - } - - return registry.get(name); -}; - -const getRegisteredStyles = () => { - return [...registry.values()].map((ElementClass: typeof ToolbarItem) => ElementClass.styles); -}; - -export { - registerToolbarItem, - getRegisteredToolbarItem, - getRegisteredStyles, -}; diff --git a/packages/main/src/ToolbarSelect.ts b/packages/main/src/ToolbarSelect.ts index 68c3c6466dbc..79faa0f938eb 100644 --- a/packages/main/src/ToolbarSelect.ts +++ b/packages/main/src/ToolbarSelect.ts @@ -1,15 +1,13 @@ +import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import type ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; - -import { registerToolbarItem } from "./ToolbarRegistry.js"; +import ToolbarSelectCss from "./generated/themes/ToolbarSelect.css.js"; // Templates - import ToolbarSelectTemplate from "./ToolbarSelectTemplate.js"; -import ToolbarPopoverSelectTemplate from "./ToolbarPopoverSelectTemplate.js"; import ToolbarItem from "./ToolbarItem.js"; import type { ToolbarItemEventDetail } from "./ToolbarItem.js"; import type ToolbarSelectOption from "./ToolbarSelectOption.js"; @@ -36,6 +34,9 @@ type ToolbarSelectChangeEventDetail = ToolbarItemEventDetail & SelectChangeEvent */ @customElement({ tag: "ui5-toolbar-select", + template: ToolbarSelectTemplate, + renderer: jsxRenderer, + styles: ToolbarSelectCss, }) /** @@ -123,14 +124,6 @@ class ToolbarSelect extends ToolbarItem { @property() accessibleNameRef?: string; - static get toolbarTemplate() { - return ToolbarSelectTemplate; - } - - static get toolbarPopoverTemplate() { - return ToolbarPopoverSelectTemplate; - } - onClick(e: Event): void { e.stopImmediatePropagation(); const prevented = !this.fireDecoratorEvent("click", { targetRef: e.target as HTMLElement }); @@ -183,8 +176,6 @@ class ToolbarSelect extends ToolbarItem { } } -registerToolbarItem(ToolbarSelect); - ToolbarSelect.define(); export default ToolbarSelect; diff --git a/packages/main/src/ToolbarSelectTemplate.tsx b/packages/main/src/ToolbarSelectTemplate.tsx index ec95a7e27f8c..8fc0fd8c9852 100644 --- a/packages/main/src/ToolbarSelectTemplate.tsx +++ b/packages/main/src/ToolbarSelectTemplate.tsx @@ -5,7 +5,7 @@ import Option from "./Option.js"; export default function ToolbarSelectTemplate(this: ToolbarSelect) { return (