diff --git a/packages/main/cypress/specs/ToolbarItem.cy.tsx b/packages/main/cypress/specs/ToolbarItem.cy.tsx index dc579edc71858..f6337db58ca97 100644 --- a/packages/main/cypress/specs/ToolbarItem.cy.tsx +++ b/packages/main/cypress/specs/ToolbarItem.cy.tsx @@ -11,6 +11,7 @@ import DatePicker from "../../src/DatePicker.js"; import Select from "../../src/Select.js"; import Option from "../../src/Option.js"; import CheckBox from "../../src/CheckBox.js"; +import Title from "../../src/Title.js"; import add from "@ui5/webcomponents-icons/dist/add.js"; describe("Toolbar Item Properties", () => { @@ -790,4 +791,106 @@ describe("ToolbarItem CSS Custom State", () => { expect(item._internals.states.has("overflowed")).to.be.true; }); }); -}); \ No newline at end of file +}); + +describe("ToolbarItem shrink-content", () => { + it("Should keep the toolbar button visible when shrink-content is set on a wide NeverOverflow item", () => { + cy.mount( +
+ + + Super Super SuperSuperSuperSuper SuperSuperSuperSuper SuperSuperSuperSuper Long Text + + + +
+ ); + + // The button must be visible — the title shrinks to yield space + cy.get("[ui5-toolbar-button][text='Button 1']") + .should("be.visible"); + + // No overflow button needed — the title shrinks instead of pushing items out + cy.get("[ui5-toolbar]") + .shadow() + .find(".ui5-tb-overflow-btn") + .should("have.class", "ui5-tb-overflow-btn-hidden"); + }); + + it("Should apply ui5-tb-self-overflow class to the item wrapper when shrink-content is set", () => { + cy.mount( +
+ + + Super Super SuperSuperSuperSuper SuperSuperSuperSuper Long Text + + + +
+ ); + + // hasOverflow must be true when shrink-content is set + cy.get("[ui5-toolbar-item]").then($item => { + const item = $item[0] as ToolbarItem; + expect(item.hasOverflow).to.be.true; + }); + + // The toolbar wrapper div must carry the ui5-tb-self-overflow CSS class + cy.get("[ui5-toolbar]").then($toolbar => { + const toolbar = $toolbar[0] as Toolbar; + const item = document.querySelector("[ui5-toolbar-item]") as ToolbarItem; + const wrapper = toolbar.shadowRoot!.querySelector(`#${item._individualSlot}`) as HTMLElement; + expect(wrapper.classList.contains("ui5-tb-self-overflow")).to.be.true; + }); + }); + + it("Should NOT apply shrink behavior when shrink-content is not set", () => { + cy.mount( +
+ + + Super Super SuperSuperSuperSuper SuperSuperSuperSuper Long Text + + + +
+ ); + + // hasOverflow must be false when shrink-content is not set + cy.get("[ui5-toolbar-item]").then($item => { + const item = $item[0] as ToolbarItem; + expect(item.hasOverflow).to.be.false; + }); + }); + + it("Should keep the button visible after the toolbar is resized", () => { + cy.mount( +
+ + + Super Super SuperSuperSuperSuper SuperSuperSuperSuper SuperSuperSuperSuper Long Text + + + +
+ ); + + // Button is visible at narrow width + cy.get("[ui5-toolbar-button][text='Button 1']") + .should("be.visible"); + + // Widen the container + cy.get("#shrink-resize-container") + .invoke("css", "width", "800px"); + + // Button must still be visible after resize + cy.get("[ui5-toolbar-button][text='Button 1']") + .should("be.visible"); + + // Overflow button must remain hidden throughout + cy.get("[ui5-toolbar]") + .shadow() + .find(".ui5-tb-overflow-btn") + .should("have.class", "ui5-tb-overflow-btn-hidden"); + }); +}); diff --git a/packages/main/src/Toolbar.ts b/packages/main/src/Toolbar.ts index b8b12089dc9ba..8b6761532d7de 100644 --- a/packages/main/src/Toolbar.ts +++ b/packages/main/src/Toolbar.ts @@ -354,7 +354,7 @@ class Toolbar extends UI5Element { addItemsAdditionalProperties(item: ToolbarItemBase) { item.isOverflowed = this.overflowItems.indexOf(item) !== -1; const itemWrapper = this.shadowRoot!.querySelector(`#${item._individualSlot}`) as HTMLElement; - if (item.hasOverflow && !item.isOverflowed && itemWrapper) { + if (item.hasOverflow && item.clampMaxWidth && !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, @@ -433,8 +433,13 @@ class Toolbar extends UI5Element { this.items.forEach(item => { const itemWidth = this.getItemWidth(item); + // shrinkContent items (clampMaxWidth=false) are intentionally included in totalWidth: + // getItemWidth returns 0 on first render (_isRendering=true) and the cached flex-distributed + // width on subsequent renders, both of which keep overflowSpace non-positive so distributeItems + // is not incorrectly triggered. They are excluded from minWidth only, as their rendered + // width is not a stable natural-width value. totalWidth += itemWidth; - if (item.overflowPriority === ToolbarItemOverflowBehavior.NeverOverflow) { + if (item.overflowPriority === ToolbarItemOverflowBehavior.NeverOverflow && item.clampMaxWidth) { minWidth += itemWidth; } this.ITEMS_WIDTH_MAP.set(item._id, itemWidth); diff --git a/packages/main/src/ToolbarItem.ts b/packages/main/src/ToolbarItem.ts index 2a0458cc0dc66..707ef495bbf3b 100644 --- a/packages/main/src/ToolbarItem.ts +++ b/packages/main/src/ToolbarItem.ts @@ -1,4 +1,5 @@ import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js"; +import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js"; @@ -115,6 +116,18 @@ class ToolbarItem extends ToolbarItemBase { }) item!: DefaultSlot; + /** + * Defines whether the item should shrink to fit the available toolbar space + * instead of pushing other items out of view. + * Use this when the slotted content (e.g. a long title) should yield space + * to other toolbar items rather than overflow them. + * @default false + * @public + * @since 2.25.0 + */ + @property({ type: Boolean }) + shrinkContent = false; + // Method called by ui5-toolbar to inform about the existing toolbar wrapper checkForWrapper() { if (this._wrapperChecked) { @@ -209,7 +222,11 @@ class ToolbarItem extends ToolbarItemBase { } get hasOverflow(): boolean { - return this.item[0]?.hasOverflow ?? false; + return this.item[0]?.hasOverflow || this.shrinkContent; + } + + get clampMaxWidth(): boolean { + return !this.shrinkContent; } getFocusDomRef(): HTMLElement | undefined { diff --git a/packages/main/src/ToolbarItemBase.ts b/packages/main/src/ToolbarItemBase.ts index 989819ad242eb..0037154491ac5 100644 --- a/packages/main/src/ToolbarItemBase.ts +++ b/packages/main/src/ToolbarItemBase.ts @@ -132,6 +132,17 @@ class ToolbarItemBase extends UI5Element { return false; } + /** + * Returns whether the toolbar should apply a max-width cap to this item + * during the self-overflow measurement cycle. + * Override to return false for items that must remain free to grow and shrink + * (i.e. items that use flex-shrink to yield space rather than overflowing). + * @protected + */ + get clampMaxWidth(): boolean { + return true; + } + /** * Returns if the item is separator. * @protected diff --git a/packages/website/docs/_samples/main/Toolbar/ShrinkContent/ShrinkContent.md b/packages/website/docs/_samples/main/Toolbar/ShrinkContent/ShrinkContent.md new file mode 100644 index 0000000000000..0c062a836e844 --- /dev/null +++ b/packages/website/docs/_samples/main/Toolbar/ShrinkContent/ShrinkContent.md @@ -0,0 +1,5 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; +import react from '!!raw-loader!./sample.tsx'; + + diff --git a/packages/website/docs/_samples/main/Toolbar/ShrinkContent/main.js b/packages/website/docs/_samples/main/Toolbar/ShrinkContent/main.js new file mode 100644 index 0000000000000..c003cbcdaa967 --- /dev/null +++ b/packages/website/docs/_samples/main/Toolbar/ShrinkContent/main.js @@ -0,0 +1,4 @@ +import "@ui5/webcomponents/dist/Toolbar.js"; +import "@ui5/webcomponents/dist/ToolbarItem.js"; +import "@ui5/webcomponents/dist/ToolbarButton.js"; +import "@ui5/webcomponents/dist/Title.js"; diff --git a/packages/website/docs/_samples/main/Toolbar/ShrinkContent/sample.html b/packages/website/docs/_samples/main/Toolbar/ShrinkContent/sample.html new file mode 100644 index 0000000000000..450b3ea6181db --- /dev/null +++ b/packages/website/docs/_samples/main/Toolbar/ShrinkContent/sample.html @@ -0,0 +1,24 @@ + + + + + + + Sample + + + + + + + Super Long Title That Should Shrink To Fit Available Space + + + + + + + + + + diff --git a/packages/website/docs/_samples/main/Toolbar/ShrinkContent/sample.tsx b/packages/website/docs/_samples/main/Toolbar/ShrinkContent/sample.tsx new file mode 100644 index 0000000000000..2dcb10e174dea --- /dev/null +++ b/packages/website/docs/_samples/main/Toolbar/ShrinkContent/sample.tsx @@ -0,0 +1,28 @@ +import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js"; +import ToolbarClass from "@ui5/webcomponents/dist/Toolbar.js"; +import ToolbarItemClass from "@ui5/webcomponents/dist/ToolbarItem.js"; +import ToolbarButtonClass from "@ui5/webcomponents/dist/ToolbarButton.js"; +import TitleClass from "@ui5/webcomponents/dist/Title.js"; + +const Toolbar = createReactComponent(ToolbarClass); +const ToolbarItem = createReactComponent(ToolbarItemClass); +const ToolbarButton = createReactComponent(ToolbarButtonClass); +const Title = createReactComponent(TitleClass); + +function App() { + return ( + <> + + + + Super Long Title That Should Shrink To Fit Available Space + + + + + + + ); +} + +export default App;