From 3b8b6b5d12cd206f235780c87a796cefc2eb5edd Mon Sep 17 00:00:00 2001 From: Nikola Anachkov Date: Wed, 2 Sep 2026 10:44:31 +0300 Subject: [PATCH 1/2] feat(ui5-shellbar): enhance responsive behaviour for custom branding --- packages/fiori/src/ShellBar.ts | 129 ++++++- packages/fiori/src/ShellBarBranding.ts | 8 - .../fiori/src/ShellBarBrandingTemplate.tsx | 8 +- packages/fiori/src/ShellBarTemplate.tsx | 40 +- .../fiori/src/shellbar/ShellBarOverflow.ts | 106 +++++- packages/fiori/src/themes/ShellBar.css | 47 ++- .../fiori/src/themes/ShellBarBranding.css | 56 ++- .../fiori/test/pages/ShellBarBranding.html | 357 ++++++++++++++++++ 8 files changed, 700 insertions(+), 51 deletions(-) create mode 100644 packages/fiori/test/pages/ShellBarBranding.html diff --git a/packages/fiori/src/ShellBar.ts b/packages/fiori/src/ShellBar.ts index 3bf8a17ac653a..c6918c18cbbc6 100644 --- a/packages/fiori/src/ShellBar.ts +++ b/packages/fiori/src/ShellBar.ts @@ -651,11 +651,6 @@ class ShellBar extends UI5Element { if (!this.legacyAdaptor) { this.initLegacyController(); } - // Sync branding breakpoint state - this.branding.forEach(brandingEl => { - brandingEl._isSBreakPoint = this.isSBreakPoint; - }); - this.buildActions(); this.searchAdaptor?.syncShowSearchFieldState(); @@ -762,15 +757,78 @@ class ShellBar extends UI5Element { return; } + const brandingEl = this.branding[0]; + + // Measure before the loop squeezes the branding. If the title's natural single-line + // width exceeds the logo width, stacking is visually meaningful (title will wrap). + // Temporarily remove _title-hidden so scrollWidth is readable even if it was hidden + // by the previous overflow pass. + const brandingTitleWouldWrap = (() => { + if (!brandingEl) { + return false; + } + const titleEl = brandingEl.shadowRoot?.querySelector(".ui5-shellbar-title"); + const logoEl = brandingEl.shadowRoot?.querySelector(".ui5-shellbar-logo"); + if (!titleEl || !logoEl) { + return false; + } + const wasHidden = brandingEl.hasAttribute("_title-hidden"); + if (wasHidden) { + brandingEl.removeAttribute("_title-hidden"); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + brandingEl.offsetWidth; + } + const result = titleEl.scrollWidth > logoEl.offsetWidth * 2; + if (wasHidden) { + brandingEl.setAttribute("_title-hidden", ""); + } + return result; + })(); + const result = this.overflow.updateOverflow({ actions: this.actions, content: this.sortContent(this.content), customItems: this._validItems, hiddenItemsIds: this.hiddenItemsIds, showSearchField: this.enabledFeatures.search && this.showSearchField, + hasBranding: this.enabledFeatures.branding && !!brandingEl, overflowOuter: this.overflowOuter!, overflowInner: this.overflowInner!, setVisible: (selector: string, visible: boolean) => { + if (selector === "[data-ui5-stable='branding-stacked']") { + if (brandingEl && !visible) { + // Only stack if the title is wider than the logo — meaning it will + // actually wrap in the stacked (column) layout. Short titles go to overflow. + if (brandingTitleWouldWrap) { + brandingEl.toggleAttribute("_stacked", true); + // Force synchronous layout flush so the next isOverflowing() call + // reads the post-stack offsetWidth, not the stale pre-stack value. + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + brandingEl.offsetWidth; + } + } else if (brandingEl && visible) { + brandingEl.removeAttribute("_stacked"); + } + return; + } + if (selector === "[data-ui5-stable='branding-identifier']") { + if (brandingEl) { + brandingEl.toggleAttribute("_measure-title-hidden", !visible); + if (visible) { + brandingEl.removeAttribute("_title-hidden"); + } + } + return; + } + if (selector === "[data-ui5-stable='branding-logo']") { + if (brandingEl) { + brandingEl.toggleAttribute("_measure-logo-hidden", !visible); + if (visible) { + brandingEl.removeAttribute("_logo-hidden"); + } + } + return; + } const element = this.shadowRoot!.querySelector(selector); if (element) { element.classList[visible ? "remove" : "add"]("ui5-shellbar-hidden"); @@ -780,6 +838,13 @@ class ShellBar extends UI5Element { this.handleUpdateOverflowResult(result); + // Sync overflow button CSS class immediately after measurement — the measurement loop + // manipulates it directly via setVisible, and the template won't fix it until next render. + const overflowBtn = this.shadowRoot!.querySelector(ShellBarActionsSelectors.Overflow); + if (overflowBtn) { + overflowBtn.classList.toggle("ui5-shellbar-hidden", !result.showOverflowButton); + } + return result.hiddenItemsIds; } @@ -795,11 +860,20 @@ class ShellBar extends UI5Element { } }); + // Sync branding hidden state + const brandingEl = this.branding[0]; + if (brandingEl) { + brandingEl.removeAttribute("_measure-title-hidden"); + brandingEl.removeAttribute("_measure-logo-hidden"); + brandingEl.toggleAttribute("_title-hidden", hiddenItemsIds.includes("branding-identifier")); + brandingEl.toggleAttribute("_logo-hidden", hiddenItemsIds.includes("branding-logo")); + } + if (!arraysAreEqual(this.hiddenItemsIds, hiddenItemsIds)) { this.handleContentVisibilityChanged(this.hiddenItemsIds, hiddenItemsIds); this.hiddenItemsIds = hiddenItemsIds; - this.showOverflowButton = showOverflowButton; } + this.showOverflowButton = showOverflowButton; this.showFullWidthSearch = this.searchAdaptor?.shouldShowFullScreen() || false; } @@ -843,6 +917,14 @@ class ShellBar extends UI5Element { this.overflowPopoverOpen = false; } + handleBrandingOverflowClick() { + const brandingEl = this.branding[0]; + if (brandingEl) { + brandingEl._fireClick(); + } + this.overflowPopoverOpen = false; + } + handleOverflowItemClick(e: MouseEvent) { const target = e.target as HTMLElement; const actionId = target.getAttribute("data-action-id"); @@ -865,9 +947,41 @@ class ShellBar extends UI5Element { actions: this.actions, customItems: this._validItems, hiddenItemsIds: this.hiddenItemsIds, + hasBranding: this.enabledFeatures.branding && !!this.branding[0], }); } + get brandingOverflowTitle(): string | undefined { + return this.branding[0]?.textContent?.trim() || undefined; + } + + get brandingOverflowLabel(): string | undefined { + const title = this.brandingOverflowTitle; + if (title) { + return `${title} Home`; + } + const accessibleName = this.branding[0]?.accessibleName?.trim(); + return accessibleName ? `${accessibleName} Home` : undefined; + } + + get brandingOverflowLogoSrc(): string | undefined { + const brandingEl = this.branding[0]; + if (!brandingEl) { + return undefined; + } + const img = brandingEl.querySelector("[slot='logo']"); + return img?.src || undefined; + } + + get brandingOverflowLogoAlt(): string | undefined { + const brandingEl = this.branding[0]; + if (!brandingEl) { + return undefined; + } + const img = brandingEl.querySelector("[slot='logo']"); + return img?.alt || undefined; + } + /** * Only entries that are actually `ui5-shellbar-item` instances participate in the * overflow calculation and template rendering. The default slot's type is @@ -885,7 +999,8 @@ class ShellBar extends UI5Element { * Shows count if only one item with count is overflowed, otherwise shows attention dot. */ get overflowBadge(): string | undefined { - const itemsWithCount = this.overflowItems.filter(item => item.data.count); + const countableItems = this.overflowItems.filter(item => item.type !== "branding"); + const itemsWithCount = countableItems.filter(item => item.data.count !== undefined); if (itemsWithCount.length === 1) { return itemsWithCount[0].data.count; } diff --git a/packages/fiori/src/ShellBarBranding.ts b/packages/fiori/src/ShellBarBranding.ts index 7ada92b4581fe..0a6afe07e59ad 100644 --- a/packages/fiori/src/ShellBarBranding.ts +++ b/packages/fiori/src/ShellBarBranding.ts @@ -88,14 +88,6 @@ class ShellBarBranding extends UI5Element { @property() accessibleName?: string; - /** - * Defines if the title of the branding is shown on an S breakpoint. - * @default false - * @private - */ - @property({ type: Boolean }) - _isSBreakPoint = false; - /** * Defines the title for the ui5-shellbar-branding component. * diff --git a/packages/fiori/src/ShellBarBrandingTemplate.tsx b/packages/fiori/src/ShellBarBrandingTemplate.tsx index df718a28069cd..27b777861693b 100644 --- a/packages/fiori/src/ShellBarBrandingTemplate.tsx +++ b/packages/fiori/src/ShellBarBrandingTemplate.tsx @@ -17,11 +17,9 @@ export default function ShellBarBrandingTemplate(this: ShellBarBranding) { - {!this._isSBreakPoint && ( - - - - )} + + + ); } diff --git a/packages/fiori/src/ShellBarTemplate.tsx b/packages/fiori/src/ShellBarTemplate.tsx index 0e489b6853330..1f1f35ac79a94 100644 --- a/packages/fiori/src/ShellBarTemplate.tsx +++ b/packages/fiori/src/ShellBarTemplate.tsx @@ -46,18 +46,19 @@ export default function ShellBarTemplate(this: ShellBar) { )} - {this.enabledFeatures.branding && ( -
- -
- )} - {/* Legacy branding (logo + primaryTitle) when no menu items */} {!this.enabledFeatures.branding && ShellBarLegacyBrandingArea.call(this)}
+ {/* Branding — first in DOM = visually leftmost */} + {this.enabledFeatures.branding && ( +
+ +
+ )} + {this.enabledFeatures.content && (
- + {this.overflowItems.map(item => { + if (item.type === "branding") { + return ( + <> + + + + ); + } if (item.type === "action") { const actionData = item.data; return ( diff --git a/packages/fiori/src/shellbar/ShellBarOverflow.ts b/packages/fiori/src/shellbar/ShellBarOverflow.ts index aa232fcf5c220..50ff712335fa0 100644 --- a/packages/fiori/src/shellbar/ShellBarOverflow.ts +++ b/packages/fiori/src/shellbar/ShellBarOverflow.ts @@ -8,6 +8,7 @@ interface ShellBarHidableItem { hideOrder: number; // Priority for hiding - later adjusted based on search field state keepHidden: boolean; // Keep item hidden to prevent flickering when searchfield expands/collapses showInOverflow?: boolean; // If true, hiding this item triggers overflow button + neverHide?: boolean; // Never actually hide in DOM, but still report in hiddenItemsIds for events } interface ShellBarOverflowParams { @@ -18,6 +19,7 @@ interface ShellBarOverflowParams { overflowInner: HTMLElement; hiddenItemsIds: readonly string[]; showSearchField: boolean; + hasBranding: boolean; setVisible: (selector: string, visible: boolean) => void; } @@ -36,21 +38,45 @@ type ShellBarOverflowItem = { id: string; data: ShellBarItem; order: number; +} | { + type: "branding"; + id: string; + order: number; + logoHidden: boolean; + identifierHidden: boolean; } +const BrandingIds = { + Stacked: "branding-stacked", + Identifier: "branding-identifier", + Logo: "branding-logo", +} as const; + +const BrandingSelectors = { + Stacked: "[data-ui5-stable='branding-stacked']", + Identifier: "[data-ui5-stable='branding-identifier']", + Logo: "[data-ui5-stable='branding-logo']", +} as const; + class ShellBarOverflow { private readonly CLOSED_SEARCH_STRATEGY = { ACTIONS: 0, // All actions hide first CONTENT: 1000, // Then content (except last) SEARCH: 2000, // Then search button LAST_CONTENT: 3000, // Last content item hides last + BRANDING_STACKED: 3500, // Branding stacks (title below logo) before identifier hides + BRANDING_IDENTIFIER: 4000, // Branding identifier hides after all actions + BRANDING_LOGO: 5000, // Logo hides last }; private readonly OPEN_SEARCH_STRATEGY = { - CONTENT: 0, // All content hide first - ACTIONS: 1000, // All actions next + ACTIONS: 0, // Actions hide first (same as closed — spec says actions before content) + CONTENT: 1000, // Then content SEARCH: 2000, // Then search button - LAST_CONTENT: 0, // Last content same as other content + LAST_CONTENT: 1000, // Last content same as other content + BRANDING_STACKED: 3500, + BRANDING_IDENTIFIER: 4000, + BRANDING_LOGO: 5000, }; updateOverflow(params: ShellBarOverflowParams): ShellBarOverflowResult { @@ -72,7 +98,7 @@ class ShellBarOverflow { }); let nextItemToHide = null; - let showOverflowButton = false; + let overflowItemCount = 0; const hiddenItemsIds: string[] = []; // Iteratively hide items until no overflow @@ -83,16 +109,21 @@ class ShellBarOverflow { break; // No more overflow, stop hiding } - setVisible(nextItemToHide.selector, false); + if (!nextItemToHide.neverHide) { + setVisible(nextItemToHide.selector, false); + } hiddenItemsIds.push(nextItemToHide.id); if (nextItemToHide.showInOverflow) { - // show overflow button to account in isOverflowing calculation + overflowItemCount++; + // Always show the overflow button in DOM during measurement to account for its width. + // It will only be rendered visibly if overflowItemCount > 1 (enforced via showOverflowButton). setVisible(ShellBarActionsSelectors.Overflow, true); - showOverflowButton = true; } } + const showOverflowButton = overflowItemCount > 0; + return { hiddenItemsIds, showOverflowButton, @@ -111,6 +142,7 @@ class ShellBarOverflow { const items: ShellBarHidableItem[] = [ ...this.buildContent(params), ...this.buildActions(params), + ...this.buildBranding(params), ]; // sort by hideOrder first then by keepHidden keepHidden items are at the start @@ -125,6 +157,39 @@ class ShellBarOverflow { }); } + private buildBranding(params: ShellBarOverflowParams): readonly ShellBarHidableItem[] { + if (!params.hasBranding) { + return []; + } + + const strategy = this.getOverflowStrategy(params.showSearchField); + const { hiddenItemsIds } = params; + + return [ + { + id: BrandingIds.Stacked, + selector: BrandingSelectors.Stacked, + hideOrder: strategy.BRANDING_STACKED, + keepHidden: hiddenItemsIds.includes(BrandingIds.Stacked), + showInOverflow: false, + }, + { + id: BrandingIds.Identifier, + selector: BrandingSelectors.Identifier, + hideOrder: strategy.BRANDING_IDENTIFIER, + keepHidden: hiddenItemsIds.includes(BrandingIds.Identifier), + showInOverflow: true, + }, + { + id: BrandingIds.Logo, + selector: BrandingSelectors.Logo, + hideOrder: strategy.BRANDING_LOGO, + keepHidden: hiddenItemsIds.includes(BrandingIds.Logo), + showInOverflow: true, + }, + ]; + } + private buildContent(params: ShellBarOverflowParams): readonly ShellBarHidableItem[] { const { content, showSearchField, @@ -136,7 +201,11 @@ class ShellBarOverflow { // Build content items content.forEach((item, index) => { const slotName = (item as any)._individualSlot as string; - const dataHideOrder = parseInt(item.getAttribute("data-hide-order") || String(index)); + const isNeverHide = item.hasAttribute("data-never-hide"); + const hasExplicitOrder = item.hasAttribute("data-hide-order"); + // Default: last added hides first (higher index = lower hide order number = hides earlier) + const defaultOrder = content.length - index; + const dataHideOrder = hasExplicitOrder ? parseInt(item.getAttribute("data-hide-order")!) : defaultOrder; const isLast = index === content.length - 1; const priority = isLast ? overflowStrategy.LAST_CONTENT : overflowStrategy.CONTENT; @@ -145,8 +214,9 @@ class ShellBarOverflow { id: slotName, selector: `#${slotName}`, hideOrder: priority + dataHideOrder, - keepHidden: false, // Content items don't cause flickering + keepHidden: false, showInOverflow: false, + neverHide: isNeverHide || undefined, }); }); @@ -202,10 +272,22 @@ class ShellBarOverflow { actions: readonly ShellBarActionItem[]; customItems: readonly ShellBarItem[]; hiddenItemsIds: readonly string[]; + hasBranding: boolean; }): ReadonlyArray { - const { actions, customItems, hiddenItemsIds } = params; + const { actions, customItems, hiddenItemsIds, hasBranding } = params; const result: ShellBarOverflowItem[] = []; + // Branding goes first when identifier or logo is hidden + if (hasBranding && (hiddenItemsIds.includes(BrandingIds.Identifier) || hiddenItemsIds.includes(BrandingIds.Logo))) { + result.push({ + type: "branding", + id: "branding", + order: -1, + logoHidden: hiddenItemsIds.includes(BrandingIds.Logo), + identifierHidden: hiddenItemsIds.includes(BrandingIds.Identifier), + }); + } + // Add hidden custom items const hiddenCustomItems = customItems.filter((item: ShellBarItem) => hiddenItemsIds.includes(item._id)); hiddenCustomItems.forEach((item: ShellBarItem, index: number) => { @@ -216,8 +298,8 @@ class ShellBarOverflow { const actionOrder: Record = { [ShellBarActions.Search]: 0, - [ShellBarActions.Notifications]: 1, - [ShellBarActions.Assistant]: 2, + [ShellBarActions.Assistant]: 1, + [ShellBarActions.Notifications]: 2, }; const hiddenActions = actions.filter(action => hiddenItemsIds.includes(action.id)); diff --git a/packages/fiori/src/themes/ShellBar.css b/packages/fiori/src/themes/ShellBar.css index 86ae2ecbc361b..274aa39e41e4f 100644 --- a/packages/fiori/src/themes/ShellBar.css +++ b/packages/fiori/src/themes/ShellBar.css @@ -135,6 +135,8 @@ flex-shrink: 0; display: flex; align-items: center; + /* Push all subsequent items (actions) to the right */ + margin-inline-end: auto; } /* ============================================================================ @@ -156,7 +158,6 @@ .ui5-shellbar-overflow-container-inner { display: flex; align-items: center; - justify-content: end; flex-shrink: 0; min-width: 100%; } @@ -357,6 +358,50 @@ slot[name="profile"] { padding: 0 2rem; } +/* ============================================================================ + OVERFLOW BRANDING (branding shown at top of overflow popover) + ============================================================================ */ + +.ui5-shellbar-overflow-branding { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.25rem; + min-height: 2.5rem; + padding: 0.25rem 2rem; + box-sizing: border-box; + cursor: pointer; + overflow-x: auto; +} + +.ui5-shellbar-overflow-branding:hover { + background: var(--sapList_Hover_Background); +} + +.ui5-shellbar-overflow-branding:active { + background: var(--sapList_Active_Background); +} + +.ui5-shellbar-overflow-branding-logo { + max-height: 1.5625rem; + width: auto; + flex-shrink: 0; +} + +.ui5-shellbar-overflow-branding-title { + font-family: var(--sapFontSemiboldDuplexFamily); + font-size: var(--sapFontHeader6Size); + color: var(--sapList_TextColor); + white-space: normal; + word-break: break-word; +} + +.ui5-shellbar-overflow-branding-separator { + height: 1px; + background-color: var(--_ui5-shellbar_separator-color); + margin: 0 1rem; +} + /* ============================================================================ Utilities (Keep these at the end of the file to avoid specificity issues) ============================================================================ */ diff --git a/packages/fiori/src/themes/ShellBarBranding.css b/packages/fiori/src/themes/ShellBarBranding.css index b1962c15533c1..f7a342c8fa139 100644 --- a/packages/fiori/src/themes/ShellBarBranding.css +++ b/packages/fiori/src/themes/ShellBarBranding.css @@ -3,16 +3,17 @@ } :host(:not([hidden])) { - display: inline; + display: block; + min-width: 0; } .ui5-shellbar-branding-root { - overflow: hidden; display: flex; align-items: center; - height: 2.25rem; - padding-block: 0.125rem; - padding-inline: 0.25rem 0.5rem; + min-height: 2.5rem; + min-width: 0; + padding-block: 0.25rem; + padding-inline: 0.25rem 2rem; box-sizing: border-box; cursor: pointer; background: var(--sapButton_Lite_Background); @@ -23,6 +24,12 @@ margin-inline-end: 0.5rem; } +.ui5-shellbar-branding-root--stacked, +:host([_stacked]) .ui5-shellbar-branding-root { + flex-direction: column; + align-items: flex-start; +} + .ui5-shellbar-branding-root:focus { outline: var(--_ui5_shellbar_logo_outline); outline-offset: calc(-1 * var(--sapContent_FocusWidth)); @@ -41,15 +48,24 @@ } .ui5-shellbar-title { - display: inline-block; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; font-family: var(--sapFontSemiboldDuplexFamily); margin: 0; - font-size: var(--_ui5_shellbar_menu_button_title_font_size); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + font-size: var(--sapFontHeader6Size); color: var(--sapShell_SubBrand_TextColor); margin-inline-start: 0.25rem; + text-align: start; +} + +.ui5-shellbar-branding-root--stacked .ui5-shellbar-title, +:host([_stacked]) .ui5-shellbar-title { + display: block; + overflow: visible; + -webkit-line-clamp: unset; + margin-inline-start: 0; } .ui5-shellbar-logo-area { @@ -67,7 +83,7 @@ } ::slotted([slot="logo"]) { - max-height: 1.875rem; + max-height: 1.5625rem; width: auto; padding-block: 0.0625rem; padding-inline: 0.25rem; @@ -76,4 +92,22 @@ ::slotted([slot="logo"]):active { pointer-events: none; +} + +/* Measurement-only visibility — set via setAttribute, no reactive property, no re-render */ +:host([_measure-title-hidden]) .ui5-shellbar-title { + display: none !important; +} + +:host([_measure-logo-hidden]) .ui5-shellbar-logo { + display: none !important; +} + +/* Final hidden state — also non-reactive, driven by toggleAttribute from ShellBar */ +:host([_title-hidden]) .ui5-shellbar-title { + display: none !important; +} + +:host([_logo-hidden]) .ui5-shellbar-logo { + display: none !important; } \ No newline at end of file diff --git a/packages/fiori/test/pages/ShellBarBranding.html b/packages/fiori/test/pages/ShellBarBranding.html new file mode 100644 index 0000000000000..705ca106b7cd1 --- /dev/null +++ b/packages/fiori/test/pages/ShellBarBranding.html @@ -0,0 +1,357 @@ + + + + + ShellBar — Spec Scenarios + + + + + + + + +
+ Custom Branding — What changed + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AreaBeforeAfter
S breakpoint hidingProduct identifier hidden at S (<600px)Identifier stays visible as long as possible — only hides after all actions have overflowed
Stacked modeDid not existNEW — when title needs more than 2 lines, it moves below the logo automatically. Triggered by the overflow loop, not a breakpoint.
Overflow orderBranding hidden at S breakpoint (fixed)Stack → identifier overflow → logo overflow (logo is always last)
Branding in overflow popoverDid not appear in overflow menuNEW — appears at top of overflow popover with separator, shows only the hidden parts (identifier and/or logo)
Title wrappingwhite-space: nowrap + ellipsis — title always on one line-webkit-line-clamp: 2 — title can wrap up to 2 lines before going stacked
Content area priorityNo control over hide orderNEW — data-hide-order attribute sets explicit priority; data-never-hide keeps an item always visible
Branding DOM positionOutside the overflow container — not measured by overflow loopMoved inside the overflow container so the overflow loop can measure and hide it progressively
Actions alignmentjustify-content: end on inner containermargin-inline-end: auto on branding — pushes actions right while keeping branding naturally left-aligned
Removed_isSBreakPoint property — parent ShellBar set this on the branding element to hide the title at SRemoved — replaced by the overflow loop driving stacked/hidden state via non-reactive attributes
+
+ + +
+ 1 · Basic branding — no actions +

Logo and product identifier visible. No overflow button.

+ + + Product Title + Logo + + +
+ + +
+ 2 · Branding + shell actions — resize to overflow +

+ Resize narrow: shell actions go to overflow first (rightmost first: Help → Settings → Notifications). + Branding stays visible until all actions are overflowed. Overflow button appears. +

+ + + Product Identifier + Logo + + + + + +
+ + +
+ 3 · Long product identifier — stacking + overflow +

+ Resize narrow: identifier wraps to 2 lines (stacks below logo). Further → identifier moves to overflow. + Logo is last to be hidden. +

+ + + A very Long Brand Name that needs wrapping + Logo + + + + + +
+ + +
+ 4 · Content area — default priority (last added, first hidden) +

+ Resize narrow: "Item C" (last added) hides first, then "Item B", then "Item A". + Separators visible on M/L/XL, hidden on S (<600px). +

+ + + Product Identifier + Logo + + Item A + Item B + Item C + + +
+ + +
+ 5 · Content area — explicit priority (data-hide-order) +

+ Resize narrow: "Priority 1" hides first, then "Priority 2", "Priority 3", "Priority 4" last. +

+ + + Product Identifier + Logo + + Priority 4 (last hidden) + Priority 3 + Priority 2 + Priority 1 (first hidden) + + +
+ + +
+ 6 · Never hide content item +

+ Resize narrow: "Can Hide" hides first, then shell actions, then branding. + "Always Visible" (data-never-hide) stays in the bar at all times. +

+ + + Product Identifier + Logo + + Always Visible ★ + Can Hide + + + +
+ + +
+ 7 · Full cascade — content + actions + branding +

+ Resize narrow. Expected order: search collapses → Help overflows → Settings overflows → + Notifications overflows → "Item 1" hides → "Item 2*" (never-hide, stays) → + identifier wraps → identifier overflows → logo overflows. +

+ + + Product Identifier + Logo + + Item 2* (never hide) + Item 1 + + + + +
+ + +
+ 8 · Badge — single badge action in overflow +

+ Resize narrow until notifications overflows. Overflow button should show "72" badge counter. +

+ + + Product Identifier + Logo + + + + +
+ + +
+ 9 · Badge — multiple badge actions → attention dot +

+ Resize narrow until both notifications (72) and custom item (count="1") overflow. + Overflow button shows attention dot, not a specific count. + Also: notifications with count="0" still causes attention dot since it CAN show a badge. +

+ + + Product Identifier + Logo + + + + + +
+ + +
+ 10 · Overflow popover — branding entry at top +

+ Resize very narrow until logo overflows. Open overflow menu: branding entry (logo + identifier) + appears at top with separator below it, followed by shell actions. + Clicking branding entry fires its click event. +

+ + + A very Long Brand Name that needs wrapping to overflow + Logo + + + + + +
+ + +
+ 11 · Branding — identifier only (no logo) +

Only product identifier text, no logo. Overflow still works.

+ + + Product Title Without Logo + + + + +
+ + +
+ 12 · Profile + product switch — always visible +

+ Resize to any size. Profile avatar and product switch never go into overflow. +

+ + + Product Identifier + Logo + + + + + Avatar + + +
+ + + From c0f2a95a295589df2e1b3f76c6ae3e375ec1e65c Mon Sep 17 00:00:00 2001 From: Nikola Anachkov Date: Wed, 2 Sep 2026 11:01:36 +0300 Subject: [PATCH 2/2] fix: lint error --- packages/fiori/src/ShellBar.ts | 4 ++-- packages/fiori/src/shellbar/ShellBarOverflow.ts | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/fiori/src/ShellBar.ts b/packages/fiori/src/ShellBar.ts index c6918c18cbbc6..cd9a3ceba706f 100644 --- a/packages/fiori/src/ShellBar.ts +++ b/packages/fiori/src/ShellBar.ts @@ -775,7 +775,7 @@ class ShellBar extends UI5Element { const wasHidden = brandingEl.hasAttribute("_title-hidden"); if (wasHidden) { brandingEl.removeAttribute("_title-hidden"); - // eslint-disable-next-line @typescript-eslint/no-unused-expressions + // eslint-disable-next-line no-unused-expressions brandingEl.offsetWidth; } const result = titleEl.scrollWidth > logoEl.offsetWidth * 2; @@ -803,7 +803,7 @@ class ShellBar extends UI5Element { brandingEl.toggleAttribute("_stacked", true); // Force synchronous layout flush so the next isOverflowing() call // reads the post-stack offsetWidth, not the stale pre-stack value. - // eslint-disable-next-line @typescript-eslint/no-unused-expressions + // eslint-disable-next-line no-unused-expressions brandingEl.offsetWidth; } } else if (brandingEl && visible) { diff --git a/packages/fiori/src/shellbar/ShellBarOverflow.ts b/packages/fiori/src/shellbar/ShellBarOverflow.ts index 50ff712335fa0..2b2490ab48bd2 100644 --- a/packages/fiori/src/shellbar/ShellBarOverflow.ts +++ b/packages/fiori/src/shellbar/ShellBarOverflow.ts @@ -64,9 +64,9 @@ class ShellBarOverflow { CONTENT: 1000, // Then content (except last) SEARCH: 2000, // Then search button LAST_CONTENT: 3000, // Last content item hides last - BRANDING_STACKED: 3500, // Branding stacks (title below logo) before identifier hides + BRANDING_STACKED: 3500, // Branding stacks (title below logo) before identifier hides BRANDING_IDENTIFIER: 4000, // Branding identifier hides after all actions - BRANDING_LOGO: 5000, // Logo hides last + BRANDING_LOGO: 5000, // Logo hides last }; private readonly OPEN_SEARCH_STRATEGY = { @@ -274,7 +274,9 @@ class ShellBarOverflow { hiddenItemsIds: readonly string[]; hasBranding: boolean; }): ReadonlyArray { - const { actions, customItems, hiddenItemsIds, hasBranding } = params; + const { + actions, customItems, hiddenItemsIds, hasBranding, + } = params; const result: ShellBarOverflowItem[] = []; // Branding goes first when identifier or logo is hidden