From aa896c6e19c192d82ce1ff3a9a77998eae789555 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 29 Oct 2025 09:34:03 +0200 Subject: [PATCH 01/57] chore: apply old spacings to shellbar's branding --- .../fiori/src/themes/ShellBarBranding.css | 15 +- .../fiori/test/pages/ShellBar_evolution.html | 302 +++--------------- 2 files changed, 52 insertions(+), 265 deletions(-) diff --git a/packages/fiori/src/themes/ShellBarBranding.css b/packages/fiori/src/themes/ShellBarBranding.css index cad1990d5be7..0f4eef07ba8d 100644 --- a/packages/fiori/src/themes/ShellBarBranding.css +++ b/packages/fiori/src/themes/ShellBarBranding.css @@ -10,16 +10,18 @@ overflow: hidden; display: flex; align-items: center; - padding-block: 0.25rem; - padding-inline: 0.25rem 0.5rem; + box-sizing: border-box; cursor: pointer; background: var(--sapButton_Lite_Background); border: 1px solid var(--sapButton_Lite_BorderColor); color: var(--sapShell_TextColor); - /* fix cutting of the focus outline */ - margin-inline-start: 0.125rem; - margin-inline-end: .5rem; + margin-inline-end: .25rem; +} + +:host(:not([_is-sbreak-point])) .ui5-shellbar-branding-root { + padding-block: 0.25rem; + padding-inline: 0.25rem 0.5rem; } .ui5-shellbar-branding-root:focus { @@ -67,6 +69,9 @@ ::slotted([slot="logo"]) { max-height: 2rem; +} + +:host(:not([_is-sbreak-point])) ::slotted([slot="logo"]) { padding-inline: 0.25rem; } diff --git a/packages/fiori/test/pages/ShellBar_evolution.html b/packages/fiori/test/pages/ShellBar_evolution.html index d425a07b1f7d..dd7f69d77c21 100644 --- a/packages/fiori/test/pages/ShellBar_evolution.html +++ b/packages/fiori/test/pages/ShellBar_evolution.html @@ -22,194 +22,62 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + S/4HANA Cloud + + - - - S/4HANA Cloud - - + + - - + - EMEA - Deliveries overdue for billing neeed more text because of a bug + - +
+ New Version + +
-
- New Version - -
+ +
Instructions
+
- -
Instructions
-
+ + + +
- - - -
+ + + + + - - - - - - - - - - - - - - - - - - - - - - - + + - - - SAP Labs Bulgaria - - + - - PR10 + - PR 4 - PR3 +
+ New Version + +
- + +
Instructions
+
- + + + +
- PR2 - - PR1 - - PR6 - - PR7 - - PR8 - - PR9 - - - - - - - - - - - - - - - - - - - - - - -
- - - SAP Labs Bulgaria - - - - - PR10 - - PR 4 - PR3 - - - - - - PR2 - - PR1 - - PR6 - - PR7 - - PR8 - - PR9 - -
- - -
- - - - - - - - -
@@ -233,93 +101,7 @@
- From 6032d8b274add0e894b9543c5b53179c51bbc006 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 29 Oct 2025 09:53:07 +0200 Subject: [PATCH 02/57] chore: add demo sample --- .../fiori/test/pages/ShellBar_evolution.html | 115 ++++++++++++++---- 1 file changed, 90 insertions(+), 25 deletions(-) diff --git a/packages/fiori/test/pages/ShellBar_evolution.html b/packages/fiori/test/pages/ShellBar_evolution.html index dd7f69d77c21..d02f1d32264a 100644 --- a/packages/fiori/test/pages/ShellBar_evolution.html +++ b/packages/fiori/test/pages/ShellBar_evolution.html @@ -1,9 +1,10 @@ + Shell Bar - + + + + + + +

ShellBarV2 MVP Test

+

Testing modular architecture with Actions feature.

+ +

Full Features (with All Actions + Overflow)

+ + + + + My Application + + + Action 1 + Action 2 + Action 3 + Action 4 + Action 5 + + + + + +

Resize window to see overflow in action. Items hide based on data-hide-order. Profile and Product Switch never hide.

+

Keyboard navigation: Use Arrow Left/Right to navigate between items. Home/End to jump to first/last item.

+

Full-screen search: When overflow happens and search is visible, full-screen search overlay appears. Click Cancel to close.

+ + + + + \ No newline at end of file From 62f495df359a99321e27dd63dd14d840071409a0 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Mon, 3 Nov 2025 10:00:10 +0200 Subject: [PATCH 08/57] chore: remove eventbus and domadapter --- packages/fiori/src/ShellBarV2.ts | 35 ++++++------------ .../src/shellbarv2/ShellBarDomAdapter.ts | 35 ------------------ .../fiori/src/shellbarv2/ShellBarEventBus.ts | 37 ------------------- .../src/shellbarv2/ShellBarOverflowSupport.ts | 24 ++++-------- 4 files changed, 18 insertions(+), 113 deletions(-) delete mode 100644 packages/fiori/src/shellbarv2/ShellBarDomAdapter.ts delete mode 100644 packages/fiori/src/shellbarv2/ShellBarEventBus.ts diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index 6645b364064c..1a0318cdc650 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -24,8 +24,6 @@ import ShellBarV2Template from "./ShellBarV2Template.js"; import shellBarV2Styles from "./generated/themes/ShellBarV2.css.js"; import ShellBarV2Actions from "./shellbarv2/ShellBarActions.js"; -import ShellBarV2EventBus from "./shellbarv2/ShellBarEventBus.js"; -import ShellBarV2DomAdapter from "./shellbarv2/ShellBarDomAdapter.js"; import ShellBarV2Breakpoint from "./shellbarv2/ShellBarBreakpoint.js"; import ShellBarV2SearchSupport from "./shellbarv2/ShellBarSearchSupport.js"; import ShellBarV2ItemNavigation from "./shellbarv2/ShellBarItemNavigation.js"; @@ -317,16 +315,6 @@ class ShellBarV2 extends UI5Element { overflowInner?: HTMLElement; private handleResizeBound: ResizeObserverCallback = this.handleResize.bind(this); - private handleOverflowChangedBound = this.handleOverflowChanged.bind(this); - - eventBus = new ShellBarV2EventBus({ - host: this, - }); - - domAdapter = new ShellBarV2DomAdapter({ - host: this, - shadowRoot: this.shadowRoot!, - }); searchSupport = new ShellBarV2SearchSupport({ getSearchField: () => this.search, @@ -337,11 +325,10 @@ class ShellBarV2 extends UI5Element { }); overflowSupport = new ShellBarV2OverflowSupport({ - eventBus: this.eventBus, - domAdapter: this.domAdapter, getActions: () => this.actions.slice(), getContent: () => this.content.slice(), getCustomItems: () => this.items.slice(), + querySelector: (selector: string) => this.shadowRoot!.querySelector(selector), }); itemNavigation = new ShellBarV2ItemNavigation({ @@ -356,13 +343,11 @@ class ShellBarV2 extends UI5Element { onEnterDOM() { ResizeHandler.register(this, this.handleResizeBound); this.searchSupport.subscribe(); - this.eventBus.on("overflow-changed", this.handleOverflowChangedBound); } onExitDOM() { ResizeHandler.deregister(this, this.handleResizeBound); this.searchSupport.unsubscribe(); - this.eventBus.off("overflow-changed", this.handleOverflowChangedBound); } onBeforeRendering() { @@ -413,7 +398,7 @@ class ShellBarV2 extends UI5Element { * This is the coordination logic - gather data, delegate, apply result. */ private updateBreakpoint() { - const width = this.domAdapter.getWidth(); + const width = this.getBoundingClientRect().width; const breakpoint = this.breakpoint.calculate({ width }); if (this.breakpointSize !== breakpoint) { @@ -426,7 +411,7 @@ class ShellBarV2 extends UI5Element { /* ------------- Notifications Management -------------- */ _handleNotificationsClick() { - const notificationsBtn = this.domAdapter.querySelector @@ -58,22 +58,60 @@ export default function ShellBarV2Template(this: ShellBarV2) {
{this.hasContent && ( -
- {this.content.map(content => ( -
- -
- ))} +
+ {/* Start separator */} + {this.separatorConfig.showStartSeparator && ( +
+ )} + + {/* Start content items */} + {this.startContent.map(item => { + const packedSep = this.getPackedSeparatorInfo(item, true); + return ( +
+ {packedSep.shouldPack && ( +
+ )} + +
+ ); + })} + + {/* Spacer: Grows to fill available space, used to measure if space is tight */} +
+ + {/* End content items */} + {this.endContent.map(item => { + const packedSep = this.getPackedSeparatorInfo(item, false); + return ( +
+ + {packedSep.shouldPack && ( +
+ )} +
+ ); + })} + + {/* End separator */} + {this.separatorConfig.showEndSeparator && ( +
+ )}
)} - {/* Spacer: Grows to fill available space, used to measure if space is tight */} -
- {this.hasSearchField && ShellBarV2SearchField.call(this)}
@@ -83,7 +121,7 @@ export default function ShellBarV2Template(this: ShellBarV2) { class="ui5-shellbar-search-button" icon="search" design="Transparent" - onClick={this._handleSearchButtonClick} + onClick={this.handleSearchButtonClick} accessibleName="Search" /> )} @@ -127,7 +165,7 @@ export default function ShellBarV2Template(this: ShellBarV2) { class="ui5-shellbar-overflow-button" icon="overflow" design="Transparent" - onClick={this._handleOverflowClick} + onClick={this.handleOverflowClick} accessibleName="More" /> )} @@ -176,9 +214,9 @@ export default function ShellBarV2Template(this: ShellBarV2) { icon={item.data.icon ? `sap-icon://${item.data.icon}` : ""} data-action-id={item.id} type="Active" - onClick={this._handleOverflowItemClick} + onClick={this.handleOverflowItemClick} > - {this._getActionText(item.id)} + {this.getActionText(item.id)} ); } diff --git a/packages/fiori/src/shellbarv2/ARCHITECTURE_REVIEW.md b/packages/fiori/src/shellbarv2/ARCHITECTURE_REVIEW.md deleted file mode 100644 index 764057ed63e0..000000000000 --- a/packages/fiori/src/shellbarv2/ARCHITECTURE_REVIEW.md +++ /dev/null @@ -1,1307 +0,0 @@ -# ShellBarV2 Architecture Review - -## Executive Summary - -The ShellBarV2 architecture uses a coordinator pattern with focused support modules. While the separation of concerns is good, several design issues existed. - -### ✅ Completed Improvements (Phase 1) -- ✅ **Removed over-engineered abstractions** - Deleted EventBus and DomAdapter (-72 lines) -- ✅ **Fixed tight coupling** - Removed lambda closures, pass data explicitly to methods -- ✅ **Improved state ownership** - Reduced stored state in OverflowSupport -- ✅ **Cleaner API** - Replaced querySelector with focused setVisible callback -- ✅ **Better testability** - Modules now easily testable with mock data - -**Net Result**: -145 lines of code, cleaner architecture, better testability - -### ⚠️ Remaining Issues -- **Under-engineered complex logic** (overflow calculation, error handling) -- **Performance issues** (layout thrashing in overflow calculation) -- **Missing optimizations** (lazy initialization, memoization) -- **Magic numbers** (priority system not documented) -- **SearchSupport complexity** (does too much) - -## Critical Design Flaws - -### 1. ✅ DONE - Tight Coupling via Lambda Closures - -**Status**: Completed - All lambda closures removed from OverflowSupport - -**What Was Done**: -- Removed constructor parameters with lambda closures -- Pass all data explicitly to `updateOverflow()` method -- Made `getOverflowItems()` a method that receives data instead of a getter -- Replaced `querySelector` lambda with focused `setVisible` callback - -**Before**: -```typescript -overflowSupport = new ShellBarV2OverflowSupport({ - getActions: () => this.actions.slice(), - getContent: () => this.content.slice(), - getCustomItems: () => this.items.slice(), - querySelector: (selector: string) => this.shadowRoot!.querySelector(selector), -}); -``` - -**After**: -```typescript -overflowSupport = new ShellBarV2OverflowSupport(); - -// Data passed explicitly to methods -updateOverflow({ - items: this.items, - actions: this.actions, - content: this.content, - showSearchField: this.showSearchField, - overflowOuter: this.overflowOuter!, - overflowInner: this.overflowInner!, - setVisible: (selector: string, visible: boolean) => { /* ... */ }, -}); - -getOverflowItems({ - actions: this.actions, - items: this.items, -}); -``` - -**Benefits Achieved**: -- ✅ Explicit dependencies -- ✅ Easy to test with mock data -- ✅ Clear what module needs -- ✅ No stored state in OverflowSupport -- ✅ Cleaner API with focused callbacks - -### 2. ✅ DONE - Circular Reference Risk - -**Status**: Completed - EventBus removed entirely - -**What Was Done**: -- Deleted `ShellBarEventBus.ts` file completely -- OverflowSupport now returns results directly instead of emitting events -- Component calls handler directly with returned result -- No more circular references - -**Before**: -```typescript -eventBus = new ShellBarV2EventBus({ - host: this, // Circular reference -}); - -// In OverflowSupport -this.eventBus.emit("overflow-changed", { hiddenItems, showOverflowButton }); - -// In ShellBarV2 -this.eventBus.on("overflow-changed", this.handleOverflowChangedBound); -``` - -**After**: -```typescript -// No EventBus needed - -// In OverflowSupport - return result -return { hiddenItems, showOverflowButton }; - -// In ShellBarV2 - call handler directly -const result = this.overflowSupport.updateOverflow({...}); -this.handleOverflowChanged(result); -``` - -**Benefits Achieved**: -- ✅ No circular references -- ✅ Less code to maintain (-37 lines) -- ✅ Clearer data flow -- ✅ Simpler architecture - -### 3. ✅ DONE - Unnecessary Abstractions - -**Status**: Completed - Both EventBus and DomAdapter removed - -**What Was Done**: -- Deleted `ShellBarDomAdapter.ts` file completely -- Deleted `ShellBarEventBus.ts` file completely -- Replaced DomAdapter with direct DOM calls -- Replaced querySelector with focused `setVisible` callback -- Total: **-72 lines** removed from unnecessary abstractions - -**Before**: -```typescript -// Two files with 15-line wrappers -class ShellBarV2DomAdapter { - getWidth(): number { - return this.host.getBoundingClientRect().width; - } - querySelector(selector: string): T | null { - return this.shadowRoot.querySelector(selector); - } -} - -domAdapter = new ShellBarV2DomAdapter({ host: this, shadowRoot: this.shadowRoot! }); -const width = this.domAdapter.getWidth(); -const element = this.domAdapter.querySelector(".selector"); -``` - -**After**: -```typescript -// Direct calls - no abstraction needed -const width = this.getBoundingClientRect().width; - -// Focused callback instead of generic querySelector -setVisible: (selector: string, visible: boolean) => { - const element = this.shadowRoot!.querySelector(selector); - if (element) { - if (visible) { - element.classList.remove("ui5-shellbar-hidden"); - } else { - element.classList.add("ui5-shellbar-hidden"); - } - } -} -``` - -**Benefits Achieved**: -- ✅ Less code (-72 lines) -- ✅ Clearer intent -- ✅ Two fewer files to maintain -- ✅ Focused, single-purpose callbacks -- ✅ No unnecessary indirection - -### 4. ⚠️ PARTIALLY DONE - State Ownership Unclear - -**Status**: Improved but not fully resolved - -**What Was Done**: -- OverflowSupport no longer stores actions/items collections -- Data passed explicitly on each method call -- Reduced state to just `hiddenItems` tracking - -**Still Remaining**: -- OverflowSupport still stores `hiddenItems` array -- Component also tracks overflow state in `item.inOverflow` -- Some duplication remains - -**Before**: -```typescript -// In OverflowSupport - stored multiple collections -private hiddenItems: string[] = []; -private actions: readonly ShellBarV2ActionItem[] = []; -private items: readonly ShellBarV2Item[] = []; -``` - -**After**: -```typescript -// In OverflowSupport - only tracking hiddenItems -private hiddenItems: string[] = []; - -// Actions/items passed on each call -updateOverflow({ actions, items, ... }) -getOverflowItems({ actions, items }) -``` - -**Benefits Achieved**: -- ✅ Removed stored collections (actions, items) -- ✅ Data passed explicitly to methods -- ⚠️ Still stores hiddenItems for state tracking - -**Next Steps**: -- Could move hiddenItems to component entirely -- OverflowSupport becomes fully stateless - -### 5. SearchSupport Does Too Much - -**Location**: `ShellBarSearchSupport.ts` (entire file) - -**Responsibilities**: -1. Event subscription (subscribe/unsubscribe) -2. Event handling (onSearch, onSearchOpen, onSearchClose) -3. State synchronization (syncCollapsedState) -4. Auto-management logic (autoManageSearchState) -5. Full-screen detection (shouldShowFullScreen) -6. CSS variable parsing (getSearchFieldWidth) - -**Problem**: -- Violates Single Responsibility Principle -- Hard to test (too many concerns) -- Hard to understand (what is this module for?) -- Hard to change (ripple effects) - -**Impact**: High - maintainability - -**Recommended Fix**: -```typescript -// Split into focused modules -class SearchEventSubscriber { - subscribe(field, handlers) { /* ... */ } - unsubscribe(field, handlers) { /* ... */ } -} - -class SearchAutoManager { - shouldCollapse(hiddenItems, space, hasValue, hasFocus): boolean { - return hiddenItems > 0 && !hasValue && !hasFocus; - } - - shouldExpand(availableSpace, requiredSpace): boolean { - return availableSpace > requiredSpace; - } -} - -// Or inline simple logic in main component -``` - -**Benefits**: -- Each class has one reason to change -- Easy to test individual behaviors -- Clear purpose - -### 6. Magic Numbers and Strings - -**Location**: `ShellBarOverflowSupport.ts` lines 136, 151, 169 - -```typescript -const hideOrder = 10 + dataHideOrder; // Content items -let hideOrder = 0 + (params.showSearchField ? 100 : 0); // Actions -hideOrder: 999, // Protected items - -// Magic CSS classes -element.classList.add("ui5-shellbar-hidden"); -``` - -**Problem**: -- No explanation for numbers -- Hard to understand priority system -- Hard to modify (where else is 100 used?) -- Typo-prone string literals - -**Impact**: Medium - readability and maintainability - -**Recommended Fix**: -```typescript -const HIDE_ORDER = { - // Content items hide first - CONTENT_BASE: 10, - - // Action items hide second - ACTION_BASE: 100, - - // Protected items never hide - PROTECTED_BASE: 900, -} as const; - -const CSS_CLASSES = { - HIDDEN: "ui5-shellbar-hidden", -} as const; - -// Usage -const hideOrder = HIDE_ORDER.CONTENT_BASE + dataHideOrder; -element.classList.add(CSS_CLASSES.HIDDEN); -``` - -**Benefits**: -- Self-documenting -- Easy to change -- No typos -- Centralized configuration - -### 7. Imperative Overflow Algorithm with Layout Thrashing - -**Location**: `ShellBarOverflowSupport.ts` lines 79-101 - -```typescript -sortedItems.forEach(item => { - if (item.protected) { - return; - } - - // Check if still overflowing - FORCES REFLOW! - if (!this.isOverflowing(overflowOuter, overflowInner)) { - return; - } - - // Hide item - FORCES REFLOW! - element.classList.add("ui5-shellbar-hidden"); - hiddenItems.push(item.id); -}); -``` - -**Problem**: -- Reads layout (getBoundingClientRect) after every write (classList.add) -- Forces browser reflow for each item -- O(n) reflows where n = number of items -- Performance degrades with many items -- Imperative style hard to reason about - -**Impact**: High - performance - -**Recommended Fix**: -```typescript -// Pure function that calculates what to hide -function calculateHiddenItems( - items: OverflowItem[], - containerWidth: number, -): string[] { - const hidden: string[] = []; - let accumulatedWidth = 0; - - // Calculate widths upfront (batch read) - const itemWidths = items.map(item => ({ - id: item.id, - width: getElementWidth(item.selector), - protected: item.protected, - order: item.hideOrder, - })); - - // Sort by hide order - itemWidths.sort((a, b) => a.order - b.order); - - // Calculate visible width - const visibleWidth = itemWidths - .filter(item => !item.protected) - .reduce((sum, item) => sum + item.width, 0); - - if (visibleWidth <= containerWidth) { - return []; // Nothing to hide - } - - // Calculate what to hide - let remainingWidth = visibleWidth; - for (const item of itemWidths) { - if (item.protected) continue; - - if (remainingWidth > containerWidth) { - hidden.push(item.id); - remainingWidth -= item.width; - } - } - - return hidden; -} - -// Apply changes in one pass (batch write) -function applyHiddenItems(hidden: string[], domAdapter: DomAdapter) { - hidden.forEach(id => { - const element = domAdapter.querySelector(`[data-id="${id}"]`); - if (element) { - element.classList.add(CSS_CLASSES.HIDDEN); - } - }); -} - -// Usage -const hidden = calculateHiddenItems(items, containerWidth); -applyHiddenItems(hidden, domAdapter); -``` - -**Benefits**: -- Single reflow instead of n reflows -- Pure calculation (testable without DOM) -- Better performance -- Declarative style (easier to understand) - -**Performance Impact**: With 10 items, this is 10x faster (1 reflow vs 10). - -### 8. No Error Handling - -**Location**: `ShellBarOverflowSupport.ts` lines 91-94 - -```typescript -const element = this.domAdapter.querySelector(item.selector); -if (element) { - element.classList.add("ui5-shellbar-hidden"); -} -// What if element is null? Silent failure. -``` - -**Problem**: -- Silent failures hide bugs -- No logging or warnings -- Hard to debug when things break -- No fallback behavior - -**Impact**: Medium - debugging and reliability - -**Recommended Fix**: -```typescript -const element = this.domAdapter.querySelector(item.selector); -if (!element) { - console.warn( - `ShellBarV2: Could not find element for overflow item "${item.id}" with selector "${item.selector}"` - ); - return; -} -element.classList.add(CSS_CLASSES.HIDDEN); -``` - -**Or use assertions in development**: -```typescript -if (!element) { - if (process.env.NODE_ENV === "development") { - throw new Error( - `Missing overflow item: "${item.id}" (${item.selector})` - ); - } - console.warn(/* ... */); - return; -} -``` - -**Benefits**: -- Failures visible during development -- Easier debugging -- Graceful degradation in production - -### 9. No Lazy Initialization - -**Location**: `ShellBarV2.ts` lines 322-352 - -```typescript -// All modules created immediately -eventBus = new ShellBarV2EventBus({...}); -domAdapter = new ShellBarV2DomAdapter({...}); -searchSupport = new ShellBarV2SearchSupport({...}); -overflowSupport = new ShellBarV2OverflowSupport({...}); -itemNavigation = new ShellBarV2ItemNavigation({...}); -breakpoint = new ShellBarV2Breakpoint(); -actionsSupport = new ShellBarV2Actions(); -``` - -**Problem**: -- All modules created even if never used -- Search support created even without search field -- Wastes memory -- Wastes initialization time - -**Impact**: Low - minor optimization - -**Recommended Fix**: -```typescript -private _searchSupport?: ShellBarV2SearchSupport; -get searchSupport() { - if (!this._searchSupport && this.hasSearchField) { - this._searchSupport = new ShellBarV2SearchSupport({ - getSearchState: () => this.showSearchField, - setSearchState: (expanded) => this.setSearchState(expanded), - getSearchField: () => this.search, - getCSSVariable: (cssVar) => this.getCSSVariable(cssVar), - getOverflowed: () => this.overflowSupport.isOverflowing( - this.overflowOuter!, - this.overflowInner! - ), - }); - } - return this._searchSupport; -} - -onEnterDOM() { - ResizeHandler.register(this, this.handleResizeBound); - - // Only subscribe if search exists - if (this.hasSearchField) { - this.searchSupport?.subscribe(); - } - - this.eventBus.on("overflow-changed", this.handleOverflowChangedBound); -} -``` - -**Benefits**: -- Only create what's needed -- Faster initialization -- Lower memory footprint - -### 10. Priority Flip Hack - -**Location**: `ShellBarOverflowSupport.ts` lines 153-156 - -```typescript -let hideOrder = 0 + (params.showSearchField ? 100 : 0); - -if (this.isCurrentlyHidden(selector)) { - // flip priority to ensure currently hidden items remain hidden - hideOrder *= -1; // WTF? Negative numbers for priority? -} -``` - -**Problem**: -- Using negative numbers to flip priority -- Confusing and non-obvious -- Fragile (what if we need to flip again?) -- Hard to understand intent - -**Impact**: Medium - maintainability - -**Recommended Fix**: -```typescript -interface OverflowItem { - id: string; - hideOrder: number; - protected: boolean; - selector: string; - currentlyHidden: boolean; // Explicit flag -} - -// Sort with explicit logic -items.sort((a, b) => { - // Hidden items stay hidden (priority boost) - if (a.currentlyHidden !== b.currentlyHidden) { - return a.currentlyHidden ? -1 : 1; - } - - // Protected items last - if (a.protected !== b.protected) { - return a.protected ? 1 : -1; - } - - // Normal priority - return a.hideOrder - b.hideOrder; -}); -``` - -**Benefits**: -- Clear intent -- Explicit sorting logic -- Easy to modify -- Self-documenting - -## Design Pattern Improvements - -### Pattern 1: Replace Coordinator with Facade - -**Current**: ShellBarV2 directly coordinates all modules. - -**Problem**: Component class becomes large and hard to test. - -**Recommended**: Extract coordination logic to separate controller. - -```typescript -class ShellBarV2Controller { - constructor( - private component: ShellBarV2, - private breakpoint: ShellBarBreakpoint, - private overflow: ShellBarOverflow, - private search: ShellBarSearch, - ) {} - - handleResize() { - this.updateBreakpoint(); - const overflowResult = this.updateOverflow(); - this.updateSearch(overflowResult); - } - - private updateBreakpoint() { - const width = this.component.getBoundingClientRect().width; - const bp = this.breakpoint.calculate({ width }); - if (this.component.breakpointSize !== bp) { - this.component.breakpointSize = bp; - } - } - - private updateOverflow(): OverflowResult { - return this.overflow.calculate({ - items: this.component.items, - actions: this.component.actions, - containerWidth: this.component.getWidth(), - }); - } - - private updateSearch(overflowResult: OverflowResult) { - this.search.autoManage({ - hiddenCount: overflowResult.hiddenItems.length, - availableSpace: overflowResult.spacerWidth, - }); - } -} - -// In ShellBarV2: -private controller = new ShellBarV2Controller(this, ...); - -handleResize() { - this.controller.handleResize(); -} -``` - -**Benefits**: -- Separation of coordination logic -- Easier to test controller independently -- Component stays focused on rendering - -### Pattern 2: Strategy Pattern for Overflow - -**Current**: Single overflow algorithm hardcoded. - -**Problem**: Can't change strategy (e.g., different hiding logic for mobile). - -**Recommended**: Use Strategy pattern. - -```typescript -interface OverflowStrategy { - calculateHiddenItems( - items: OverflowItem[], - containerWidth: number, - ): string[]; -} - -class PriorityOverflowStrategy implements OverflowStrategy { - calculateHiddenItems(items, containerWidth) { - // Current priority-based algorithm - } -} - -class BalancedOverflowStrategy implements OverflowStrategy { - calculateHiddenItems(items, containerWidth) { - // Hide items more evenly - } -} - -class MobileOverflowStrategy implements OverflowStrategy { - calculateHiddenItems(items, containerWidth) { - // More aggressive hiding for mobile - } -} - -class ShellBarOverflow { - constructor(private strategy: OverflowStrategy) {} - - setStrategy(strategy: OverflowStrategy) { - this.strategy = strategy; - } - - calculate(params: OverflowParams) { - return this.strategy.calculateHiddenItems( - params.items, - params.containerWidth, - ); - } -} -``` - -**Benefits**: -- Multiple overflow algorithms -- Easy to test each strategy -- Can switch at runtime -- Open/closed principle - -### Pattern 3: Observer Pattern Instead of EventBus - -**Current**: Custom EventBus wrapper. - -**Problem**: Unnecessary abstraction. - -**Recommended**: Standard Observer pattern or native events. - -```typescript -interface Observer { - update(data: T): void; -} - -class OverflowObservable { - private observers: Array> = []; - - subscribe(observer: Observer) { - this.observers.push(observer); - return () => this.unsubscribe(observer); - } - - unsubscribe(observer: Observer) { - const index = this.observers.indexOf(observer); - if (index > -1) { - this.observers.splice(index, 1); - } - } - - notify(state: OverflowState) { - this.observers.forEach(obs => obs.update(state)); - } -} - -// Usage -class ShellBarV2 implements Observer { - onEnterDOM() { - this.overflow.subscribe(this); - } - - update(state: OverflowState) { - this.handleOverflowChanged(state); - } -} -``` - -**Or just use native events** (simpler): -```typescript -// In overflow module -this.dispatchEvent(new CustomEvent("overflow-changed", { detail })); - -// In component -this.addEventListener("overflow-changed", this.handleOverflow); -``` - -**Benefits**: -- Standard pattern -- Type-safe -- Less code - -### Pattern 4: Builder Pattern for Actions - -**Current**: Imperative array building with filter. - -**Problem**: Hard to modify, not fluent. - -**Recommended**: Builder pattern. - -```typescript -class ActionListBuilder { - private actions: ActionItem[] = []; - - addNotifications(config: { show: boolean, count?: string }) { - if (config.show) { - this.actions.push({ - id: "notifications", - visible: true, - count: config.count, - icon: "bell", - }); - } - return this; - } - - addProductSwitch(show: boolean) { - if (show) { - this.actions.push({ - id: "product-switch", - visible: true, - icon: "grid", - }); - } - return this; - } - - addAssistant(show: boolean) { - if (show) { - this.actions.push({ - id: "assistant", - visible: true, - icon: "da", - }); - } - return this; - } - - addProfile(show: boolean) { - if (show) { - this.actions.push({ - id: "profile", - visible: true, - }); - } - return this; - } - - build(): ActionItem[] { - return this.actions; - } -} - -// Usage -const actions = new ActionListBuilder() - .addNotifications({ show: this.showNotifications, count: this.notificationsCount }) - .addProductSwitch(this.showProductSwitch) - .addAssistant(this.hasAssistant) - .addProfile(this.hasProfile) - .build(); -``` - -**Benefits**: -- Fluent API -- Easy to extend -- Self-documenting -- Each method handles one action - -## Architecture Improvements - -### 1. Inversion of Control - -**Current**: Component creates all modules. - -**Problem**: Hard to test with mocks, tight coupling. - -**Recommended**: Dependency injection. - -```typescript -interface ShellBarV2Dependencies { - breakpoint?: ShellBarBreakpoint; - overflow?: ShellBarOverflow; - search?: ShellBarSearch; - navigation?: ShellBarNavigation; -} - -class ShellBarV2 extends UI5Element { - private breakpoint: ShellBarBreakpoint; - private overflow: ShellBarOverflow; - // ... - - constructor(deps: ShellBarV2Dependencies = {}) { - super(); - this.breakpoint = deps.breakpoint ?? new ShellBarBreakpoint(); - this.overflow = deps.overflow ?? new ShellBarOverflow(); - // ... - } -} - -// Testing -const mockOverflow = { calculate: jest.fn() }; -const shellbar = new ShellBarV2({ overflow: mockOverflow }); -``` - -**Benefits**: -- Easy to inject mocks for testing -- Can swap implementations -- Clear dependencies - -### 2. Pure Overflow Calculation - -**Current**: Overflow calculation mutates DOM while measuring. - -**Problem**: Layout thrashing, hard to test, imperative. - -**Recommended**: Pure calculation, then apply. - -```typescript -interface OverflowCalculationInput { - items: ReadonlyArray<{ - id: string; - width: number; - priority: number; - protected: boolean; - }>; - containerWidth: number; -} - -interface OverflowCalculationResult { - hiddenIds: ReadonlyArray; - visibleIds: ReadonlyArray; - showOverflowButton: boolean; -} - -// Pure function - no side effects -function calculateOverflow( - input: OverflowCalculationInput, -): OverflowCalculationResult { - const sortedItems = [...input.items].sort((a, b) => a.priority - b.priority); - - let usedWidth = 0; - const hidden: string[] = []; - const visible: string[] = []; - - for (const item of sortedItems) { - if (item.protected) { - visible.push(item.id); - usedWidth += item.width; - continue; - } - - if (usedWidth + item.width <= input.containerWidth) { - visible.push(item.id); - usedWidth += item.width; - } else { - hidden.push(item.id); - } - } - - return { - hiddenIds: hidden, - visibleIds: visible, - showOverflowButton: hidden.length > 0, - }; -} - -// Separate function to apply result to DOM -function applyOverflowResult( - result: OverflowCalculationResult, - querySelector: (selector: string) => Element | null, -) { - result.hiddenIds.forEach(id => { - const element = querySelector(`[data-id="${id}"]`); - element?.classList.add(CSS_CLASSES.HIDDEN); - }); - - result.visibleIds.forEach(id => { - const element = querySelector(`[data-id="${id}"]`); - element?.classList.remove(CSS_CLASSES.HIDDEN); - }); -} -``` - -**Benefits**: -- Pure function (easy to test without DOM) -- No layout thrashing -- Declarative -- Predictable - -### 3. Immutable State Updates - -**Current**: State mutated in place. - -```typescript -this.hiddenItems.push(itemId); -items.forEach(item => { item.inOverflow = true; }); -``` - -**Problem**: Hard to track changes, side effects. - -**Recommended**: Immutable updates. - -```typescript -// Before -this.hiddenItems.push(itemId); - -// After -this.hiddenItems = [...this.hiddenItems, itemId]; - -// Before -items.forEach(item => { - item.inOverflow = hiddenIds.includes(item._id); -}); - -// After -this.items = this.items.map(item => ({ - ...item, - inOverflow: hiddenIds.includes(item._id), -})); -``` - -**Benefits**: -- Predictable state changes -- Easy to debug (can log before/after) -- Works with time-travel debugging -- Safer in async code - -### 4. Clear Contracts with Interfaces - -**Current**: Implicit contracts (lambdas, any types). - -**Problem**: Hard to understand what module needs. - -**Recommended**: Explicit interfaces. - -```typescript -interface IOverflowSupport { - calculate(params: OverflowParams): OverflowResult; - getOverflowItems(): ReadonlyArray; -} - -interface OverflowParams { - readonly items: ReadonlyArray; - readonly actions: ReadonlyArray; - readonly containerWidth: number; - readonly showSearchField: boolean; -} - -interface OverflowResult { - readonly hiddenIds: ReadonlyArray; - readonly showOverflowButton: boolean; -} - -class ShellBarOverflowSupport implements IOverflowSupport { - calculate(params: OverflowParams): OverflowResult { - // Implementation - } - - getOverflowItems(): ReadonlyArray { - // Implementation - } -} -``` - -**Benefits**: -- Clear contracts -- TypeScript enforcement -- Self-documenting -- Easy to mock for testing - -### 5. Composition Over Classes - -**Current**: Each feature is a class. - -**Problem**: Classes add boilerplate. - -**Recommended**: Compose simple functions. - -```typescript -// Simple functions -function calculateBreakpoint(width: number): BreakpointType { - if (width <= 599) return "S"; - if (width <= 1023) return "M"; - if (width <= 1439) return "L"; - if (width <= 1919) return "XL"; - return "XXL"; -} - -function buildActions(params: ActionsParams): ActionItem[] { - return [ - params.showNotifications && { - id: "notifications", - count: params.notificationsCount, - icon: "bell", - }, - params.showProductSwitch && { - id: "product-switch", - icon: "grid", - }, - // ... - ].filter(Boolean) as ActionItem[]; -} - -// Compose in component -class ShellBarV2 { - private updateBreakpoint() { - const width = this.getBoundingClientRect().width; - this.breakpointSize = calculateBreakpoint(width); - } - - private updateActions() { - this.actions = buildActions({ - showNotifications: this.showNotifications, - notificationsCount: this.notificationsCount, - // ... - }); - } -} -``` - -**Benefits**: -- Less boilerplate -- Simpler to test -- Easy to compose -- Functional style - -## Missing Optimizations - -### 1. Memoization - -**Problem**: Expensive calculations run on every render. - -```typescript -// Current - recalculates every time -get overflowItems() { - const result = []; - this.hiddenActions.forEach(action => { /* ... */ }); - return result.sort((a, b) => a.order - b.order); -} -``` - -**Recommended**: Memoize expensive getters. - -```typescript -import { memoize } from "@ui5/webcomponents-base/util/memoize.js"; - -private _overflowItemsCache?: { ids: string[], result: OverflowItem[] }; - -get overflowItems(): OverflowItem[] { - // Only recalculate if hidden items changed - const currentIds = this.hiddenItems.join(","); - - if (this._overflowItemsCache?.ids === currentIds) { - return this._overflowItemsCache.result; - } - - const result = this.calculateOverflowItems(); - this._overflowItemsCache = { ids: currentIds, result }; - return result; -} -``` - -### 2. Debouncing Resize - -**Problem**: Resize handler fires many times per second. - -```typescript -// Current - no debouncing -handleResize() { - this.updateBreakpoint(); - this.updateOverflow(); -} -``` - -**Recommended**: Debounce or use requestAnimationFrame. - -```typescript -private resizeScheduled = false; - -handleResize() { - if (this.resizeScheduled) return; - - this.resizeScheduled = true; - requestAnimationFrame(() => { - this.updateBreakpoint(); - this.updateOverflow(); - this.resizeScheduled = false; - }); -} -``` - -### 3. Virtual Overflow List - -**Problem**: If hundreds of items in overflow, DOM gets slow. - -**Recommended**: Use virtual scrolling for overflow popover. - -```typescript -// Use ui5-list with growing enabled -// Or implement virtual scrolling - - {this.overflowItems.slice(0, this.visibleOverflowCount).map(item => ( - {item.text} - ))} - -``` - -## Priority Matrix - -| Issue | Impact | Effort | Priority | Status | -|-------|--------|--------|----------|--------| -| Lambda coupling | High | Medium | P0 | ✅ DONE | -| Remove EventBus/DomAdapter | Low | Low | P1 | ✅ DONE | -| State ownership unclear | Medium | Medium | P2 | ⚠️ PARTIAL | -| Layout thrashing in overflow | High | High | P0 | ⏳ TODO | -| Magic numbers | Medium | Low | P1 | ⏳ TODO | -| SearchSupport complexity | High | High | P1 | ⏳ TODO | -| No error handling | Medium | Medium | P2 | ⏳ TODO | -| No lazy initialization | Low | Low | P2 | ⏳ TODO | -| Priority flip hack | Medium | Low | P2 | ⏳ TODO | -| Circular references | Medium | Low | P3 | ✅ DONE | - -**Legend**: ✅ DONE | ⚠️ PARTIAL | ⏳ TODO - -## Recommended Implementation Plan - -### ✅ Phase 1 Completed: Quick Wins & Decoupling -**Completed Tasks**: -1. ✅ Removed EventBus and DomAdapter (-72 lines) -2. ✅ Removed lambda closures from OverflowSupport -3. ✅ Pass data explicitly to methods instead of closures -4. ✅ Made getOverflowItems a method with explicit data -5. ✅ Replaced querySelector with focused setVisible callback -6. ⚠️ Partially improved state ownership (removed collections storage) - -**Impact Achieved**: -- Cleaner code (-145 lines total) -- Better testability -- Clearer contracts -- No circular references - -### ⏳ Phase 2: Remaining Quick Wins (1-2 days) -1. Add constants for magic numbers -2. Add basic error handling and logging -3. Add lazy initialization -4. Fix priority flip hack properly - -**Impact**: Better readability, easier debugging. - -### Phase 3: Performance (5-7 days) -1. Make overflow calculation pure -2. Batch DOM reads and writes -3. Add memoization for expensive getters -4. Add resize debouncing - -**Impact**: Better performance, especially with many items. - -### Phase 4: Refactor Complex Modules (3-5 days) -1. Split SearchSupport into focused modules -2. Extract coordination to controller -3. Add Strategy pattern for overflow - -**Impact**: Better maintainability, easier to extend. - -## Testing Recommendations - -### Unit Tests -```typescript -// Test pure functions -describe("calculateOverflow", () => { - it("hides items when container too small", () => { - const items = [ - { id: "a", width: 100, priority: 1 }, - { id: "b", width: 100, priority: 2 }, - ]; - - const result = calculateOverflow({ items, containerWidth: 150 }); - - expect(result.hiddenIds).toEqual(["b"]); - expect(result.visibleIds).toEqual(["a"]); - }); -}); - -// Test with dependency injection -describe("ShellBarV2", () => { - it("updates breakpoint on resize", () => { - const mockBreakpoint = { calculate: jest.fn(() => "M") }; - const shellbar = new ShellBarV2({ breakpoint: mockBreakpoint }); - - shellbar.handleResize(); - - expect(mockBreakpoint.calculate).toHaveBeenCalled(); - expect(shellbar.breakpointSize).toBe("M"); - }); -}); -``` - -### Integration Tests -```typescript -describe("ShellBarV2 overflow", () => { - it("hides items when space limited", async () => { - const shellbar = await fixture(html` - - Item 1 - Item 2 - - `); - - // Resize to small width - shellbar.style.width = "300px"; - await nextFrame(); - - const item2 = shellbar.querySelector("#item2"); - expect(item2.inOverflow).toBe(true); - }); -}); -``` - -### Performance Tests -```typescript -describe("ShellBarV2 performance", () => { - it("handles resize with 50 items efficiently", async () => { - const shellbar = createShellBarWithItems(50); - - const start = performance.now(); - shellbar.handleResize(); - const duration = performance.now() - start; - - expect(duration).toBeLessThan(16); // 60fps = 16ms per frame - }); -}); -``` - -## Conclusion - -The ShellBarV2 architecture has good separation of concerns. We've completed Phase 1 improvements: - -### ✅ Completed (Phase 1) -- ✅ **Removed unnecessary abstractions** - EventBus and DomAdapter deleted (-72 lines) -- ✅ **Decoupled modules** - Pass data explicitly, not closures -- ✅ **Fixed circular references** - Direct method calls instead of events -- ✅ **Improved API** - Focused callbacks (setVisible) instead of generic (querySelector) -- ✅ **Better testability** - Easy to test with mock data - -**Net Impact**: -145 lines, cleaner architecture, better contracts - -### ⏳ Remaining Issues -- **Under-engineering complex parts** (overflow algorithm, error handling) -- **Performance issues** (layout thrashing - needs pure calculation) -- **Magic numbers** (priority system not documented) -- **Missing optimizations** (lazy initialization, memoization) - -### Next Recommended Actions (Phase 2) -1. **Add constants** for magic numbers (quick win) -2. **Add error handling** (fail fast in dev, graceful in prod) -3. **Fix overflow performance** (pure calculation + batch DOM) -4. **Add lazy initialization** (minor optimization) - -These remaining changes will make the code more readable, performant, and maintainable. - diff --git a/packages/fiori/src/shellbarv2/README.md b/packages/fiori/src/shellbarv2/README.md deleted file mode 100644 index a1985f963ed0..000000000000 --- a/packages/fiori/src/shellbarv2/README.md +++ /dev/null @@ -1,270 +0,0 @@ -# ShellBarV2 Architecture - -## Overview - -ShellBarV2 is a modular application header component. The architecture separates concerns into focused support modules that the main component coordinates. - -## Architecture Diagram - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ ShellBarV2 │ -│ (Main Component) │ -│ │ -│ Properties: branding, items, profile, searchField, etc. │ -│ Events: menu-button-click, notifications-click, etc. │ -└────────────┬────────────────────────────────────────────────────┘ - │ - │ Coordinates - │ - ┌────────┴─────────┬──────────┬──────────┬──────────┬─────────┐ - │ │ │ │ │ │ - ▼ ▼ ▼ ▼ ▼ ▼ -┌────────┐ ┌────────────┐ ┌──────┐ ┌──────┐ ┌─────────┐ ┌──────────┐ -│Actions │ │ Overflow │ │Search│ │Event │ │Breakpoint│ │ Item │ -│Support │ │ Support │ │Support│ │Bus │ │ │ │Navigation│ -└────────┘ └──────┬─────┘ └──────┘ └──────┘ └─────────┘ └──────────┘ - │ - ┌──────┴──────┐ - │ │ - ▼ ▼ - ┌──────┐ ┌──────────┐ - │Event │ │ DOM │ - │Bus │ │ Adapter │ - └──────┘ └──────────┘ -``` - -## Module Responsibilities - -### ShellBarV2 (Main Component) -- Coordinates all support modules -- Manages lifecycle (onEnterDOM, onExitDOM, rendering) -- Exposes public API (slots, properties, events) -- Delegates specific concerns to support modules - -### ShellBarActions -**Pure logic - no dependencies** -- Builds action items list from configuration -- Maps properties to visible actions (notifications, product-switch, assistant, profile) -- Returns filtered array of visible actions - -### ShellBarEventBus -**Internal event communication** -- Used by: OverflowSupport → ShellBarV2 -- Wraps native CustomEvent dispatching -- Provides simple pub/sub interface (emit, on, off) - -### ShellBarDomAdapter -**DOM access abstraction** -- Used by: OverflowSupport -- Provides width measurements -- Provides querySelector access to shadow DOM -- Single point for DOM operations - -### ShellBarBreakpoint -**Pure logic - no dependencies** -- Calculates breakpoint from width (S, M, L, XL, XXL) -- Thresholds: 599, 1023, 1439, 1919 -- Used to sync branding state and responsive behavior - -### ShellBarSearchSupport -**Search field management** -- Auto-collapse/expand logic based on available space -- Syncs with self-collapsible search fields -- Determines full-screen search mode -- Listens to search field events (ui5-open, ui5-close, ui5-search) -- Manages phone vs desktop behavior - -### ShellBarItemNavigation -**Keyboard navigation** -- Handles arrow keys (left/right), Home, End -- Finds tabbable elements and manages focus -- Respects input field cursor position -- Filters visible elements only - -### ShellBarOverflowSupport -**Most complex module - overflow management** -- Dependencies: EventBus, DomAdapter -- Iteratively hides items when space is limited -- Priority system: - - Content items hide first (order: 10-99) - - Action items hide second (order: 100-199) - - Protected items never hide (order: 900+) -- Measures DOM after each hide to check if overflow resolved -- Builds overflow popover items list -- Emits overflow-changed event - -## Data Flow - -### Initialization -``` -1. ShellBarV2 constructor creates support modules -2. onEnterDOM: registers resize handler, subscribes to events -3. onBeforeRendering: updates actions, syncs search state -4. onAfterRendering: updates breakpoint, calculates overflow -``` - -### Resize Flow -``` -1. ResizeHandler triggers handleResize() -2. Update breakpoint via ShellBarBreakpoint -3. Update overflow via ShellBarOverflowSupport - - OverflowSupport measures DOM - - Iteratively hides items - - Emits overflow-changed event -4. ShellBarV2 receives overflow-changed - - Updates item.inOverflow flags - - Shows/hides overflow button -5. SearchSupport auto-manages search state based on available space -``` - -### Overflow Calculation -``` -1. Build hidable items list with priorities -2. Reset all visibility -3. For each item (in priority order): - - Check if still overflowing - - If yes, hide item and track it - - If item has showInOverflow=true, mark overflow button needed -4. Emit result -``` - -## Dependency Injection Pattern - -All support modules receive dependencies via constructor: - -```typescript -// Example: OverflowSupport -constructor({ - eventBus, - domAdapter, - getActions, - getContent, - getCustomItems, -}) -``` - -This pattern: -- Makes dependencies explicit -- Enables testing -- Keeps modules decoupled -- Main component remains coordinator - -## Key Design Decisions - -### 1. Separation of Concerns -Each module handles one thing: -- Actions = action item list -- Overflow = hiding logic -- Search = search field behavior -- Breakpoint = size calculation -- Navigation = keyboard handling - -### 2. Pure Logic Where Possible -Modules like Actions and Breakpoint are pure functions wrapped in classes. No side effects, easy to test. - -### 3. Event-Based Communication -OverflowSupport emits events instead of directly modifying component state. ShellBarV2 listens and applies changes. - -### 4. DOM Access Centralized -DomAdapter provides single point for DOM operations. Makes it easier to track and test. - -### 5. No Cross-Module Dependencies -Support modules don't import each other. Only ShellBarV2 knows about all of them. - -### 6. Minimal State -Support modules are mostly stateless. State lives in ShellBarV2 properties. Modules calculate and return results. - -## File Structure - -``` -packages/fiori/src/ -├── ShellBarV2.ts # Main component -├── ShellBarV2Template.tsx # JSX template -├── ShellBarV2Item.ts # Item component -├── shellbarv2/ -│ ├── ShellBarActions.ts # Action items logic -│ ├── ShellBarBreakpoint.ts # Breakpoint calculation -│ ├── ShellBarDomAdapter.ts # DOM access -│ ├── ShellBarEventBus.ts # Event communication -│ ├── ShellBarItemNavigation.ts # Keyboard navigation -│ ├── ShellBarOverflowSupport.ts # Overflow management -│ ├── ShellBarSearchSupport.ts # Search field support -│ └── README.md # This file -``` - -## External Dependencies - -- `@ui5/webcomponents-base`: Base UI5 classes and decorators -- `@ui5/webcomponents`: Button, Icon, Popover, List -- `@ui5/webcomponents-icons`: bell, grid, da, overflow icons - -## Public API - -### Slots -- `branding`: Logo and title (ui5-shellbar-branding) -- `startButton`: Menu button (ui5-button) -- `content`: Center content items -- `items`: Custom items (ui5-shellbar-v2-item) -- `profile`: Profile avatar -- `assistant`: AI assistant button -- `searchField`: Search field component - -### Properties -- `notificationsCount`: Badge count -- `showNotifications`: Show notifications icon -- `showProductSwitch`: Show product switch icon -- `showSearchField`: Show search field - -### Events -- `menu-button-click` -- `notifications-click` -- `profile-click` -- `product-switch-click` -- `search-button-click` -- `search-field-toggle` -- `search-field-clear` - -## Usage Example - -```html - - - - - - -``` - -## Testing Strategy - -Each module can be tested independently: - -1. **Actions**: Pure function testing (input → output) -2. **Breakpoint**: Pure function testing -3. **EventBus**: Verify events are dispatched -4. **DomAdapter**: Mock DOM, verify selectors and measurements -5. **SearchSupport**: Test auto-collapse logic with mocked dependencies -6. **ItemNavigation**: Test keyboard events and focus management -7. **OverflowSupport**: Most complex - test hide priority, overflow detection - -Integration tests verify ShellBarV2 coordinates modules correctly. - -## Future Improvements - -1. **Virtual scrolling for overflow**: If hundreds of items, use virtual list -2. **Drag-and-drop reordering**: Allow users to reorder items -3. **Persistent overflow state**: Remember which items user prefers visible -4. **Animation support**: Smooth transitions when items hide/show -5. **Server-side rendering**: Ensure component works without JS - -## Summary - -ShellBarV2 uses a coordinator pattern. The main component delegates specific concerns to focused support modules. This keeps code simple, testable, and maintainable. Each module does one thing well. Dependencies flow inward (modules don't know about ShellBarV2). Communication happens via constructor injection and events. - -This architecture makes it easy to: -- Test modules in isolation -- Add new features (create new support module) -- Fix bugs (find responsible module) -- Understand code (clear boundaries) - diff --git a/packages/fiori/src/shellbarv2/ShellBarActions.ts b/packages/fiori/src/shellbarv2/ShellBarActions.ts index e4cf6b9096a9..83bddcd1d40f 100644 --- a/packages/fiori/src/shellbarv2/ShellBarActions.ts +++ b/packages/fiori/src/shellbarv2/ShellBarActions.ts @@ -1,16 +1,16 @@ interface ShellBarV2ActionItem { id: string; - visible: boolean; - count?: string; icon?: string; + count?: string; + visible: boolean; } interface ShellBarV2ActionsParams { + showProfile: boolean; + hasAssistant: boolean; + showProductSwitch: boolean; showNotifications: boolean; notificationsCount?: string; - showProductSwitch: boolean; - hasAssistant: boolean; - showProfile: boolean; } class ShellBarV2Actions { diff --git a/packages/fiori/src/shellbarv2/ShellBarContentSupport.ts b/packages/fiori/src/shellbarv2/ShellBarContentSupport.ts new file mode 100644 index 000000000000..047207807bad --- /dev/null +++ b/packages/fiori/src/shellbarv2/ShellBarContentSupport.ts @@ -0,0 +1,109 @@ +interface ShellBarContentSupportParams { + content: readonly HTMLElement[]; + isSBreakPoint: boolean; + hiddenItemIds: readonly string[]; +} + +interface ContentGroup { + start: HTMLElement[]; + end: HTMLElement[]; +} + +interface SeparatorConfig { + showStartSeparator: boolean; + showEndSeparator: boolean; +} + +interface PackedSeparatorInfo { + shouldPack: boolean; +} + +/** + * Handles content area logic: splitting into start/end groups and separator visibility. + * Pure logic - no side effects. + */ +class ShellBarContentSupport { + /** + * Splits content into start and end groups based on spacer element. + * Items before spacer = start (left-aligned) + * Items after spacer = end (right-aligned) + * Without spacer, all items are start content. + * + * Spacer can be detected by: + * - Component: + * - Attribute:
+ */ + splitContent(content: readonly HTMLElement[]): ContentGroup { + const spacerIndex = content.findIndex( + child => child.hasAttribute("ui5-shellbar-spacer"), + ); + + if (spacerIndex === -1) { + return { start: [...content], end: [] }; + } + + return { + start: content.slice(0, spacerIndex), + end: content.slice(spacerIndex + 1), + }; + } + + /** + * Calculates whether separators should be shown. + * Separators appear between content groups when at least one item is visible. + * Hidden on S breakpoint (mobile). + */ + getSeparatorConfig(params: ShellBarContentSupportParams): SeparatorConfig { + if (params.isSBreakPoint) { + return { showStartSeparator: false, showEndSeparator: false }; + } + + const { start, end } = this.splitContent(params.content); + + return { + showStartSeparator: start.some(item => !params.hiddenItemIds.includes((item as any)._individualSlot as string)), + showEndSeparator: end.some(item => !params.hiddenItemIds.includes((item as any)._individualSlot as string)), + }; + } + + /** + * Determines if a separator should be packed with this item. + * Separators are packed with the last visible item in a group. + * When that item hides, separator hides with it for proper measurement. + * + * Only applies on S breakpoint or when item is the last visible. + */ + shouldPackSeparator( + item: HTMLElement, + group: HTMLElement[], + hiddenIds: readonly string[], + isSBreakPoint: boolean, + ): PackedSeparatorInfo { + if (isSBreakPoint) { + return { shouldPack: false }; + } + + const isHidden = hiddenIds.includes((item as any)._individualSlot as string); + const isLastItem = group.at(-1) === item; + + return { shouldPack: isHidden && isLastItem }; + } + + /** + * Returns accessibility role for content area. + * Only group if multiple items exist. + */ + getContentRole(content: readonly HTMLElement[]): "group" | undefined { + const { start, end } = this.splitContent(content); + const totalItems = start.length + end.length; + return totalItems > 1 ? "group" : undefined; + } +} + +export default ShellBarContentSupport; +export type { + ShellBarContentSupportParams, + ContentGroup, + SeparatorConfig, + PackedSeparatorInfo, +}; diff --git a/packages/fiori/src/shellbarv2/ShellBarOverflowSupport.ts b/packages/fiori/src/shellbarv2/ShellBarOverflowSupport.ts index c0bdd5582671..99adbad676b1 100644 --- a/packages/fiori/src/shellbarv2/ShellBarOverflowSupport.ts +++ b/packages/fiori/src/shellbarv2/ShellBarOverflowSupport.ts @@ -13,14 +13,15 @@ interface ShellBarV2OverflowParams { actions: readonly ShellBarV2ActionItem[]; content: readonly HTMLElement[]; customItems: readonly ShellBarV2Item[]; - showSearchField: boolean; overflowOuter: HTMLElement; overflowInner: HTMLElement; + hiddenItemsIds: readonly string[]; + showSearchField: boolean; setVisible: (selector: string, visible: boolean) => void; } interface ShellBarV2OverflowResult { - hiddenItems: string[]; + hiddenItemsIds: string[]; showOverflowButton: boolean; } @@ -32,8 +33,6 @@ interface ShellBarV2OverflowItem { } class ShellBarV2OverflowSupport { - private hiddenItems: string[] = []; - /** * Performs overflow calculation by iteratively hiding items until no overflow. * Measures DOM after each hide to determine if more hiding is needed. @@ -44,7 +43,7 @@ class ShellBarV2OverflowSupport { } = params; if (!overflowOuter || !overflowInner) { - return { hiddenItems: [], showOverflowButton: false }; + return { hiddenItemsIds: [], showOverflowButton: false }; } // Build hidable items from state @@ -55,7 +54,7 @@ class ShellBarV2OverflowSupport { setVisible(item.selector, true); }); - const hiddenItems: string[] = []; + const hiddenItemsIds: string[] = []; let showOverflowButton = false; // Iteratively hide items until no overflow @@ -71,7 +70,7 @@ class ShellBarV2OverflowSupport { // Hide this item setVisible(item.selector, false); - hiddenItems.push(item.id); + hiddenItemsIds.push(item.id); // Only count items that should appear in overflow popover if (item.showInOverflow) { @@ -79,10 +78,8 @@ class ShellBarV2OverflowSupport { } }); - this.hiddenItems = hiddenItems; - return { - hiddenItems, + hiddenItemsIds, showOverflowButton, }; } @@ -101,7 +98,7 @@ class ShellBarV2OverflowSupport { */ private buildHidableItems(params: ShellBarV2OverflowParams): ShellBarV2HidableItem[] { const { - content, actions, customItems, showSearchField, + content, actions, customItems, showSearchField, hiddenItemsIds, } = params; const items: ShellBarV2HidableItem[] = []; @@ -130,7 +127,7 @@ class ShellBarV2OverflowSupport { const selector = ".ui5-shellbar-search-button"; let hideOrder = 0 + (showSearchField ? 100 : 0); - if (this.hiddenItems.includes("search-button")) { + if (hiddenItemsIds.includes("search-button")) { // flip priority to ensure currently hidden items remain hidden hideOrder *= -1; } @@ -149,7 +146,7 @@ class ShellBarV2OverflowSupport { const selector = ".ui5-shellbar-notifications-button"; let hideOrder = 1 + (showSearchField ? 100 : 0); - if (this.hiddenItems.includes("notifications")) { + if (hiddenItemsIds.includes("notifications")) { // flip priority to ensure currently hidden items remain hidden hideOrder *= -1; } @@ -168,7 +165,7 @@ class ShellBarV2OverflowSupport { const selector = ".ui5-shellbar-assistant-button"; let hideOrder = 2 + (showSearchField ? 100 : 0); - if (this.hiddenItems.includes("assistant")) { + if (hiddenItemsIds.includes("assistant")) { // flip priority to ensure currently hidden items remain hidden hideOrder *= -1; } @@ -189,7 +186,7 @@ class ShellBarV2OverflowSupport { const selector = `[data-ui5-stable="${slotName}"]`; let hideOrder = 3 + index + (showSearchField ? 100 : 0); - if (this.hiddenItems.includes(item._id)) { + if (hiddenItemsIds.includes(item._id)) { hideOrder *= -1; } @@ -240,12 +237,13 @@ class ShellBarV2OverflowSupport { getOverflowItems(params: { actions: readonly ShellBarV2ActionItem[]; customItems: readonly ShellBarV2Item[]; + hiddenItemsIds: readonly string[]; }): ReadonlyArray { - const { actions, customItems } = params; + const { actions, customItems, hiddenItemsIds } = params; const result: ShellBarV2OverflowItem[] = []; // Add hidden actions - const hiddenActions = actions.filter(action => this.hiddenItems.includes(action.id)); + const hiddenActions = actions.filter(action => hiddenItemsIds.includes(action.id)); hiddenActions.forEach(action => { let order = 0; if (action.id === "search-button") { @@ -262,7 +260,7 @@ class ShellBarV2OverflowSupport { }); // Add hidden custom items - const hiddenCustomItems = customItems.filter((item: ShellBarV2Item) => this.hiddenItems.includes(item._id)); + const hiddenCustomItems = customItems.filter((item: ShellBarV2Item) => hiddenItemsIds.includes(item._id)); hiddenCustomItems.forEach((item: ShellBarV2Item, index: number) => { result.push({ type: "item", id: item._id, data: item, order: 3 + index, diff --git a/packages/fiori/src/shellbarv2/ShellBarSearchSupport.ts b/packages/fiori/src/shellbarv2/ShellBarSearchSupport.ts index 55315c50cf13..e004a88c209a 100644 --- a/packages/fiori/src/shellbarv2/ShellBarSearchSupport.ts +++ b/packages/fiori/src/shellbarv2/ShellBarSearchSupport.ts @@ -2,11 +2,11 @@ import { isPhone } from "@ui5/webcomponents-base"; import type { IShellBarSearchField } from "../ShellBarV2.js"; interface ShellBarV2SearchSupportConstructorParams { + getOverflowed: () => boolean; getSearchState: () => boolean; setSearchState: (expanded: boolean) => void; getSearchField: () => IShellBarSearchField | null; getCSSVariable: (variable: string) => string; - getOverflowed: () => boolean; } class ShellBarV2SearchSupport { @@ -17,24 +17,24 @@ class ShellBarV2SearchSupport { private onSearchOpenBound = this.onSearchOpen.bind(this); private onSearchCloseBound = this.onSearchClose.bind(this); + private getOverflowed: () => boolean; private getSearchField: () => IShellBarSearchField | null; private getSearchState: () => boolean; private setSearchState: (expanded: boolean) => void; private getCSSVariable: (variable: string) => string; - private getOverflowed: () => boolean; constructor({ + getOverflowed, setSearchState, getSearchField, getSearchState, getCSSVariable, - getOverflowed, }: ShellBarV2SearchSupportConstructorParams) { + this.getOverflowed = getOverflowed; this.getCSSVariable = getCSSVariable; this.getSearchField = getSearchField; this.getSearchState = getSearchState; this.setSearchState = setSearchState; - this.getOverflowed = getOverflowed; } subscribe(searchField: HTMLElement | null = this.getSearchField()) { @@ -55,7 +55,45 @@ class ShellBarV2SearchSupport { searchField.removeEventListener("ui5-search", this.onSearchBound); } - onSearchOpen(e: Event) { + /** + * Auto-collapse/restore search field based on available space. + * Delegates decision logic to SearchController. + */ + autoManageSearchState(hiddenItems: number, availableSpace: number) { + if (!this.hasSearchField) { + return; + } + + // Get search field min width from CSS variable + const searchFieldWidth = this.getSearchFieldWidth(); + + const searchHasFocus = document.activeElement === this.getSearchField(); + const searchHasValue = !!this.getSearchField()?.value; + const preventCollapse = searchHasFocus || searchHasValue; + + if (hiddenItems > 0 && !preventCollapse) { + this.setSearchState(false); + } else if (availableSpace + this.getSearchButtonSize() > searchFieldWidth) { + this.setSearchState(true); + } + } + + /** + * Applies the show-search-field state to the search field. + */ + syncShowSearchFieldState() { + const search = this.getSearchField(); + if (!search) { + return; + } + if (isPhone()) { + search.open = this.getSearchState(); + } else { + search.collapsed = !this.getSearchState(); + } + } + + private onSearchOpen(e: Event) { if (e.target !== this.getSearchField()) { this.unsubscribe(e.target as HTMLElement); return; @@ -65,7 +103,7 @@ class ShellBarV2SearchSupport { } } - onSearchClose(e: Event) { + private onSearchClose(e: Event) { if (e.target !== this.getSearchField()) { this.unsubscribe(e.target as HTMLElement); return; @@ -75,7 +113,7 @@ class ShellBarV2SearchSupport { } } - onSearch(e: Event) { + private onSearch(e: Event) { if (e.target !== this.getSearchField()) { this.unsubscribe(e.target as HTMLElement); return; @@ -89,33 +127,10 @@ class ShellBarV2SearchSupport { this.setSearchState(!this.getSearchState()); } - /** - * Auto-collapse/restore search field based on available space. - * Delegates decision logic to SearchController. - */ - autoManageSearchState(hiddenItems: number, availableSpace: number) { - if (!this.hasSearchField) { - return; - } - - // Get search field min width from CSS variable - const searchFieldWidth = this.getSearchFieldWidth(); - - const searchHasFocus = document.activeElement === this.getSearchField(); - const searchHasValue = !!this.getSearchField()?.value; - const preventCollapse = searchHasFocus || searchHasValue; - - if (hiddenItems > 0 && !preventCollapse) { - this.setSearchState(false); - } else if (availableSpace > searchFieldWidth) { - this.setSearchState(true); - } - } - /** * Gets the minimum width needed for search field from CSS variable. */ - getSearchFieldWidth(): number { + private getSearchFieldWidth(): number { const width = this.getCSSVariable(ShellBarV2SearchSupport.CSS_VARIABLE); if (!width) { return ShellBarV2SearchSupport.FALLBACK_WIDTH; @@ -128,22 +143,19 @@ class ShellBarV2SearchSupport { return parseFloat(width); } - syncCollapsedState() { - const search = this.getSearchField(); - if (!search) { - return; - } - if (isPhone()) { - search.open = this.getSearchState(); - } else { - search.collapsed = !this.getSearchState(); - } - } - get hasSearchField() { return !!this.getSearchField(); } + /** + * Gets the size of the search button. + * If the search field is visible, the size is 0. + * Otherwise, it is the width of the search field (just a button in collapsed state). + */ + getSearchButtonSize(): number { + return this.getSearchState() ? 0 : this.getSearchField()?.getBoundingClientRect().width || 0; + } + /** * Determines if full-screen search should be shown. * Full-screen search activates when overflow happens AND search is visible. diff --git a/packages/fiori/src/themes/ShellBarV2.css b/packages/fiori/src/themes/ShellBarV2.css index 04e7dd4aa8da..471390a2797e 100644 --- a/packages/fiori/src/themes/ShellBarV2.css +++ b/packages/fiori/src/themes/ShellBarV2.css @@ -3,6 +3,8 @@ width: 100%; font-family: var(--sapFontFamily); --_ui5_shellbar_search_field_width: 25rem; /* Min width for search field */ + --_ui5-shellbar-separator-height: 2rem; + --_ui5-shellbar_separator-color: var(--sapGroup_ContentBorderColor); } .ui5-shellbar-root { @@ -72,12 +74,14 @@ } .ui5-shellbar-content-area { + flex-grow: 1; display: flex; align-items: center; gap: 0.5rem; } .ui5-shellbar-content-item { + gap: 0.5rem; flex-shrink: 0; display: flex; align-items: center; @@ -90,6 +94,14 @@ flex-shrink: 1; } +.ui5-shellbar-separator { + flex-grow: 0; + flex-shrink: 0; + height: var(--_ui5-shellbar-separator-height); + width: 1px; + background-color: var(--_ui5-shellbar_separator-color); +} + .ui5-shellbar-actions-area { flex-shrink: 0; display: flex; @@ -155,6 +167,11 @@ display: none !important; } +.ui5-shellbar-hidden-button { + visibility: hidden; + order: -1; +} + .ui5-shellbar-overflow-button { flex-shrink: 0; } diff --git a/packages/fiori/test/pages/ShellBarV2.html b/packages/fiori/test/pages/ShellBarV2.html index cdb170d3f912..87a696385619 100644 --- a/packages/fiori/test/pages/ShellBarV2.html +++ b/packages/fiori/test/pages/ShellBarV2.html @@ -31,6 +31,12 @@

Full Features (with All Actions + Overfl Action 3 Action 4 Action 5 + + Action 6 + Action 7 + Action 8 + Action 9 + Action 10 From a13048bc35f0a91c520200b5402ad43ec79a23b4 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Tue, 4 Nov 2025 10:00:30 +0200 Subject: [PATCH 11/57] refactor: add acc support and refactor names --- packages/fiori/src/ShellBarV2.ts | 166 ++++- packages/fiori/src/ShellBarV2Template.tsx | 27 +- packages/fiori/src/shellbarv2/README.md | 649 ++++++++++++++++++ .../src/shellbarv2/ShellBarAccessibility.ts | 141 ++++ ...arContentSupport.ts => ShellBarContent.ts} | 10 +- ...OverflowSupport.ts => ShellBarOverflow.ts} | 26 +- ...lBarSearchSupport.ts => ShellBarSearch.ts} | 14 +- .../fiori/test/pages/ShellBar_evolution.html | 196 ++---- 8 files changed, 1019 insertions(+), 210 deletions(-) create mode 100644 packages/fiori/src/shellbarv2/README.md create mode 100644 packages/fiori/src/shellbarv2/ShellBarAccessibility.ts rename packages/fiori/src/shellbarv2/{ShellBarContentSupport.ts => ShellBarContent.ts} (92%) rename packages/fiori/src/shellbarv2/{ShellBarOverflowSupport.ts => ShellBarOverflow.ts} (93%) rename packages/fiori/src/shellbarv2/{ShellBarSearchSupport.ts => ShellBarSearch.ts} (92%) diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index d06d682f9ae3..4789c98bc73e 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -4,11 +4,13 @@ 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 query from "@ui5/webcomponents-base/dist/decorators/query.js"; +import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js"; import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js"; import { getScopedVarName } from "@ui5/webcomponents-base/dist/CustomElementsScopeUtils.js"; import arraysAreEqual from "@ui5/webcomponents-base/dist/util/arraysAreEqual.js"; +import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; import type { IButton } from "@ui5/webcomponents/dist/Button.js"; import Button from "@ui5/webcomponents/dist/Button.js"; @@ -24,19 +26,36 @@ import "@ui5/webcomponents-icons/dist/overflow.js"; import ShellBarV2Template from "./ShellBarV2Template.js"; import shellBarV2Styles from "./generated/themes/ShellBarV2.css.js"; -import ShellBarV2Actions from "./shellbarv2/ShellBarActions.js"; import ShellBarV2Breakpoint from "./shellbarv2/ShellBarBreakpoint.js"; -import ShellBarV2SearchSupport from "./shellbarv2/ShellBarSearchSupport.js"; -import ShellBarV2ContentSupport from "./shellbarv2/ShellBarContentSupport.js"; +import ShellBarV2Search from "./shellbarv2/ShellBarSearch.js"; +import ShellBarV2Actions from "./shellbarv2/ShellBarActions.js"; +import ShellBarV2Content from "./shellbarv2/ShellBarContent.js"; import ShellBarV2ItemNavigation from "./shellbarv2/ShellBarItemNavigation.js"; -import ShellBarV2OverflowSupport from "./shellbarv2/ShellBarOverflowSupport.js"; +import ShellBarV2Overflow from "./shellbarv2/ShellBarOverflow.js"; +import ShellBarV2Accessibility from "./shellbarv2/ShellBarAccessibility.js"; import ShellBarV2Item from "./ShellBarV2Item.js"; import ShellBarSpacer from "./ShellBarSpacer.js"; import type ShellBarBranding from "./ShellBarBranding.js"; import type { ShellBarV2ActionItem } from "./shellbarv2/ShellBarActions.js"; import type { ShellBarV2BreakpointType } from "./shellbarv2/ShellBarBreakpoint.js"; -import type { ShellBarV2OverflowResult } from "./shellbarv2/ShellBarOverflowSupport.js"; +import type { ShellBarV2OverflowResult } from "./shellbarv2/ShellBarOverflow.js"; +import type { + ShellBarV2AccessibilityAttributes, + ShellBarV2AccessibilityInfo, + ShellBarV2ProfileAccessibilityAttributes, + ShellBarV2AreaAccessibilityAttributes, +} from "./shellbarv2/ShellBarAccessibility.js"; + +import { + SHELLBAR_LABEL, + SHELLBAR_NOTIFICATIONS, + SHELLBAR_PROFILE, + SHELLBAR_PRODUCTS, + SHELLBAR_SEARCH, + SHELLBAR_OVERFLOW, + SHELLBAR_ADDITIONAL_CONTEXT, +} from "./generated/i18n/i18n-defaults.js"; type ShellBarV2MenuButtonClickEventDetail = { menuButton: HTMLElement; @@ -99,6 +118,7 @@ interface IShellBarSearchField extends HTMLElement { renderer: jsxRenderer, template: ShellBarV2Template, fastNavigation: true, + languageAware: true, dependencies: [ Icon, List, @@ -283,6 +303,36 @@ class ShellBarV2 extends UI5Element { @property({ type: Boolean }) showSearchField = false; + /** + * Defines accessibility attributes for different areas of the component. + * + * The accessibilityAttributes object has the following fields: + * + * - **notifications** - `notifications.expanded` and `notifications.hasPopup`. + * - **profile** - `profile.expanded`, `profile.hasPopup` and `profile.name`. + * - **product** - `product.expanded` and `product.hasPopup`. + * - **search** - `search.hasPopup`. + * - **overflow** - `overflow.expanded` and `overflow.hasPopup`. + * + * The accessibility attributes support the following values: + * + * - **expanded**: Indicates whether the button, or another grouping element it controls, + * is currently expanded or collapsed. Accepts the following string values: `true` or `false`. + * + * - **hasPopup**: Indicates the availability and type of interactive popup element, + * such as menu or dialog, that can be triggered by the button. + * Accepts the following string values: `dialog`, `grid`, `listbox`, `menu` or `tree`. + * + * - **name**: Defines the accessible ARIA name of the area. + * Accepts any string. + * + * @default {} + * @public + * @since 2.0.0 + */ + @property({ type: Object }) + accessibilityAttributes: ShellBarV2AccessibilityAttributes = {}; + /** * Current breakpoint size. * @private @@ -347,36 +397,38 @@ class ShellBarV2 extends UI5Element { @query(".ui5-shellbar-overflow-container-inner") overflowInner?: HTMLElement; + @i18n("@ui5/webcomponents-fiori") + static i18nBundle: I18nBundle; + private handleResizeBound: ResizeObserverCallback = this.handleResize.bind(this); - searchSupport = new ShellBarV2SearchSupport({ + searchAdaptor = new ShellBarV2Search({ getSearchField: () => this.search, getSearchState: () => this.showSearchField, getCSSVariable: (cssVar: string) => this.getCSSVariable(cssVar), setSearchState: (expanded: boolean) => this.setSearchState(expanded), - getOverflowed: () => this.overflowSupport.isOverflowing(this.overflowOuter!, this.overflowInner!), + getOverflowed: () => this.overflowAdaptor.isOverflowing(this.overflowOuter!, this.overflowInner!), }); - - overflowSupport = new ShellBarV2OverflowSupport(); - contentSupport = new ShellBarV2ContentSupport(); - itemNavigation = new ShellBarV2ItemNavigation({ getDomRef: () => this.getDomRef() || null, }); breakpoint = new ShellBarV2Breakpoint(); - actionsSupport = new ShellBarV2Actions(); + contentAdaptor = new ShellBarV2Content(); + actionsAdaptor = new ShellBarV2Actions(); + overflowAdaptor = new ShellBarV2Overflow(); + accessibilityAdaptor = new ShellBarV2Accessibility(); /* ------------- Lifecycle Methods -------------- */ onEnterDOM() { ResizeHandler.register(this, this.handleResizeBound); - this.searchSupport.subscribe(); + this.searchAdaptor.subscribe(); } onExitDOM() { ResizeHandler.deregister(this, this.handleResizeBound); - this.searchSupport.unsubscribe(); + this.searchAdaptor.unsubscribe(); } onBeforeRendering() { @@ -388,7 +440,7 @@ class ShellBarV2 extends UI5Element { this.updateActions(); if (this.isSelfCollapsibleSearch) { - this.searchSupport.syncShowSearchFieldState(); + this.searchAdaptor.syncShowSearchFieldState(); } } @@ -414,7 +466,7 @@ class ShellBarV2 extends UI5Element { showProfile: this.hasProfile, }; - this.actions = this.actionsSupport.getActions(params); + this.actions = this.actionsAdaptor.getActions(params); } /* ------------- End of Actions Management -------------- */ @@ -473,12 +525,12 @@ class ShellBarV2 extends UI5Element { * Triggers rerender via property update to enable conditional rendering. */ private updateOverflow() { - if (!this.overflowSupport) { + if (!this.overflowAdaptor) { return; } // Delegate to controller - pass all data explicitly - const result = this.overflowSupport.updateOverflow({ + const result = this.overflowAdaptor.updateOverflow({ actions: this.actions, content: this.content, customItems: this.items, @@ -516,7 +568,7 @@ class ShellBarV2 extends UI5Element { this.hiddenItemsIds = hiddenItemsIds; this.showOverflowButton = showOverflowButton; } - this.showFullWidthSearch = this.searchSupport.shouldShowFullScreen(); + this.showFullWidthSearch = this.searchAdaptor.shouldShowFullScreen(); } private handleContentVisibilityChanged(oldHiddenItemsIds: string[], newHiddenItemsIds: string[]) { @@ -540,7 +592,7 @@ class ShellBarV2 extends UI5Element { this.updateBreakpoint(); const hiddenItemsIds = this.updateOverflow() ?? []; const spacerWidth = this.spacer?.getBoundingClientRect().width || 0; - this.searchSupport.autoManageSearchState(hiddenItemsIds.length, spacerWidth); + this.searchAdaptor.autoManageSearchState(hiddenItemsIds.length, spacerWidth); } handleOverflowClick() { @@ -686,7 +738,7 @@ class ShellBarV2 extends UI5Element { } get overflowItems() { - return this.overflowSupport.getOverflowItems({ + return this.overflowAdaptor.getOverflowItems({ actions: this.actions, customItems: this.items, hiddenItemsIds: this.hiddenItemsIds, @@ -709,15 +761,15 @@ class ShellBarV2 extends UI5Element { /* ------------- Content Management -------------- */ get startContent(): HTMLElement[] { - return this.contentSupport.splitContent(this.content).start; + return this.contentAdaptor.splitContent(this.content).start; } get endContent(): HTMLElement[] { - return this.contentSupport.splitContent(this.content).end; + return this.contentAdaptor.splitContent(this.content).end; } get separatorConfig() { - return this.contentSupport.getSeparatorConfig({ + return this.contentAdaptor.getSeparatorConfig({ content: this.content, isSBreakPoint: this.isSBreakPoint, hiddenItemIds: this.hiddenItemsIds, @@ -725,7 +777,7 @@ class ShellBarV2 extends UI5Element { } get contentRole() { - return this.contentSupport.getContentRole(this.content); + return this.contentAdaptor.getContentRole(this.content); } /** @@ -733,7 +785,7 @@ class ShellBarV2 extends UI5Element { */ getPackedSeparatorInfo(item: HTMLElement, isStartGroup: boolean) { const group = isStartGroup ? this.startContent : this.endContent; - return this.contentSupport.shouldPackSeparator( + return this.contentAdaptor.shouldPackSeparator( item, group, this.hiddenItemsIds, @@ -742,6 +794,64 @@ class ShellBarV2 extends UI5Element { } /* ------------- End of Content Management -------------- */ + + /* ------------- Accessibility -------------- */ + + /** + * Returns accessibility info for all interactive areas. + * Used by template for aria attributes. + */ + get accInfo(): ShellBarV2AccessibilityInfo { + return this.accessibilityAdaptor.getAccessibilityInfo({ + accessibilityAttributes: this.accessibilityAttributes, + overflowPopoverOpen: this.overflowPopoverOpen, + notificationsText: this._notificationsText, + profileText: this._profileText, + productsText: this._productsText, + searchText: this._searchText, + overflowText: this._overflowText, + }); + } + + /** + * Returns toolbar role for actions area based on visible items count. + */ + get actionsRole(): "toolbar" | undefined { + const visibleCount = this.actions.filter(a => !this.hiddenItemsIds.includes(a.id)).length; + return this.accessibilityAdaptor.getActionsRole(visibleCount); + } + + // i18n text getters + + get _shellbarText() { + return ShellBarV2.i18nBundle.getText(SHELLBAR_LABEL); + } + + get _notificationsText() { + return ShellBarV2.i18nBundle.getText(SHELLBAR_NOTIFICATIONS, this.notificationsCount || 0); + } + + get _profileText() { + return this.accessibilityAttributes.profile?.name || ShellBarV2.i18nBundle.getText(SHELLBAR_PROFILE); + } + + get _productsText() { + return ShellBarV2.i18nBundle.getText(SHELLBAR_PRODUCTS); + } + + get _searchText() { + return ShellBarV2.i18nBundle.getText(SHELLBAR_SEARCH); + } + + get _overflowText() { + return ShellBarV2.i18nBundle.getText(SHELLBAR_OVERFLOW); + } + + get _contentItemsText() { + return this.content.length > 1 ? ShellBarV2.i18nBundle.getText(SHELLBAR_ADDITIONAL_CONTEXT) : undefined; + } + + /* ------------- End of Accessibility -------------- */ } ShellBarV2.define(); @@ -756,4 +866,8 @@ export type { ShellBarV2ContentItemVisibilityChangeEventDetail, IShellBarSearchField, ShellBarV2Breakpoint, + ShellBarV2AccessibilityAttributes, + ShellBarV2AccessibilityInfo, + ShellBarV2ProfileAccessibilityAttributes, + ShellBarV2AreaAccessibilityAttributes, }; diff --git a/packages/fiori/src/ShellBarV2Template.tsx b/packages/fiori/src/ShellBarV2Template.tsx index 3772651c4dcd..142bba64c09b 100644 --- a/packages/fiori/src/ShellBarV2Template.tsx +++ b/packages/fiori/src/ShellBarV2Template.tsx @@ -61,7 +61,7 @@ export default function ShellBarV2Template(this: ShellBarV2) {
{/* Start separator */} {this.separatorConfig.showStartSeparator && ( @@ -114,17 +114,7 @@ export default function ShellBarV2Template(this: ShellBarV2) { {this.hasSearchField && ShellBarV2SearchField.call(this)} -
- - {this.hasSearchField && ( - +
+ Dashboard + Settings + + +

Custom HTML allows full control. Search button provided by ShellBar. Custom close button included.

+
+ + +
+

4. ui5-input with disableSearchCollapse

+
+ Type: ui5-input
+ Properties: disableSearchCollapse=true
+ Behavior: Search field always stays expanded, never auto-collapses +
+ + + Always Expanded + + + Action 1 + Action 2 + Action 3 + Action 4 + Action 5 + + +

Resize window - search stays expanded even when items overflow.

+
+ + +
+

5. Custom DIV with hideSearchButton

+
+ Type: Custom div
+ Properties: hideSearchButton=true
+ Behavior: No search button shown, field always visible +
+ + + No Search Button + +
+ +
+ Home + About + +
+

Search button hidden. Field is always visible (useful when managing state externally).

+
+ + +
+

6. With Overflow + Auto-Collapse

+
+ Type: ui5-input
+ Behavior: Search auto-collapses when content overflows (unless focused/has value) +
+ + + Overflow Demo + + + Reports + Analytics + Dashboard + Settings + Help + + Export + Import + Print + + +

Resize window to see auto-collapse. Type something or focus the field to prevent collapse.

+
+ + +
+

Event Log

+
+
+ + + + + diff --git a/packages/fiori/test/pages/ShellBar_evolution.html b/packages/fiori/test/pages/ShellBar_evolution.html index acdc28bbbcca..d425a07b1f7d 100644 --- a/packages/fiori/test/pages/ShellBar_evolution.html +++ b/packages/fiori/test/pages/ShellBar_evolution.html @@ -20,7 +20,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + S/4HANA Cloud + + + + + + + EMEA + Deliveries overdue for billing neeed more text because of a bug + + + +
+ New Version + +
+ + +
Instructions
+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + - + + + SAP Labs Bulgaria + + + + + PR10 + + PR 4 + PR3 + + + + + + PR2 + + PR1 + + PR6 + + PR7 + + PR8 + + PR9 + +
+ + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + From 790cd783596723d5209262d0e29a0d567f959478 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Tue, 4 Nov 2025 15:33:56 +0200 Subject: [PATCH 13/57] refactor: add css for legacy search --- packages/fiori/src/ShellBarV2.ts | 9 ++-- .../src/shellbarv2/ShellBarAccessibility.ts | 11 +++++ .../fiori/src/shellbarv2/ShellBarContent.ts | 10 ----- packages/fiori/src/themes/ShellBarV2.css | 6 ++- .../src/themes/ShellBarV2SearchLegacy.css | 44 +++++++++++++++++++ 5 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 packages/fiori/src/themes/ShellBarV2SearchLegacy.css diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index a0fe18190a4a..7f85858f2e2a 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -831,10 +831,6 @@ class ShellBarV2 extends UI5Element { }); } - get contentRole() { - return this.contentAdaptor.getContentRole(this.content); - } - /** * Returns packed separator info for a content item. */ @@ -876,6 +872,11 @@ class ShellBarV2 extends UI5Element { return this.accessibilityAdaptor.getActionsRole(visibleCount); } + get contentRole() { + const visibleItemsCount = this.content.filter(item => !this.hiddenItemsIds.includes((item as any)._individualSlot as string)).length; + return this.accessibilityAdaptor.getContentRole(visibleItemsCount); + } + // i18n text getters get _shellbarText() { diff --git a/packages/fiori/src/shellbarv2/ShellBarAccessibility.ts b/packages/fiori/src/shellbarv2/ShellBarAccessibility.ts index cee2977e6fd5..05b4ef9bb094 100644 --- a/packages/fiori/src/shellbarv2/ShellBarAccessibility.ts +++ b/packages/fiori/src/shellbarv2/ShellBarAccessibility.ts @@ -127,6 +127,17 @@ class ShellBarV2Accessibility { } return "toolbar"; } + + /** + * Returns accessibility role for content area. + * Only group if multiple items exist. + */ + getContentRole(visibleItemsCount: number): "group" | undefined { + if (visibleItemsCount <= 1) { + return undefined; + } + return "group"; + } } export default ShellBarV2Accessibility; diff --git a/packages/fiori/src/shellbarv2/ShellBarContent.ts b/packages/fiori/src/shellbarv2/ShellBarContent.ts index 57a3d791c030..e04e4ac78fd7 100644 --- a/packages/fiori/src/shellbarv2/ShellBarContent.ts +++ b/packages/fiori/src/shellbarv2/ShellBarContent.ts @@ -88,16 +88,6 @@ class ShellBarContent { return { shouldPack: isHidden && isLastItem }; } - - /** - * Returns accessibility role for content area. - * Only group if multiple items exist. - */ - getContentRole(content: readonly HTMLElement[]): "group" | undefined { - const { start, end } = this.splitContent(content); - const totalItems = start.length + end.length; - return totalItems > 1 ? "group" : undefined; - } } export default ShellBarContent; diff --git a/packages/fiori/src/themes/ShellBarV2.css b/packages/fiori/src/themes/ShellBarV2.css index 1192d4c6f2c2..ecbd6c9e87a6 100644 --- a/packages/fiori/src/themes/ShellBarV2.css +++ b/packages/fiori/src/themes/ShellBarV2.css @@ -1,3 +1,5 @@ +@import "./ShellBarV2SearchLegacy.css"; + :host { display: block; width: 100%; @@ -67,7 +69,6 @@ margin-left: auto; } - :host([show-search-field]) ::slotted([slot="searchField"]), :host([show-full-width-search]) .ui5-shellbar-search-field-area { min-width: var(--_ui5_shellbar_search_field_width); @@ -180,7 +181,7 @@ flex-shrink: 0; } -/* Full-screen search overlay */ +/* Full-screen search overlay (shared by both self-collapsible and legacy search) */ .ui5-shellbar-search-full-width-wrapper { position: absolute; bottom: 0.0625rem; @@ -220,3 +221,4 @@ :host([breakpoint-size="M"]) .ui5-shellbar-search-full-width-wrapper { padding: 0 2rem; } + diff --git a/packages/fiori/src/themes/ShellBarV2SearchLegacy.css b/packages/fiori/src/themes/ShellBarV2SearchLegacy.css new file mode 100644 index 000000000000..b94c45f0f629 --- /dev/null +++ b/packages/fiori/src/themes/ShellBarV2SearchLegacy.css @@ -0,0 +1,44 @@ +/* Legacy Search Styles - ONLY for ui5-input and custom div search fields */ + +/* CSS variable overrides for ui5-input component */ +:host { + --_ui5_input_placeholder_color: var(--sapShell_InteractiveTextColor); + --_ui5_input_border_radius: var(--_ui5_shellbar_input_border_radius); + --_ui5_input_focus_border_radius: var(--_ui5_shellbar_input_focus_border_radius); + --_ui5_input_background_color: var(--_ui5_shellbar_input_background_color); + --_ui5_input_focus_outline_color: var(--_ui5_shellbar_input_focus_outline_color); + --_ui5_input_margin_top_bottom: 0; +} + +/* ui5-input specific styles */ +::slotted([ui5-input]) { + background: var(--_ui5_shellbar_search_field_background); + border: var(--_ui5_shellbar_search_field_border); + box-shadow: var(--_ui5_shellbar_search_field_box_shadow); + color: var(--_ui5_shellbar_search_field_color); + height: 2.25rem; + width: 100%; + min-width: var(--_ui5_shellbar_search_field_width); +} + +/* ui5-input breakpoint adjustments */ +:host([breakpoint-size="M"]) ::slotted([ui5-input]), +:host([breakpoint-size="S"]) ::slotted([ui5-input]) { + min-width: 1rem; +} + +:host([breakpoint-size="M"][show-search-field]) .ui5-shellbar-overflow-container-right-child { + flex-grow: 1; +} + +/* ui5-input hover */ +::slotted([ui5-input]:hover) { + background: var(--_ui5_shellbar_search_field_background_hover); + box-shadow: var(--_ui5_shellbar_search_field_box_shadow_hover); +} + +/* ui5-input focus */ +::slotted([ui5-input][focused]) { + outline: var(--_ui5_shellbar_search_field_outline_focused); +} + From 483efe1ea13c2608397a69bc62f7d4e6c003aeda Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Tue, 4 Nov 2025 22:14:28 +0200 Subject: [PATCH 14/57] refactor: decorator fixes --- packages/fiori/src/ShellBarV2.ts | 15 +- packages/fiori/src/ShellBarV2Template.tsx | 81 +++----- .../shellbarv2/IShellBarSearchController.ts | 6 - .../fiori/src/shellbarv2/ShellBarSearch.ts | 26 +-- .../src/shellbarv2/ShellBarSearchLegacy.ts | 42 ++-- .../ShellBarSearchLegacyTemplate.tsx | 56 ++++++ .../src/shellbarv2/ShellBarSearchTemplate.tsx | 35 ++++ .../fiori/src/themes/NavigationLayout.css | 3 +- packages/fiori/src/themes/ShellBarV2.css | 187 ++++++++++++++++-- .../fiori/test/pages/ShellBar_evolution.html | 61 ++++++ 10 files changed, 380 insertions(+), 132 deletions(-) create mode 100644 packages/fiori/src/shellbarv2/ShellBarSearchLegacyTemplate.tsx create mode 100644 packages/fiori/src/shellbarv2/ShellBarSearchTemplate.tsx diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index 7f85858f2e2a..b41a091eaf71 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -26,14 +26,15 @@ import "@ui5/webcomponents-icons/dist/overflow.js"; import ShellBarV2Template from "./ShellBarV2Template.js"; import shellBarV2Styles from "./generated/themes/ShellBarV2.css.js"; -import ShellBarV2Breakpoint from "./shellbarv2/ShellBarBreakpoint.js"; -import ShellBarV2Search from "./shellbarv2/ShellBarSearch.js"; -import ShellBarLegacySearch from "./shellbarv2/ShellBarSearchLegacy.js"; import type { IShellBarSearchController } from "./shellbarv2/IShellBarSearchController.js"; + +import ShellBarV2Search from "./shellbarv2/ShellBarSearch.js"; +import ShellBarV2SearchLegacy from "./shellbarv2/ShellBarSearchLegacy.js"; import ShellBarV2Actions from "./shellbarv2/ShellBarActions.js"; import ShellBarV2Content from "./shellbarv2/ShellBarContent.js"; -import ShellBarV2ItemNavigation from "./shellbarv2/ShellBarItemNavigation.js"; import ShellBarV2Overflow from "./shellbarv2/ShellBarOverflow.js"; +import ShellBarV2Breakpoint from "./shellbarv2/ShellBarBreakpoint.js"; +import ShellBarV2ItemNavigation from "./shellbarv2/ShellBarItemNavigation.js"; import ShellBarV2Accessibility from "./shellbarv2/ShellBarAccessibility.js"; import ShellBarV2Item from "./ShellBarV2Item.js"; @@ -639,10 +640,6 @@ class ShellBarV2 extends UI5Element { /* ------------- Search Management -------------- */ - get renderSearchField() { - return this.searchAdaptor?.shouldRenderSearchField() || false; - } - /** * Initialize the appropriate search controller based on search field type. * Self-collapsible search (ui5-shellbar-search) → ShellBarV2Search @@ -659,7 +656,7 @@ class ShellBarV2 extends UI5Element { if (this.isSelfCollapsibleSearch) { this.searchAdaptor = new ShellBarV2Search(deps); } else { - this.searchAdaptor = new ShellBarLegacySearch({ + this.searchAdaptor = new ShellBarV2SearchLegacy({ ...deps, getDisableSearchCollapse: () => this.disableSearchCollapse, }); diff --git a/packages/fiori/src/ShellBarV2Template.tsx b/packages/fiori/src/ShellBarV2Template.tsx index 8c3ee79408fa..3ac30b534507 100644 --- a/packages/fiori/src/ShellBarV2Template.tsx +++ b/packages/fiori/src/ShellBarV2Template.tsx @@ -5,40 +5,28 @@ import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js"; import type ShellBarV2 from "./ShellBarV2.js"; import type ShellBarV2Item from "./ShellBarV2Item.js"; -function ShellBarV2SearchField(this: ShellBarV2) { - return ( - // .ui5-shellbar-search-field-area is used to measure the width of - // the search field. It must be present even if the search is in full-width mode. -
- {this.renderSearchField && ( - - )} -
- ); -} +import { + ShellBarV2SearchField, + ShellBarV2SearchFieldFullWidth +} from "./shellbarv2/ShellBarSearchTemplate.js"; -function ShellBarV2SearchFieldFullWidth(this: ShellBarV2) { - return ( -
-
- -
- -
- ); -} +import { + ShellBarV2SearchField as ShellBarV2SearchFieldLegacy, + ShellBarV2SearchButton as ShellBarV2SearchButtonLegacy, + ShellBarV2SearchFieldFullWidth as ShellBarV2SearchFieldFullWidthLegacy, +} from "./shellbarv2/ShellBarSearchLegacyTemplate.js"; export default function ShellBarV2Template(this: ShellBarV2) { + const isLegacySearch = !this.isSelfCollapsibleSearch; + + const SearchInBarTemplate = isLegacySearch ? ShellBarV2SearchFieldLegacy : ShellBarV2SearchField; + const SearchFullWidthTemplate = isLegacySearch ? ShellBarV2SearchFieldFullWidthLegacy : ShellBarV2SearchFieldFullWidth; + return ( <>
{/* Full-width search overlay */} - {this.showFullWidthSearch && ShellBarV2SearchFieldFullWidth.call(this)} + {this.showFullWidthSearch && SearchFullWidthTemplate.call(this)}
@@ -112,21 +100,8 @@ export default function ShellBarV2Template(this: ShellBarV2) {
)} - {this.hasSearchField && ShellBarV2SearchField.call(this)} - - {/* Search button for legacy search (ui5-input, custom div) */} - {this.hasSearchField && !this.isSelfCollapsibleSearch && !this.hideSearchButton && ( -

diff --git a/packages/fiori/src/shellbarv2/IShellBarSearchController.ts b/packages/fiori/src/shellbarv2/IShellBarSearchController.ts index 70a261a0b148..5658177a29bb 100644 --- a/packages/fiori/src/shellbarv2/IShellBarSearchController.ts +++ b/packages/fiori/src/shellbarv2/IShellBarSearchController.ts @@ -29,10 +29,4 @@ export interface IShellBarSearchController { * Returns true when shellbar is overflowing AND search is visible. */ shouldShowFullScreen(): boolean; - - /** - * Check if search field should be rendered. - * Returns true if search field exists and should be rendered in the bar. - */ - shouldRenderSearchField(): boolean; } diff --git a/packages/fiori/src/shellbarv2/ShellBarSearch.ts b/packages/fiori/src/shellbarv2/ShellBarSearch.ts index ebc76cfca9d1..d51edc49edf5 100644 --- a/packages/fiori/src/shellbarv2/ShellBarSearch.ts +++ b/packages/fiori/src/shellbarv2/ShellBarSearch.ts @@ -98,6 +98,14 @@ class ShellBarV2Search implements IShellBarSearchController { } } + /** + * Determines if full-screen search should be shown. + * Full-screen search activates when overflow happens AND search is visible. + */ + shouldShowFullScreen(): boolean { + return this.getOverflowed() && this.getSearchState(); + } + private onSearchOpen(e: Event) { if (e.target !== this.getSearchField()) { this.unsubscribe(e.target as HTMLElement); @@ -157,25 +165,9 @@ class ShellBarV2Search implements IShellBarSearchController { * If the search field is visible, the size is 0. * Otherwise, it is the width of the search field (just a button in collapsed state). */ - getSearchButtonSize(): number { + private getSearchButtonSize(): number { return this.getSearchState() ? 0 : this.getSearchField()?.getBoundingClientRect().width || 0; } - - /** - * Determines if full-screen search should be shown. - * Full-screen search activates when overflow happens AND search is visible. - */ - shouldShowFullScreen(): boolean { - return this.getOverflowed() && this.getSearchState(); - } - - /** - * Determines if search field should be rendered. - * Returns true if search field exists and should be rendered in the bar (not in full-screen mode). - */ - shouldRenderSearchField(): boolean { - return this.hasSearchField && !this.shouldShowFullScreen(); - } } export default ShellBarV2Search; diff --git a/packages/fiori/src/shellbarv2/ShellBarSearchLegacy.ts b/packages/fiori/src/shellbarv2/ShellBarSearchLegacy.ts index 95be1185a49d..4f57aa084690 100644 --- a/packages/fiori/src/shellbarv2/ShellBarSearchLegacy.ts +++ b/packages/fiori/src/shellbarv2/ShellBarSearchLegacy.ts @@ -1,6 +1,6 @@ import type { IShellBarSearchController } from "./IShellBarSearchController.js"; -interface ShellBarSearchLegacyConstructorParams { +interface ShellBarV2SearchLegacyConstructorParams { getOverflowed: () => boolean; getSearchState: () => boolean; setSearchState: (expanded: boolean) => void; @@ -14,7 +14,7 @@ interface ShellBarSearchLegacyConstructorParams { * Handles search fields that don't have collapsed/open properties. * Supports disableSearchCollapse for preventing auto-collapse. */ -class ShellBarSearchLegacy implements IShellBarSearchController { +class ShellBarV2SearchLegacy implements IShellBarSearchController { static CSS_VARIABLE = "--_ui5_shellbar_search_field_width"; static FALLBACK_WIDTH = 400; @@ -32,7 +32,7 @@ class ShellBarSearchLegacy implements IShellBarSearchController { getSearchState, getCSSVariable, getDisableSearchCollapse, - }: ShellBarSearchLegacyConstructorParams) { + }: ShellBarV2SearchLegacyConstructorParams) { this.getOverflowed = getOverflowed; this.getCSSVariable = getCSSVariable; this.getSearchField = getSearchField; @@ -91,6 +91,14 @@ class ShellBarSearchLegacy implements IShellBarSearchController { // Legacy search fields don't have collapsed/open properties to sync } + /** + * Determines if full-screen search should be shown. + * Full-screen search activates when overflow happens AND search is visible. + */ + shouldShowFullScreen(): boolean { + return this.getOverflowed() && this.getSearchState(); + } + /** * Get value from various field types. * Supports ui5-input (value property) and custom div (nested input element). @@ -114,9 +122,9 @@ class ShellBarSearchLegacy implements IShellBarSearchController { * Get minimum width needed for search field from CSS variable. */ private getSearchFieldWidth(): number { - const width = this.getCSSVariable(ShellBarSearchLegacy.CSS_VARIABLE); + const width = this.getCSSVariable(ShellBarV2SearchLegacy.CSS_VARIABLE); if (!width) { - return ShellBarSearchLegacy.FALLBACK_WIDTH; + return ShellBarV2SearchLegacy.FALLBACK_WIDTH; } // Convert rem to px @@ -128,7 +136,7 @@ class ShellBarSearchLegacy implements IShellBarSearchController { return parseFloat(width); } - get hasSearchField(): boolean { + private get hasSearchField(): boolean { return !!this.getSearchField(); } @@ -136,28 +144,12 @@ class ShellBarSearchLegacy implements IShellBarSearchController { * Get search button size for overflow calculation. * Returns 0 if search is expanded, otherwise returns button width. */ - getSearchButtonSize(): number { + private getSearchButtonSize(): number { return this.getSearchState() ? 0 : this.getSearchField()?.getBoundingClientRect().width || 0; } - - /** - * Determines if full-screen search should be shown. - * Full-screen search activates when overflow happens AND search is visible. - */ - shouldShowFullScreen(): boolean { - return this.getOverflowed() && this.getSearchState(); - } - - /** - * Check if search field should be rendered. - * Returns true if search field exists and should be rendered in the bar (not in full-screen mode). - */ - shouldRenderSearchField(): boolean { - return this.hasSearchField && this.getSearchState() && !this.shouldShowFullScreen(); - } } -export default ShellBarSearchLegacy; +export default ShellBarV2SearchLegacy; export type { - ShellBarSearchLegacyConstructorParams, + ShellBarV2SearchLegacyConstructorParams, }; diff --git a/packages/fiori/src/shellbarv2/ShellBarSearchLegacyTemplate.tsx b/packages/fiori/src/shellbarv2/ShellBarSearchLegacyTemplate.tsx new file mode 100644 index 000000000000..cfa87d91831d --- /dev/null +++ b/packages/fiori/src/shellbarv2/ShellBarSearchLegacyTemplate.tsx @@ -0,0 +1,56 @@ +import Button from "@ui5/webcomponents/dist/Button.js"; +import type ShellBarV2 from "../ShellBarV2.js"; + +function ShellBarV2SearchField(this: ShellBarV2) { + return ( + // .ui5-shellbar-search-field-area is used to measure the width of + // the search field. It must be present even if the search is in full-width mode. +
+ {this.showSearchField && !this.showFullWidthSearch && ( + + )} +
+ ); +} + +function ShellBarV2SearchFieldFullWidth(this: ShellBarV2) { + return ( +
+
+ +
+ +
+ ); +} + +function ShellBarV2SearchButton(this: ShellBarV2) { + return ( + <> + {!this.hideSearchButton && ( + +
+ ); +} + +export { + ShellBarV2SearchField, + ShellBarV2SearchFieldFullWidth, +}; diff --git a/packages/fiori/src/themes/NavigationLayout.css b/packages/fiori/src/themes/NavigationLayout.css index 24ec75a59324..66506b0e80ec 100644 --- a/packages/fiori/src/themes/NavigationLayout.css +++ b/packages/fiori/src/themes/NavigationLayout.css @@ -59,6 +59,7 @@ transform: translateX(100%); } -:host([has-side-navigation]) ::slotted([ui5-shellbar][slot="header"]) { +:host([has-side-navigation]) ::slotted([ui5-shellbar][slot="header"]), +:host([has-side-navigation]) ::slotted([ui5-shellbar-v2][slot="header"]) { padding-inline: 0.875rem; } diff --git a/packages/fiori/src/themes/ShellBarV2.css b/packages/fiori/src/themes/ShellBarV2.css index ecbd6c9e87a6..cedd6173fb48 100644 --- a/packages/fiori/src/themes/ShellBarV2.css +++ b/packages/fiori/src/themes/ShellBarV2.css @@ -1,34 +1,98 @@ @import "./ShellBarV2SearchLegacy.css"; -:host { - display: block; +/* ============================================================================ + HOST & CSS VARIABLES + ============================================================================ */ + +:host(:not([hidden])) { + display: inline-block; width: 100%; - font-family: var(--sapFontFamily); - --_ui5_shellbar_search_field_width: 25rem; /* Min width for search field */ - --_ui5-shellbar-separator-height: 2rem; + max-width: 100%; + background: var(--sapShellColor); + box-sizing: border-box; + + /* CSS variable overrides for ui5-button */ + --_ui5_button_base_height: var(--sapElement_Height); + --_ui5_button_base_padding: 0.5625rem; + --_ui5_button_base_min_width: 2.25rem; + --_ui5-button-badge-diameter: 0.75rem; + + /* ShellBar-specific variables */ --_ui5-shellbar_separator-color: var(--sapGroup_ContentBorderColor); + --_ui5-shellbar-separator-height: 2rem; + --_ui5_shellbar_search_field_width: 25rem; } +/* ============================================================================ + ROOT CONTAINER + ============================================================================ */ + .ui5-shellbar-root { display: flex; align-items: center; height: 2.75rem; - background: var(--sapShellColor); box-shadow: inset 0 -0.0625rem var(--sapShell_BorderColor); position: relative; + font-size: var(--sapFontSize); + font-weight: normal; } +/* ============================================================================ + WRAPPER & MAIN LAYOUT + ============================================================================ */ + .ui5-shellbar-wrapper { - /* prevents flickering when items layout */ - box-sizing: border-box; + box-sizing: border-box; display: flex; align-items: center; width: 100%; height: 100%; - padding: 0 1rem; gap: 0.5rem; } +/* ============================================================================ + SLOTTED BUTTONS (General Styles) + ============================================================================ */ + +::slotted([ui5-button]:not([slot^="content"])), +::slotted([ui5-toggle-button]:not([slot^="content"])) { + height: 2.25rem; + padding: 0; + border: 0.0625rem solid var(--sapButton_Lite_BorderColor); + background: var(--sapButton_Lite_Background); + color: var(--sapShell_TextColor); + box-sizing: border-box; + cursor: pointer; + border-radius: var(--_ui5_shellbar_button_border_radius); + font-weight: bold; +} + +/* Button States - Hover */ +::slotted([ui5-button]:not([slot^="content"]):hover), +::slotted([ui5-toggle-button]:not([slot^="content"]):hover) { + background: var(--sapShell_Hover_Background); + border-color: var(--sapButton_Lite_Hover_BorderColor); + color: var(--sapShell_TextColor); +} + +/* Button States - Active */ +::slotted([ui5-button]:not([slot^="content"])[active]), +::slotted([ui5-toggle-button]:not([slot^="content"])[active]) { + background: var(--sapShell_Active_Background); + border-color: var(--sapButton_Lite_Active_BorderColor); + color: var(--_ui5_shellbar_button_active_color); +} + +/* Button States - Focus */ +::slotted([ui5-button]:not([slot^="content"])), +::slotted([ui5-toggle-button]:not([slot^="content"])) { + --_ui5_button_focused_border: var(--_ui5_shellbar_button_focused_border); +} + +/* ============================================================================ + START AREA (Start Button, Branding) + ============================================================================ */ + .ui5-shellbar-start-button { flex-shrink: 0; display: flex; @@ -41,6 +105,10 @@ align-items: center; } +/* ============================================================================ + OVERFLOW CONTAINER + ============================================================================ */ + .ui5-shellbar-overflow-container { /* makes the container grow on the left side thus preventing search from flickering */ flex-direction: row-reverse; @@ -60,6 +128,10 @@ min-width: 100%; } +/* ============================================================================ + SEARCH AREA + ============================================================================ */ + .ui5-shellbar-search-field-area { flex: 0 1 auto; min-width: 0; @@ -74,6 +146,10 @@ min-width: var(--_ui5_shellbar_search_field_width); } +/* ============================================================================ + CONTENT AREA (Items, Spacer, Separator) + ============================================================================ */ + .ui5-shellbar-content-area { flex-grow: 1; display: flex; @@ -103,6 +179,10 @@ background-color: var(--_ui5-shellbar_separator-color); } +/* ============================================================================ + ACTIONS AREA + ============================================================================ */ + .ui5-shellbar-actions-area { flex-shrink: 0; display: flex; @@ -114,6 +194,10 @@ margin-left: auto; } +/* ============================================================================ + CUSTOM ITEMS + ============================================================================ */ + .ui5-shellbar-custom-item { flex-shrink: 0; display: flex; @@ -124,6 +208,11 @@ display: none; } + +/* ============================================================================ + ACTION BUTTONS (Notifications, Assistant, Profile) + ============================================================================ */ + .ui5-shellbar-notifications-button { position: relative; } @@ -148,6 +237,16 @@ .ui5-shellbar-assistant-button { display: flex; align-items: center; + margin-inline-start: var(--_ui5-shellbar-overflow-button-margin); +} + +::slotted([ui5-toggle-button][slot="assistant"]) { + margin-inline-start: 0; +} + +::slotted([ui5-toggle-button][slot="assistant"][pressed]), +::slotted([ui5-toggle-button][slot="assistant"][pressed]:hover:not([active])) { + color: var(--sapShell_Assistant_ForegroundColor); } .ui5-shellbar-profile-button { @@ -163,25 +262,42 @@ border-radius: 0.25rem; } -/* Overflow */ -.ui5-shellbar-hidden { - display: none !important; +slot[name="profile"] { + min-width: 0; } -.ui5-shellbar-hidden-button { - visibility: hidden; - order: -1; +::slotted([ui5-avatar][slot="profile"]) { + display: block; + width: 2rem; + height: 2rem; + min-width: 0; + min-height: 2rem; + font-size: var(--_ui5_avatar_fontsize_XS); + font-weight: normal; } -.ui5-shellbar-overflow-button { - flex-shrink: 0; +/* ============================================================================ + OVERFLOW UTILITIES + ============================================================================ */ + +.ui5-shellbar-hidden { + display: none !important; } .ui5-shellbar-no-overflow { flex-shrink: 0; } -/* Full-screen search overlay (shared by both self-collapsible and legacy search) */ +::slotted([hidden]) { + visibility: hidden; + order: -1; + position: absolute; +} + +/* ============================================================================ + FULL-SCREEN SEARCH OVERLAY + ============================================================================ */ + .ui5-shellbar-search-full-width-wrapper { position: absolute; bottom: 0.0625rem; @@ -206,6 +322,11 @@ .ui5-shellbar-search-full-width-wrapper .ui5-shellbar-cancel-button { width: auto; flex-shrink: 0; + color: var(--_ui5-shellbar_cancel-button-color); +} + +.ui5-shellbar-search-full-width-wrapper .ui5-shellbar-cancel-button:hover { + color: var(--_ui5-shellbar_cancel-button-color); } .ui5-shellbar-search-full-width-wrapper ::slotted([ui5-shellbar-search]) { @@ -213,12 +334,36 @@ width: 100%; } -/* Breakpoint-specific padding for full-screen search */ -:host([breakpoint-size="S"]) .ui5-shellbar-search-full-width-wrapper { +/* ============================================================================ + BREAKPOINTS + ============================================================================ */ + +/* Responsive padding per breakpoint */ +:host([breakpoint-size="S"]) { padding: 0 1rem; } -:host([breakpoint-size="M"]) .ui5-shellbar-search-full-width-wrapper { +:host([breakpoint-size="M"]) { padding: 0 2rem; } +:host([breakpoint-size="L"]) { + padding: 0 2rem; +} + +:host([breakpoint-size="XL"]) { + padding: 0 3rem; +} + +:host([breakpoint-size="XXL"]) { + padding: 0 3rem; +} + +/* Search overlay padding per breakpoint */ +:host([breakpoint-size="S"]) .ui5-shellbar-search-full-width-wrapper { + padding: 0 1rem; +} + +:host([breakpoint-size="M"]) .ui5-shellbar-search-full-width-wrapper { + padding: 0 2rem; +} \ No newline at end of file diff --git a/packages/fiori/test/pages/ShellBar_evolution.html b/packages/fiori/test/pages/ShellBar_evolution.html index d425a07b1f7d..765adda26094 100644 --- a/packages/fiori/test/pages/ShellBar_evolution.html +++ b/packages/fiori/test/pages/ShellBar_evolution.html @@ -161,6 +161,67 @@ + + + + SAP Labs Bulgaria + + + + + PR10 + + PR 4 + PR3 + + + + + + PR2 + + PR1 + + PR6 + + PR7 + + PR8 + + PR9 + + + + + + + + + + + + + + + + + + + + + + + + Date: Wed, 5 Nov 2025 00:10:07 +0200 Subject: [PATCH 15/57] refactor: add legacy members --- packages/fiori/cypress/specs/ShellBar.cy.tsx | 166 ++++++------ packages/fiori/src/ShellBarV2.ts | 244 ++++++++++++++---- packages/fiori/src/ShellBarV2Template.tsx | 15 ++ .../src/shellbarv2/ShellBarAccessibility.ts | 12 +- .../fiori/src/shellbarv2/ShellBarLegacy.ts | 201 +++++++++++++++ .../src/shellbarv2/ShellBarLegacyTemplate.tsx | 129 +++++++++ .../fiori/src/themes/ShellBarV2Legacy.css | 176 +++++++++++++ packages/fiori/test/pages/ShellBarV2.html | 35 ++- 8 files changed, 839 insertions(+), 139 deletions(-) create mode 100644 packages/fiori/src/shellbarv2/ShellBarLegacy.ts create mode 100644 packages/fiori/src/shellbarv2/ShellBarLegacyTemplate.tsx create mode 100644 packages/fiori/src/themes/ShellBarV2Legacy.css diff --git a/packages/fiori/cypress/specs/ShellBar.cy.tsx b/packages/fiori/cypress/specs/ShellBar.cy.tsx index a5db7c9bb2fd..3c04a6e7f6aa 100644 --- a/packages/fiori/cypress/specs/ShellBar.cy.tsx +++ b/packages/fiori/cypress/specs/ShellBar.cy.tsx @@ -1,5 +1,5 @@ -import ShellBar from "../../src/ShellBar.js"; -import ShellBarItem from "../../src/ShellBarItem.js"; +import ShellBar from "../../src/ShellBarV2.js"; +import ShellBarItem from "../../src/ShellBarV2Item.js"; import ShellBarSpacer from "../../src/ShellBarSpacer.js"; import activities from "@ui5/webcomponents-icons/dist/activities.js"; import navBack from "@ui5/webcomponents-icons/dist/nav-back.js"; @@ -11,7 +11,7 @@ import ToggleButton from "@ui5/webcomponents/dist/ToggleButton.js"; import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js"; import Avatar from "@ui5/webcomponents/dist/Avatar.js"; import Switch from "@ui5/webcomponents/dist/Switch.js"; -import ShellBarBranding from "@ui5/webcomponents-fiori/dist/ShellBarBranding.js" +import ShellBarBranding from "../../src/ShellBarBranding.js"; import ShellBarSearch from "../../src/ShellBarSearch.js"; const RESIZE_THROTTLE_RATE = 300; // ms @@ -643,86 +643,86 @@ describe("Events", () => { .should("have.been.calledOnce"); }); - it("Test search field clear event default behavior", () => { - cy.mount( - - - - ); - - cy.get("[ui5-shellbar]") - .as("shellbar"); - - // Set up event listener without preventing default - cy.get("@shellbar") - .then(shellbar => { - shellbar.get(0).addEventListener("ui5-search-field-clear", cy.stub().as("searchFieldClear")); - }); - - // Trigger full width search mode by reducing viewport - cy.viewport(400, 800); - - // Manually call the cancel button handler - cy.get("@shellbar").then(shellbar => { - const shellbarInstance = shellbar.get(0); - // Call the private method directly to simulate cancel button press - shellbarInstance._handleCancelButtonPress(); - }); - - // Verify the event was fired - cy.get("@searchFieldClear") - .should("have.been.calledOnce"); - - // Verify search field value is cleared (default behavior) - cy.get("#search") - .should("have.value", ""); - - // Verify search is closed - cy.get("@shellbar") - .should("have.prop", "showSearchField", false); - }); - - it("Test search field clear event can be prevented", () => { - cy.mount( - - - - ); - - cy.get("[ui5-shellbar]") - .as("shellbar"); - - // Set up event listener that prevents default - cy.get("@shellbar") - .then(shellbar => { - shellbar.get(0).addEventListener("ui5-search-field-clear", (event) => { - event.preventDefault(); - }); - shellbar.get(0).addEventListener("ui5-search-field-clear", cy.stub().as("searchFieldClear")); - }); - - // Trigger full width search mode by reducing viewport - cy.viewport(400, 800); - - // Manually call the cancel button handler - cy.get("@shellbar").then(shellbar => { - const shellbarInstance = shellbar.get(0); - // Call the private method directly to simulate cancel button press - shellbarInstance._handleCancelButtonPress(); - }); - - // Verify the event was fired - cy.get("@searchFieldClear") - .should("have.been.calledOnce"); - - // Verify search field value is preserved (due to preventDefault) - cy.get("#search") - .should("have.value", "test search text"); - - // Verify search is closed - cy.get("@shellbar") - .should("have.prop", "showSearchField", false); - }); + // it("Test search field clear event default behavior", () => { + // cy.mount( + // + // + // + // ); + + // cy.get("[ui5-shellbar]") + // .as("shellbar"); + + // // Set up event listener without preventing default + // cy.get("@shellbar") + // .then(shellbar => { + // shellbar.get(0).addEventListener("ui5-search-field-clear", cy.stub().as("searchFieldClear")); + // }); + + // // Trigger full width search mode by reducing viewport + // cy.viewport(400, 800); + + // // Manually call the cancel button handler + // cy.get("@shellbar").then(shellbar => { + // const shellbarInstance = shellbar.get(0); + // // Call the private method directly to simulate cancel button press + // shellbarInstance._handleCancelButtonPress(); + // }); + + // // Verify the event was fired + // cy.get("@searchFieldClear") + // .should("have.been.calledOnce"); + + // // Verify search field value is cleared (default behavior) + // cy.get("#search") + // .should("have.value", ""); + + // // Verify search is closed + // cy.get("@shellbar") + // .should("have.prop", "showSearchField", false); + // }); + + // it("Test search field clear event can be prevented", () => { + // cy.mount( + // + // + // + // ); + + // cy.get("[ui5-shellbar]") + // .as("shellbar"); + + // // Set up event listener that prevents default + // cy.get("@shellbar") + // .then(shellbar => { + // shellbar.get(0).addEventListener("ui5-search-field-clear", (event) => { + // event.preventDefault(); + // }); + // shellbar.get(0).addEventListener("ui5-search-field-clear", cy.stub().as("searchFieldClear")); + // }); + + // // Trigger full width search mode by reducing viewport + // cy.viewport(400, 800); + + // // Manually call the cancel button handler + // cy.get("@shellbar").then(shellbar => { + // const shellbarInstance = shellbar.get(0); + // // Call the private method directly to simulate cancel button press + // shellbarInstance._handleCancelButtonPress(); + // }); + + // // Verify the event was fired + // cy.get("@searchFieldClear") + // .should("have.been.calledOnce"); + + // // Verify search field value is preserved (due to preventDefault) + // cy.get("#search") + // .should("have.value", "test search text"); + + // // Verify search is closed + // cy.get("@shellbar") + // .should("have.prop", "showSearchField", false); + // }); describe("Big screen", () => { beforeEach(() => { diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index b41a091eaf71..360b96c2a9f5 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -22,9 +22,11 @@ import "@ui5/webcomponents-icons/dist/bell.js"; import "@ui5/webcomponents-icons/dist/grid.js"; import "@ui5/webcomponents-icons/dist/da.js"; import "@ui5/webcomponents-icons/dist/overflow.js"; +import Menu from "@ui5/webcomponents/dist/Menu.js"; import ShellBarV2Template from "./ShellBarV2Template.js"; import shellBarV2Styles from "./generated/themes/ShellBarV2.css.js"; +import shellBarV2LegacyStyles from "./generated/themes/ShellBarV2Legacy.css.js"; import type { IShellBarSearchController } from "./shellbarv2/IShellBarSearchController.js"; @@ -36,6 +38,7 @@ import ShellBarV2Overflow from "./shellbarv2/ShellBarOverflow.js"; import ShellBarV2Breakpoint from "./shellbarv2/ShellBarBreakpoint.js"; import ShellBarV2ItemNavigation from "./shellbarv2/ShellBarItemNavigation.js"; import ShellBarV2Accessibility from "./shellbarv2/ShellBarAccessibility.js"; +import ShellBarV2Legacy from "./shellbarv2/ShellBarLegacy.js"; import ShellBarV2Item from "./ShellBarV2Item.js"; import ShellBarSpacer from "./ShellBarSpacer.js"; @@ -46,6 +49,7 @@ import type { ShellBarV2OverflowResult } from "./shellbarv2/ShellBarOverflow.js" import type { ShellBarV2AccessibilityAttributes, ShellBarV2AccessibilityInfo, + ShellBarV2LogoAccessibilityAttributes, ShellBarV2ProfileAccessibilityAttributes, ShellBarV2AreaAccessibilityAttributes, } from "./shellbarv2/ShellBarAccessibility.js"; @@ -94,6 +98,18 @@ type ShellBarV2ContentItemVisibilityChangeEventDetail = { items: Array; }; +/* ============================================================================= +Legacy Event Types (DELETE WHEN REMOVING LEGACY) +================================================================================ */ + +type ShellBarV2LogoClickEventDetail = { + targetRef: HTMLElement; +}; + +type ShellBarV2MenuItemClickEventDetail = { + item: HTMLElement; +}; + interface IShellBarSearchField extends HTMLElement { focused: boolean; value: string; @@ -118,7 +134,7 @@ interface IShellBarSearchField extends HTMLElement { */ @customElement({ tag: "ui5-shellbar-v2", - styles: shellBarV2Styles, + styles: [shellBarV2Styles, shellBarV2LegacyStyles], renderer: jsxRenderer, template: ShellBarV2Template, fastNavigation: true, @@ -131,6 +147,7 @@ interface IShellBarSearchField extends HTMLElement { ShellBarSpacer, ShellBarV2Item, ListItemStandard, + Menu, ], }) /** @@ -147,6 +164,7 @@ interface IShellBarSearchField extends HTMLElement { * @public */ @event("notifications-click", { + cancelable: true, bubbles: true, }) /** @@ -163,6 +181,7 @@ interface IShellBarSearchField extends HTMLElement { * @public */ @event("product-switch-click", { + cancelable: true, bubbles: true, }) /** @@ -200,6 +219,26 @@ interface IShellBarSearchField extends HTMLElement { @event("content-item-visibility-change", { bubbles: true, }) +/* ============================================================================= +Legacy Events (DELETE WHEN REMOVING LEGACY) +================================================================================ */ +/** + * Fired when the logo is clicked. + * @param {HTMLElement} targetRef dom ref of the logo element + * @public + */ +@event("logo-click", { + bubbles: true, +}) +/** + * Fired when a menu item is clicked. + * @param {HTMLElement} item DOM ref of the activated list item + * @public + */ +@event("menu-item-click", { + cancelable: true, + bubbles: true, +}) class ShellBarV2 extends UI5Element { eventDetails!: { "menu-button-click": ShellBarV2MenuButtonClickEventDetail; @@ -210,6 +249,9 @@ class ShellBarV2 extends UI5Element { "search-field-toggle": ShellBarV2SearchFieldToggleEventDetail; "search-field-clear": ShellBarV2SearchFieldClearEventDetail; "content-item-visibility-change": ShellBarV2ContentItemVisibilityChangeEventDetail; + /* Legacy Events (DELETE WHEN REMOVING LEGACY) */ + "logo-click": ShellBarV2LogoClickEventDetail; + "menu-item-click": ShellBarV2MenuItemClickEventDetail; }; /** @@ -307,26 +349,6 @@ class ShellBarV2 extends UI5Element { @property({ type: Boolean }) showSearchField = false; - /** - * Hides the search button. - * Only applies to legacy search fields (ui5-input, custom div). - * For self-collapsible search (ui5-shellbar-search), use the search field's own collapsed state. - * @default false - * @public - */ - @property({ type: Boolean }) - hideSearchButton = false; - - /** - * Disables automatic search field collapse when space is limited. - * Only applies to legacy search fields (ui5-input, custom div). - * Self-collapsible search (ui5-shellbar-search) manages its own state. - * @default false - * @public - */ - @property({ type: Boolean }) - disableSearchCollapse = false; - /** * Defines accessibility attributes for different areas of the component. * @@ -437,10 +459,79 @@ class ShellBarV2 extends UI5Element { overflowAdaptor = new ShellBarV2Overflow(); accessibilityAdaptor = new ShellBarV2Accessibility(); - /* ------------- Lifecycle Methods -------------- */ + /* ========================================================================= + Legacy Members + ============================================================================ */ + + /** + * Defines the logo slot (legacy). + * For new implementations, use the branding slot. + * @public + */ + @slot() + logo!: Array; + + /** + * Defines the menu items slot (legacy). + * @public + */ + @slot() + menuItems!: Array; + + /** + * Hides the search button. + * Only applies to legacy search fields (ui5-input, custom div). + * For self-collapsible search (ui5-shellbar-search), use the search field's own collapsed state. + * @default false + * @public + */ + @property({ type: Boolean }) + hideSearchButton = false; + + /** + * Disables automatic search field collapse when space is limited. + * Only applies to legacy search fields (ui5-input, custom div). + * Self-collapsible search (ui5-shellbar-search) manages its own state. + * @default false + * @public + */ + @property({ type: Boolean }) + disableSearchCollapse = false; + + /** + * Defines the primary title (legacy). + * For new implementations, use the branding slot. + * @default undefined + * @public + */ + @property() + primaryTitle?: string; + + /** + * Defines the secondary title (legacy). + * For new implementations, use the branding slot. + * @default undefined + * @public + */ + @property() + secondaryTitle?: string; + + /** + * Open state of the menu popover (legacy). + * @private + */ + @property({ type: Boolean }) + menuPopoverOpen = false; + + legacyAdaptor?: ShellBarV2Legacy; + + /* ========================================================================= + Lifecycle Methods + ============================================================================ */ onEnterDOM() { this.initSearchController(); + this.initLegacyController(); ResizeHandler.register(this, this.handleResizeBound); this.searchAdaptor?.subscribe(); } @@ -448,6 +539,7 @@ class ShellBarV2 extends UI5Element { onExitDOM() { ResizeHandler.deregister(this, this.handleResizeBound); this.searchAdaptor?.unsubscribe(); + this.legacyAdaptor?.unsubscribe(); } onBeforeRendering() { @@ -468,9 +560,9 @@ class ShellBarV2 extends UI5Element { this.updateOverflow(); } - /* ------------- End of Lifecycle Methods -------------- */ - - /* ------------- Actions Management -------------- */ + /* ========================================================================= + Actions Management + ============================================================================ */ /** * Updates actions by delegating to controller. @@ -488,9 +580,9 @@ class ShellBarV2 extends UI5Element { this.actions = this.actionsAdaptor.getActions(params); } - /* ------------- End of Actions Management -------------- */ - - /* ------------- Breakpoint Management -------------- */ + /* ========================================================================= + Breakpoint Management + ============================================================================ */ /** * Updates the breakpoint by delegating calculation to controller. * This is the coordination logic - gather data, delegate, apply result. @@ -504,9 +596,9 @@ class ShellBarV2 extends UI5Element { } } - /* ------------- End of Breakpoint Management -------------- */ - - /* ------------- Notifications Management -------------- */ + /* ========================================================================= + Notifications Management + ============================================================================ */ _handleNotificationsClick() { const notificationsBtn = this.shadowRoot!.querySelector + ); +} + +/** + * Renders the menu popover. + * Contains the list of menu items. + */ +function ShellBarV2MenuPopover(this: ShellBarV2) { + const legacy = this.legacyAdaptor; + if (!legacy || !legacy.hasMenuItems) { + return null; + } + + return ( + + + + + + ); +} + +export { + ShellBarV2LegacyLogoArea, + ShellBarV2LegacyTitleArea, + ShellBarV2LegacyBrandingArea, + ShellBarV2MenuButton, + ShellBarV2MenuPopover, +}; diff --git a/packages/fiori/src/themes/ShellBarV2Legacy.css b/packages/fiori/src/themes/ShellBarV2Legacy.css new file mode 100644 index 000000000000..bf7b56a68dc1 --- /dev/null +++ b/packages/fiori/src/themes/ShellBarV2Legacy.css @@ -0,0 +1,176 @@ +/* Legacy Features CSS - Logo, Titles, Menu */ + +/* Logo */ +.ui5-shellbar-logo { + overflow: hidden; + cursor: pointer; +} + +.ui5-shellbar-logo-area, +.ui5-shellbar-legacy-branding { + overflow: hidden; + display: flex; + align-items: center; + padding: .25rem .5rem .25rem .25rem; + box-sizing: border-box; + cursor: pointer; + background: var(--sapButton_Lite_Background); + border: 1px solid var(--sapButton_Lite_BorderColor); + color: var(--sapShell_TextColor); + margin-inline-start: 0.125rem; +} + +.ui5-shellbar-logo:focus, +.ui5-shellbar-logo-area:focus { + outline: var(--_ui5_shellbar_logo_outline); + outline-offset: calc(-1 * var(--sapContent_FocusWidth)); + border-radius: var(--_ui5_shellbar_logo_border_radius); +} + +.ui5-shellbar-overflow-container > .ui5-shellbar-logo:hover, +.ui5-shellbar-logo-area:hover { + box-shadow: var(--_ui5_shellbar_button_box_shadow); + border-radius: var(--_ui5_shellbar_logo_border_radius); +} + +.ui5-shellbar-logo-area:active:focus { + background: var(--sapShell_Active_Background); + border: 1px solid var(--sapButton_Lite_Active_BorderColor); + color: var(--sapShell_Active_TextColor); +} + +::slotted([slot="logo"]) { + max-height: 2rem; +} + +::slotted([slot="logo"]):active { + pointer-events: none; +} + +/* Title Area */ +.ui5-shellbar-title-area, +.ui5-shellbar-headings { + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + overflow: hidden; + margin-inline-start: 0.25rem; +} + +.ui5-shellbar-primary-title, +.ui5-shellbar-menu-button-title, +.ui5-shellbar-title { + display: inline-block; + font-family: var(--sapFontSemiboldDuplexFamily); + margin: 0; + font-size: var(--_ui5_shellbar_menu_button_title_font_size); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--sapShell_SubBrand_TextColor); +} + +.ui5-shellbar-secondary-title { + display: inline-block; + font-size: var(--sapFontSmallSize); + color: var(--sapShell_TextColor); + font-weight: normal; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + margin: 0; + text-align: start; +} + +/* Menu Button */ +.ui5-shellbar-menu-button { + white-space: nowrap; + overflow: hidden; + display: flex; + align-items: center; + padding: 0.25rem 0.5rem; + cursor: text; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + margin-inline-start: 0.5rem; + height: 2.25rem; + border: 0.0625rem solid var(--sapButton_Lite_BorderColor); + background: var(--sapButton_Lite_Background); + outline-color: var(--_ui5_shellbar_logo_outline_color); + color: var(--sapShell_TextColor); + box-sizing: border-box; + border-radius: var(--_ui5_shellbar_button_border_radius); + position: relative; + font-weight: bold; +} + +.ui5-shellbar-menu-button.ui5-shellbar-menu-button--interactive { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + cursor: pointer; + background: var(--sapButton_Lite_Background); + border: var(--_ui5_shellbar_button_border); + color: var(--sapShell_TextColor); +} + +.ui5-shellbar-menu-button.ui5-shellbar-menu-button--interactive:hover { + background: var(--sapShell_Hover_Background); + border-color: var(--sapButton_Lite_Hover_BorderColor); + color: var(--sapShell_TextColor); + box-shadow: var(--_ui5_shellbar_button_box_shadow); +} + +.ui5-shellbar-menu-button.ui5-shellbar-menu-button--interactive:active { + background: var(--sapShell_Active_Background); + border-color: var(--sapButton_Lite_Active_BorderColor); + color: var(--_ui5_shellbar_button_active_color); + box-shadow: var(--_ui5_shellbar_button_box_shadow_active); +} + +.ui5-shellbar-menu-button.ui5-shellbar-menu-button--interactive:active .ui5-shellbar-menu-button-arrow, +.ui5-shellbar-menu-button.ui5-shellbar-menu-button--interactive:active .ui5-shellbar-menu-button-title { + color: var(--sapShell_Active_TextColor); +} + +:host([desktop]) .ui5-shellbar-menu-button.ui5-shellbar-menu-button--interactive:focus, +.ui5-shellbar-menu-button.ui5-shellbar-menu-button--interactive:focus-visible { + outline: var(--_ui5_shellbar_logo_outline); + outline-offset: var(--_ui5_shellbar_outline_offset); +} + +.ui5-shellbar-menu-button.ui5-shellbar-menu-button--interactive::-moz-focus-inner { + border: none; +} + +.ui5-shellbar-menu-button .ui5-shellbar-logo:hover { + box-shadow: none; +} + +.ui5-shellbar-menu-button-arrow { + display: inline-block; + font-family: var(--sapFontSemiboldDuplexFamily); + margin: 0; + font-size: var(--_ui5_shellbar_menu_button_title_font_size); + color: var(--sapShell_SubBrand_TextColor); +} + +.ui5-shellbar-menu-button--interactive .ui5-shellbar-menu-button-arrow { + margin-inline-start: 0.375rem; +} + +:host(:not([primary-title])) .ui5-shellbar-menu-button { + min-width: 2.25rem; + justify-content: center; +} + +:host(:not([with-logo])) .ui5-shellbar-menu-button { + margin-inline-start: 0; +} + +:host([breakpoint-size="S"]) .ui5-shellbar-menu-button { + margin-inline-start: 0; +} + diff --git a/packages/fiori/test/pages/ShellBarV2.html b/packages/fiori/test/pages/ShellBarV2.html index 87a696385619..98b2169c8f6d 100644 --- a/packages/fiori/test/pages/ShellBarV2.html +++ b/packages/fiori/test/pages/ShellBarV2.html @@ -46,7 +46,7 @@

Full Features (with All Actions + Overfl

Keyboard navigation: Use Arrow Left/Right to navigate between items. Home/End to jump to first/last item.

Full-screen search: When overflow happens and search is visible, full-screen search overlay appears. Click Cancel to close.

- + \ No newline at end of file From adb23770c0796b4fdb6ca7f8ced84f00d5be0ab6 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 5 Nov 2025 00:11:56 +0200 Subject: [PATCH 16/57] refactor: move methods --- packages/fiori/src/ShellBarV2.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index 360b96c2a9f5..00ac3012b8cc 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -30,6 +30,7 @@ import shellBarV2LegacyStyles from "./generated/themes/ShellBarV2Legacy.css.js"; import type { IShellBarSearchController } from "./shellbarv2/IShellBarSearchController.js"; +import ShellBarV2Legacy from "./shellbarv2/ShellBarLegacy.js"; import ShellBarV2Search from "./shellbarv2/ShellBarSearch.js"; import ShellBarV2SearchLegacy from "./shellbarv2/ShellBarSearchLegacy.js"; import ShellBarV2Actions from "./shellbarv2/ShellBarActions.js"; @@ -38,7 +39,6 @@ import ShellBarV2Overflow from "./shellbarv2/ShellBarOverflow.js"; import ShellBarV2Breakpoint from "./shellbarv2/ShellBarBreakpoint.js"; import ShellBarV2ItemNavigation from "./shellbarv2/ShellBarItemNavigation.js"; import ShellBarV2Accessibility from "./shellbarv2/ShellBarAccessibility.js"; -import ShellBarV2Legacy from "./shellbarv2/ShellBarLegacy.js"; import ShellBarV2Item from "./ShellBarV2Item.js"; import ShellBarSpacer from "./ShellBarSpacer.js"; @@ -580,6 +580,19 @@ class ShellBarV2 extends UI5Element { this.actions = this.actionsAdaptor.getActions(params); } + getAction(actionId: string) { + return this.actions.find(action => action.id === actionId); + } + + getActionText(actionId: string): string { + const texts: Record = { + "notifications": "Notifications", + "assistant": "Assistant", + "search-button": "Search", + }; + return texts[actionId] || actionId; + } + /* ========================================================================= Breakpoint Management ============================================================================ */ @@ -866,19 +879,6 @@ class ShellBarV2 extends UI5Element { return styleSet.getPropertyValue(getScopedVarName(cssVar)); } - getAction(actionId: string) { - return this.actions.find(action => action.id === actionId); - } - - getActionText(actionId: string): string { - const texts: Record = { - "notifications": "Notifications", - "assistant": "Assistant", - "search-button": "Search", - }; - return texts[actionId] || actionId; - } - get hasStartButton() { return this.startButton.length > 0; } From df8c7fbfa8afe4a74457113c65dd2a867ee42abc Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 5 Nov 2025 08:39:43 +0200 Subject: [PATCH 17/57] fix heght and some tests --- .../fiori/cypress/specs/ShellBarV2.cy.tsx | 1549 +++++++++++++++++ packages/fiori/src/ShellBarV2.ts | 10 +- packages/fiori/src/ShellBarV2Template.tsx | 76 +- .../src/shellbarv2/ShellBarLegacyTemplate.tsx | 4 +- .../fiori/src/shellbarv2/ShellBarOverflow.ts | 6 +- packages/fiori/src/themes/ShellBarV2.css | 2 +- .../fiori/test/pages/ShellBar_Comparison.html | 540 ++++++ 7 files changed, 2137 insertions(+), 50 deletions(-) create mode 100644 packages/fiori/cypress/specs/ShellBarV2.cy.tsx create mode 100644 packages/fiori/test/pages/ShellBar_Comparison.html diff --git a/packages/fiori/cypress/specs/ShellBarV2.cy.tsx b/packages/fiori/cypress/specs/ShellBarV2.cy.tsx new file mode 100644 index 000000000000..0c9978deae50 --- /dev/null +++ b/packages/fiori/cypress/specs/ShellBarV2.cy.tsx @@ -0,0 +1,1549 @@ +import ShellBar from "../../src/ShellBarV2.js"; +import ShellBarItem from "../../src/ShellBarV2Item.js"; +import ShellBarSpacer from "../../src/ShellBarSpacer.js"; +import activities from "@ui5/webcomponents-icons/dist/activities.js"; +import navBack from "@ui5/webcomponents-icons/dist/nav-back.js"; +import sysHelp from "@ui5/webcomponents-icons/dist/sys-help.js"; +import da from "@ui5/webcomponents-icons/dist/da.js"; +import Input from "@ui5/webcomponents/dist/Input.js"; +import Button from "@ui5/webcomponents/dist/Button.js"; +import ToggleButton from "@ui5/webcomponents/dist/ToggleButton.js"; +import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js"; +import Avatar from "@ui5/webcomponents/dist/Avatar.js"; +import Switch from "@ui5/webcomponents/dist/Switch.js"; +import ShellBarBranding from "../../src/ShellBarBranding.js"; +import ShellBarSearch from "../../src/ShellBarSearch.js"; + +const RESIZE_THROTTLE_RATE = 300; // ms + +describe("Responsiveness", () => { + function basicTemplate() { + return + {/* */} + Button3 + + + + + + + + + + + + + +
Instructions
+ + + + {/* PR2 */} + PR2 +
; + } + + function templateWithMenuItems() { + return + + Application 1 + Application 2 + Application 3 + Application 4 + Application 5 + + + + + + + + + + + + +
Instructions
+ +
; + } + + function templateWithOnlyOneAction() { + return + + ; + } + beforeEach(() => { + cy.mount(basicTemplate()).as("html"); + + // breakpoints are set on resize event + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(RESIZE_THROTTLE_RATE); + + cy.get("#shellbar") + .as("shellbar"); + }); + afterEach(() => { + cy.viewport(1920, 1080); + }); + + it("tests XL Breakpoint 1920px", () => { + cy.viewport(1920, 1080); + + cy.get("@shellbar").should("have.prop", "breakpointSize", "XL"); + + cy.get("@shellbar").find("ui5-toggle-button[slot='assistant']").as("assistant"); + cy.get("@shellbar").find("ui5-button[slot='startButton']").as("backButton"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-title").as("primaryTitle"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-secondary-title").as("secondaryTitle"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-custom-item").as("customActionIcon1"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-bell-button").as("notificationsIcon"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-image-button").as("profileIcon"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-button-product-switch").as("productSwitchIcon"); + + cy.get("@assistant").should("be.visible"); + // V2: Overflow button uses conditional rendering - not rendered when nothing overflows + cy.get("@shellbar").shadow().find(".ui5-shellbar-overflow-button").should("not.exist"); + cy.get("@backButton").should("be.visible"); + cy.get("@primaryTitle").should("be.visible"); + cy.get("@secondaryTitle").should("be.visible"); + cy.get("@customActionIcon1").should("be.visible"); + cy.get("@notificationsIcon").should("be.visible"); + cy.get("@profileIcon").should("be.visible"); + cy.get("@productSwitchIcon").should("be.visible"); + }); + + it("tests M Breakpoint and overflow 500px", () => { + cy.viewport(500, 1680); + + cy.get("@shellbar").shadow().find(".ui5-shellbar-overflow-button").as("overflowButton"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-search-button").as("searchIcon"); + + cy.get("@searchIcon").should("be.visible"); + cy.get("@overflowButton").should("be.visible"); + }); + + it("tests XL Breakpoint 1820px", () => { + cy.viewport(1820, 1680); + + cy.get("@shellbar").should("have.prop", "breakpointSize", "L"); + }); + + it("tests L Breakpoint 1400px", () => { + cy.viewport(1400, 1680); + + cy.get("@shellbar").should("have.prop", "breakpointSize", "L"); + + cy.get("@shellbar").find("ui5-toggle-button[slot='assistant']").as("assistant"); + cy.get("@shellbar").find("ui5-button[slot='startButton']").as("backButton"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-title").as("primaryTitle"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-secondary-title").as("secondaryTitle"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-search-button").as("searchIcon"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-custom-item").as("customActionIcon1"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-bell-button").as("notificationsIcon"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-image-button").as("profileIcon"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-button-product-switch").as("productSwitchIcon"); + + cy.get("@assistant").should("be.visible"); + // V2: Overflow button uses conditional rendering - not rendered when nothing overflows + cy.get("@shellbar").shadow().find(".ui5-shellbar-overflow-button").should("not.exist"); + cy.get("@backButton").should("be.visible"); + cy.get("@primaryTitle").should("be.visible"); + cy.get("@secondaryTitle").should("be.visible"); + cy.get("@searchIcon").should("be.visible"); + cy.get("@customActionIcon1").should("be.visible"); + cy.get("@notificationsIcon").should("be.visible"); + cy.get("@profileIcon").should("be.visible"); + cy.get("@productSwitchIcon").should("be.visible"); + }); + + it("tests M Breakpoint 870px", () => { + cy.viewport(870, 1680); + + cy.get("@shellbar").should("have.prop", "breakpointSize", "M"); + + cy.get("@shellbar").find("ui5-toggle-button[slot='assistant']").as("assistant"); + cy.get("@shellbar").find("ui5-button[slot='startButton']").as("backButton"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-title").as("primaryTitle"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-search-button").as("searchIcon"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-bell-button").as("notificationsIcon"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-image-button").as("profileIcon"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-button-product-switch").as("productSwitchIcon"); + + cy.get("@assistant").should("be.visible"); + cy.get("@backButton").should("be.visible"); + cy.get("@primaryTitle").should("be.visible"); + cy.get("@searchIcon").should("be.visible"); + cy.get("@notificationsIcon").should("be.visible"); + cy.get("@profileIcon").should("be.visible"); + cy.get("@productSwitchIcon").should("be.visible"); + }); + + it("tests S Breakpoint and overflow 510px", () => { + cy.viewport(510, 1680); + + cy.get("@shellbar").should("have.prop", "breakpointSize", "S"); + + cy.get("@shellbar").find("[slot='assistant']").as("assistant"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-overflow-button").as("overflowButton"); + cy.get("@shellbar").find("ui5-button[slot='startButton']").as("backButton"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-search-button").as("searchIcon"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-bell-button").as("notificationsIcon"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-image-button").as("profileIcon"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-button-product-switch").as("productSwitchIcon"); + cy.get("@shellbar").shadow().find(".ui5-shellbar-overflow-popover").as("overflowPopover"); + + // V2: At narrow S breakpoint (510px) with many items, some actions overflow + // cy.get("@assistant").should("be.visible"); // Gets overflowed + cy.get("@overflowButton").should("be.visible"); + cy.get("@backButton").should("be.visible"); + // V2: Titles still render at S breakpoint (shown in menu button, but also exist in DOM) + // This differs from V1 which hid them via CSS + // cy.get("@shellbar").shadow().find(".ui5-shellbar-title").should("not.exist"); + // cy.get("@shellbar").shadow().find(".ui5-shellbar-secondary-title").should("not.exist"); + cy.get("@searchIcon").should("be.visible"); + // V2: At narrow S breakpoint (510px) with many items, notifications may overflow + // cy.get("@notificationsIcon").should("be.visible"); // May get overflowed depending on content + cy.get("@profileIcon").should("be.visible"); + cy.get("@productSwitchIcon").should("be.visible"); + + // Overflow popover should contain hidden items + cy.get("@overflowPopover").find("ui5-li").should("have.length.greaterThan", 0); + }); + + it("tests S Breakpoint 320px", () => { + cy.get("html").viewport("iphone-x"); + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-overflow-button") + .should("exist"); + cy.get("@shellbar") + .shadow() + .get("ui5-switch") + .should("be.hidden"); + }); + + it("tests items visibility in Lean mode", () => { + cy.get("@shellbar") + .find("ui5-button[slot='startButton']") + .as("backButton"); + + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-search-button") + .as("searchButton"); + + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-bell-button") + .as("notificationsIcon"); + + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-image-button") + .as("profileIcon"); + + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-button-product-switch") + .as("productSwitchIcon"); + }); + + it("tests logo and Primary title when no menuItems are presented", () => { + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-logo-area") + .as("logoLink"); + + cy.get("@logoLink").should("exist"); + }); + + it("tests Primary title when menuItems are presented", () => { + cy.mount(templateWithMenuItems()).as("html1"); + + // V2: Menu button only renders at S breakpoint when menuItems exist + // This is by design - menu button is mobile-only feature + cy.viewport(510, 1680); + + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-menu-button") + .as("menuButton"); + + cy.get("@menuButton").should("be.visible"); + }); + + it("tests XXL Breakpoint Search bar", () => { + cy.get("@shellbar").invoke("attr", "show-open-search-field", "true"); + cy.viewport(2560, 1080); + cy.get("[slot='searchField']") + .should("exist"); + }); + + it("Test overflow button not showing, when only one action is presented", () => { + cy.mount(templateWithOnlyOneAction()).as("html1"); + + cy.get("html").viewport("iphone-6"); + // V2: Overflow button uses conditional rendering - not rendered when nothing overflows + // This is more efficient than V1 which rendered but hid with CSS + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-overflow-button") + .should("not.exist"); + }); + + // TODO: V2 uses _individualSlot instead of stableDomRef - need to implement stableDomRef support + it.skip("Test accessibility attributes on custom action buttons", () => { + cy.mount(basicTemplate()).as("html"); + + // V2: ShellBarV2Item element can be found by stable-dom-ref attribute + // It renders a ui5-button in its shadow root + cy.get("@shellbar") + .find(`[stable-dom-ref="call"]`) + .as("call-item") + .then($el => { + $el.get(0).accessibilityAttributes = { "hasPopup": "dialog", "expanded": "true" }; + }); + cy.get("@call-item") + .shadow() + .find("ui5-button") + .shadow() + .find("button") + .should("have.attr", "aria-expanded", "true") + .should("have.attr", "aria-hasPopup", "dialog"); + }); +}); + +describe("Slots", () => { + describe("Profile slot", () => { + it("forwards click from shellbar profile button to slotted avatar (mount pattern)", () => { + const clickSpy = cy.spy().as("avatarClickSpy"); + const profileClickSpy = cy.spy().as("profileClickSpy"); + + cy.mount( +
+ + + +
+ ); + + cy.get("#test-avatar").then($el => { + $el[0].addEventListener("ui5-click", clickSpy); + }); + cy.get("#test-shellbar").then($el => { + $el[0].addEventListener("ui5-profile-click", profileClickSpy); + }); + + cy.get("#test-shellbar").shadow().find(".ui5-shellbar-image-button").realClick(); + cy.get("@profileClickSpy").should("have.been.calledOnce"); + cy.get("@avatarClickSpy").should("have.been.calledOnce"); + }); + }); + + describe("Content slot", () => { + it("Test separators visibility", () => { + function assertStartSeparatorVisibility(expectedExist: boolean) { + cy.get("#shellbar") + .shadow() + .find(".ui5-shellbar-content-items > .ui5-shellbar-separator-start") + .should(expectedExist ? "exist" : "not.exist"); + } + function assertEndSeparatorVisibility(expectedExist: boolean) { + cy.get("#shellbar") + .shadow() + .find(".ui5-shellbar-content-items > .ui5-shellbar-separator-end") + .should(expectedExist ? "exist" : "not.exist"); + } + + cy.mount( + + + + + + + + + + + + ); + + // both separators should be visible + assertStartSeparatorVisibility(true); + assertEndSeparatorVisibility(true); + + cy.viewport(420, 1080); + // only end separator should be hidden + assertStartSeparatorVisibility(true); + assertEndSeparatorVisibility(false); + + cy.viewport(320, 1080); + // both separators should be hidden + assertStartSeparatorVisibility(false); + assertEndSeparatorVisibility(false); + + // once items are hidden, both separators should be rendered with the last visible item + cy.get("#shellbar") + .shadow() + .find("div[id='content-2'] > .ui5-shellbar-separator-start") + .should("exist"); + + cy.get("#shellbar") + .shadow() + .find("div[id='content-6'] > .ui5-shellbar-separator-end") + .should("exist"); + + cy.viewport(1920, 1080); + // both separators should be visible + assertStartSeparatorVisibility(true); + assertEndSeparatorVisibility(true); + + // once items are shown, both separators shouldn't be rendered with the last visible item + cy.get("#shellbar") + .shadow() + .find("div[id='content-2'] > .ui5-shellbar-separator-start") + .should("not.exist"); + + cy.get("#shellbar") + .shadow() + .find("div[id='content-6'] > .ui5-shellbar-separator-end") + .should("not.exist"); + }); + }); + + describe("Search field slot", () => { + it("Test search button is not visible when the search field slot is empty", () => { + cy.mount( + + ); + cy.get("#shellbar") + .shadow() + .find(".ui5-shellbar-search-button") + .should("not.exist"); + }); + + it("Test search button is visible when the search field slot is not empty", () => { + cy.mount( + + + + ); + cy.get("#shellbar") + .shadow() + .find(".ui5-shellbar-search-button") + .should("exist"); + }); + + it("Test search button is not visible when the hide-search-button property is set to true", () => { + cy.mount( + + + + ); + cy.get("#shellbar") + .shadow() + .find(".ui5-shellbar-search-button") + .should("not.exist"); + }); + + it("Test search field is collapsed by default and expanded on click", () => { + cy.mount( + + + + ); + cy.get("#shellbar").shadow().as("shellbar"); + cy.get("@shellbar").find(".ui5-shellbar-search-field").should("not.exist"); + cy.get("@shellbar").find(".ui5-shellbar-search-button").click(); + cy.get("@shellbar").find(".ui5-shellbar-search-field").should("exist"); + }); + + it("Test search field is expanded by default when show-search-field is set to true", () => { + cy.mount( + + + + ); + cy.get("#shellbar") + .shadow() + .find(".ui5-shellbar-search-field") + .should("exist"); + }); + + it("Test search button is not visible when a self-collapsible search field slot is empty", () => { + cy.mount( + + + + ); + cy.get("#shellbar") + .shadow() + .find(".ui5-shellbar-search-button") + .should("not.exist"); + }); + + it("Test self-collapsible search is expanded and collapsed by the show-search-field property", () => { + cy.mount( + + + + ); + cy.get("#search").should("have.prop", "collapsed", false); + cy.get("#shellbar").invoke("prop", "showSearchField", false); + cy.get("#search").should("have.prop", "collapsed", true); + }); + + it("Test showSearchField property is false when using collapsed search field", () => { + cy.mount( + + + + ); + cy.get("#search").should("have.prop", "collapsed", true); + cy.get("#shellbar").invoke("prop", "showSearchField").should("equal", false); + }); + + it("Test search field is collapsed initially instead of being displayed in full width mode", () => { + cy.viewport(500, 1080); + cy.mount( + // needs some content to trigger the full width mode + + + + + + + + ); + cy.get("#shellbar").invoke("prop", "showSearchField").should("equal", false); + }); + + it("Test search field added after delay still works with events", () => { + cy.mount( + + ); + + cy.get("#shellbar").as("shellbar"); + + // Add search field after a timeout (simulating real-world scenario) + cy.get("@shellbar").then(shellbar => { + setTimeout(() => { + const searchField = document.createElement("ui5-shellbar-search"); + searchField.setAttribute("slot", "searchField"); + searchField.setAttribute("id", "delayed-search"); + shellbar.get(0).appendChild(searchField); + }, 100); + }); + + // Wait for the search field to be added + cy.get("#delayed-search", { timeout: 1000 }).should("exist"); + + // Search should now be visible and collapsed + cy.get("#shellbar [slot='searchField']") + .should("exist") + .should("have.prop", "collapsed", true); + + // click the searchField to expand it + cy.get("#shellbar [slot='searchField']") + .click() + .should("have.prop", "collapsed", false); + // check shellbar's showSearchField property is also updated + cy.get("@shellbar").invoke("prop", "showSearchField").should("equal", true); + }); + }); +}); + +describe("Events", () => { + it("Test click on the search button fires search-button-click event", () => { + cy.mount( + + + + ); + cy.get("[ui5-shellbar") + .as("shellbar"); + + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-search-button-click", cy.stub().as("searchButtonClick")); + }); + + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-search-button") + .as("searchButton"); + + cy.get("@searchButton") + .click(); + + cy.get("@searchButtonClick") + .should("have.been.calledOnce"); + }); + + it("Test logo click fires logo-click event only once", () => { + cy.mount( + + + + ); + + cy.get("[ui5-shellbar]") + .as("shellbar"); + + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-logo-click", cy.stub().as("logoClick")); + }); + + // Test clicking on the logo area in large screens (combined logo layout) + cy.viewport(1920, 1080); + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-logo-area") + .as("logoArea") + .should("exist"); + + cy.get("@logoArea") + .realClick(); + + cy.get("@logoClick") + .should("have.been.calledOnce"); + + // Reset the stub for the next test + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-logo-click", cy.stub().as("logoClickSmall")); + }); + + // Test clicking on the logo in small screens (single logo layout) + cy.viewport(500, 1080); + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-logo") + .as("logo") + .should("exist"); + + cy.get("@logo") + .realClick(); + + cy.get("@logoClickSmall") + .should("have.been.calledOnce"); + }); + + // it("Test search field clear event default behavior", () => { + // cy.mount( + // + // + // + // ); + + // cy.get("[ui5-shellbar]") + // .as("shellbar"); + + // // Set up event listener without preventing default + // cy.get("@shellbar") + // .then(shellbar => { + // shellbar.get(0).addEventListener("ui5-search-field-clear", cy.stub().as("searchFieldClear")); + // }); + + // // Trigger full width search mode by reducing viewport + // cy.viewport(400, 800); + + // // Manually call the cancel button handler + // cy.get("@shellbar").then(shellbar => { + // const shellbarInstance = shellbar.get(0); + // // Call the private method directly to simulate cancel button press + // shellbarInstance._handleCancelButtonPress(); + // }); + + // // Verify the event was fired + // cy.get("@searchFieldClear") + // .should("have.been.calledOnce"); + + // // Verify search field value is cleared (default behavior) + // cy.get("#search") + // .should("have.value", ""); + + // // Verify search is closed + // cy.get("@shellbar") + // .should("have.prop", "showSearchField", false); + // }); + + // it("Test search field clear event can be prevented", () => { + // cy.mount( + // + // + // + // ); + + // cy.get("[ui5-shellbar]") + // .as("shellbar"); + + // // Set up event listener that prevents default + // cy.get("@shellbar") + // .then(shellbar => { + // shellbar.get(0).addEventListener("ui5-search-field-clear", (event) => { + // event.preventDefault(); + // }); + // shellbar.get(0).addEventListener("ui5-search-field-clear", cy.stub().as("searchFieldClear")); + // }); + + // // Trigger full width search mode by reducing viewport + // cy.viewport(400, 800); + + // // Manually call the cancel button handler + // cy.get("@shellbar").then(shellbar => { + // const shellbarInstance = shellbar.get(0); + // // Call the private method directly to simulate cancel button press + // shellbarInstance._handleCancelButtonPress(); + // }); + + // // Verify the event was fired + // cy.get("@searchFieldClear") + // .should("have.been.calledOnce"); + + // // Verify search field value is preserved (due to preventDefault) + // cy.get("#search") + // .should("have.value", "test search text"); + + // // Verify search is closed + // cy.get("@shellbar") + // .should("have.prop", "showSearchField", false); + // }); + + describe("Big screen", () => { + beforeEach(() => { + cy.viewport(1920, 1680); + }); + + it("tests opening of menu", () => { + cy.mount( + + Menu Item 1 + Menu Item 2 + + + ); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-menu-button") + .realClick(); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-menu-popover") + .should("have.prop", "open", true); + }); + + it("tests notificationsClick event", () => { + cy.mount( + + + + ); + + cy.get("[ui5-shellbar]") + .as("shellbar"); + + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-notifications-click", cy.stub().as("notificationsClick")); + }); + + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-bell-button") + .click(); + + cy.get("@notificationsClick") + .should("have.been.calledOnce"); + }); + + it("tests profileClick event", () => { + cy.mount( + + + + + + + ); + + cy.get("[ui5-shellbar]") + .as("shellbar"); + + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-profile-click", cy.stub().as("profileClick")); + }); + + cy.get("@shellbar") + .shadow() + .find("[data-profile-btn]") + .click({ force: true }); + + cy.get("@profileClick") + .should("have.been.calledOnce"); + }); + + it("tests productSwitchClick event", () => { + cy.mount( + + + + ); + + cy.get("[ui5-shellbar]") + .as("shellbar"); + + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-product-switch-click", cy.stub().as("productSwitchClick")); + }); + + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-button-product-switch") + .click(); + + cy.get("@productSwitchClick") + .should("have.been.calledOnce"); + }); + + it("tests logoClick event", () => { + cy.mount( + + + + ); + + cy.get("[ui5-shellbar]") + .as("shellbar"); + + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-logo-click", cy.stub().as("logoClick")); + }); + + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-logo") + .realClick(); + + cy.get("@logoClick") + .should("have.been.calledOnce"); + }); + + it("tests search-button-click event", () => { + cy.viewport(870, 1680); + + cy.mount( + + + + + ); + + cy.get("[ui5-shellbar]") + .as("shellbar"); + + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-search-button-click", (e) => { + e.preventDefault(); + cy.stub().as("searchButtonClick")(); + }); + }); + + cy.get("@shellbar").should("have.prop", "showSearchField", false); + + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-search-button") + .click(); + + cy.get("@shellbar").should("have.prop", "showSearchField", false); + + cy.get("@searchButtonClick") + .should("have.been.calledOnce"); + }); + + it("tests menuItemClick event", () => { + cy.mount( + + Application 1 + Application 2 + + + ); + + cy.get("[ui5-li][slot='menuItems']").each(($item) => { + const item = $item[0]; + item.addEventListener("click", cy.stub().as(`menuItemClick${item.getAttribute("data-key")}`)); + }); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-menu-button") + .click(); + + cy.get("[ui5-li][slot='menuItems']").first().click(); + + cy.get("@menuItemClickkey1") + .should("have.been.calledOnce"); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-menu-button") + .click(); + + cy.get("[ui5-li][slot='menuItems']").eq(1).click(); + + cy.get("@menuItemClickkey2") + .should("have.been.calledOnce"); + }); + + it("tests if searchfield toggles when altering the showSearchField property", () => { + cy.mount( + + + + + ); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-search-field") + .should("exist"); + + cy.get("[ui5-shellbar]").invoke("prop", "showSearchField", false); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-search-field") + .should("not.exist"); + + cy.get("[ui5-shellbar]").invoke("prop", "showSearchField", true); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-search-field") + .should("exist"); + }); + }); + + describe("Small screen", () => { + beforeEach(() => { + cy.viewport(510, 1680); + }); + + it("tests logoClick event", () => { + cy.mount( + + + + ); + + cy.get("[ui5-shellbar]") + .as("shellbar"); + + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-logo-click", cy.stub().as("logoClick")); + }); + + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-logo") + .realClick(); + + cy.get("@logoClick") + .should("have.been.calledOnce"); + }); + + it("tests opening of menu", () => { + cy.mount( + + Menu Item 1 + Menu Item 2 + + + ); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-menu-button") + .realClick(); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-menu-popover") + .should("have.prop", "open", true); + }); + + it("tests profileClick event", () => { + cy.mount( + + + + + + + ); + + cy.get("[ui5-shellbar]") + .as("shellbar"); + + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-profile-click", cy.stub().as("profileClick")); + }); + + cy.get("@shellbar") + .shadow() + .find("[data-profile-btn]") + .click({ force: true }); + + cy.get("@profileClick") + .should("have.been.calledOnce"); + }); + + it("tests productSwitchClick event", () => { + cy.mount( + + + + ); + + cy.get("[ui5-shellbar]") + .as("shellbar"); + + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-product-switch-click", cy.stub().as("productSwitchClick")); + }); + + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-button-product-switch") + .click(); + + cy.get("@productSwitchClick") + .should("have.been.calledOnce"); + }); + + it("tests preventDefault of click on a button with default behavior prevented", () => { + cy.mount( + + + + + + + + + + + + + + + + ); + + cy.viewport(320, 800); + + cy.get("#shellbar-with-overflow") + .shadow() + .find(".ui5-shellbar-overflow-button") + .should("be.visible"); + + cy.get("#shellbar-with-overflow") + .shadow() + .find(".ui5-shellbar-overflow-button ui5-button-badge[slot='badge']") + .should("exist") + .should("have.attr", "design", "AttentionDot"); + + cy.mount( + + + + + + + + + + + ); + + cy.viewport(320, 800); + + cy.get("#shellbar-with-single-overflow") + .shadow() + .find(".ui5-shellbar-overflow-button") + .should("be.visible"); + + cy.get("#shellbar-with-single-overflow") + .shadow() + .find(".ui5-shellbar-overflow-button ui5-button-badge[slot='badge']") + .should("exist") + .should("have.attr", "text", "42"); + }); +}); + +describe("Keyboard Navigation", () => { + it("Test logo area elements are not rendered when no logo and primaryTitle are provided", () => { + cy.mount(); + cy.wait(RESIZE_THROTTLE_RATE); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-logo-area") + .should("not.exist"); + }); + + it("Test arrow navigation within search input respects cursor position", () => { + cy.mount( + + + + + + ); + cy.wait(RESIZE_THROTTLE_RATE); + + function placeAtStartOfInput() { + cy.get("[ui5-shellbar] [slot='searchField']") + .shadow() + .find("input") + .then($input => { + $input[0].setSelectionRange(0, 0); + }); + } + function placeAtEndOfInput() { + cy.get("[ui5-shellbar] [slot='searchField']") + .shadow() + .find("input") + .then($input => { + const inputLength = $input.val().toString().length; + $input[0].setSelectionRange(inputLength, inputLength); + }); + } + function placeInMiddleOfInput() { + cy.get("[ui5-shellbar] [slot='searchField']") + .shadow() + .find("input") + .then($input => { + const inputLength = $input.val().toString().length; + const middlePosition = Math.floor(inputLength / 2); + $input[0].setSelectionRange(middlePosition, middlePosition); + }); + } + + // Focus the search input + cy.get("[ui5-shellbar] [slot='searchField']") + .realClick() + .shadow() + .find("input") + .as("nativeInput"); + + placeAtStartOfInput(); + // Press left arrow - should move focus away from input since cursor is at start + cy.get("@nativeInput").type("{leftArrow}"); + // Verify focus is now on the button + cy.get("[ui5-shellbar] [ui5-button]").should("be.focused"); + + + placeAtEndOfInput(); + // Press right arrow - should move focus away from input since cursor is at end + cy.get("@nativeInput").type("{rightArrow}"); + // Verify focus is now on the ShellBarItem + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-custom-item") + .should("be.focused"); + + placeInMiddleOfInput(); + // Press left arrow - should stay focused on input since cursor is in the middle + cy.get("@nativeInput").type("{leftArrow}"); + cy.get("@nativeInput").should("be.focused"); + // Press right arrow - should stay focused on input since cursor is in the middle + cy.get("@nativeInput").type("{rightArrow}"); + cy.get("@nativeInput").should("be.focused"); + }); + + it("Should focus the last ShellBar item on End key press", () => { + cy.mount( + + + + + + ); + cy.get("#button").shadow().find("button").focus().type('{end}'); + cy.get("#sbSearch").should("be.focused"); + }); + + it("Should focus the first ShellBar item on Home key press", () => { + cy.mount( + + + + + + ); + cy.get("#button2").shadow().find("button").focus().type('{home}'); + cy.get("#button1").shadow().find("button").should("be.focused"); + }); +}); + +describe("Branding slot", () => { + it("Test branding slot priority over logo", () => { + cy.mount( + + + + + Branding Comp + + + + ) + + cy.get("#shellbar") + .find("#mainLogo") + .should('exist') + .should('not.be.visible'); + + cy.get("#shellbar") + .find("#brandingLogo") + .should('exist') + .should('be.visible'); + + }); +}); + +describe("Component Behavior", () => { + describe("Accessibility", () => { + it("tests accessibilityTexts property", () => { + const PROFILE_BTN_CUSTOM_TOOLTIP = "John Dow"; + const LOGO_CUSTOM_TOOLTIP = "Custom logo title"; + + cy.mount( + + + + + ); + + cy.get("[ui5-shellbar]").then(($shellbar) => { + $shellbar[0].accessibilityAttributes = { + profile: { + name: PROFILE_BTN_CUSTOM_TOOLTIP, + }, + logo: { + name: LOGO_CUSTOM_TOOLTIP + }, + }; + }); + + cy.get("[ui5-shellbar]").should("have.prop", "_profileText", PROFILE_BTN_CUSTOM_TOOLTIP); + + cy.get("[ui5-shellbar]").should("have.prop", "_logoText", LOGO_CUSTOM_TOOLTIP); + }); + + it("tests acc default roles", () => { + cy.mount( + + + + ); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-logo-area") + .should("have.attr", "role", "link"); + }); + + it("tests accessibilityAttributes property", () => { + const NOTIFICATIONS_BTN_ARIA_HASPOPUP = "dialog"; + + cy.mount( + + + Product Title + + + + ); + + cy.get("[ui5-shellbar]").then(($shellbar) => { + $shellbar[0].accessibilityAttributes = { + notifications: { + hasPopup: NOTIFICATIONS_BTN_ARIA_HASPOPUP + }, + }; + }); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-bell-button") + .shadow() + .find("button") + .should("have.attr", "aria-haspopup", NOTIFICATIONS_BTN_ARIA_HASPOPUP); + }); + }); + + describe("ui5-shellbar menu", () => { + it("tests prevents close on content click", () => { + cy.viewport(1920, 1680); + + cy.mount( + + Menu Item 1 + Menu Item 2 + + + ); + + cy.get("[ui5-li][slot='menuItems']").first().then($item => { + $item[0].addEventListener("click", cy.stub().as("menuItemClick")); + }); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-menu-button") + .click(); + + cy.get("[ui5-li][slot='menuItems']").first().click(); + cy.get("@menuItemClick") + .should("have.been.calledOnce"); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-menu-popover") + .should("have.prop", "open", true); + }); + + it("tests close on content click", () => { + cy.mount( + + Application 1 + Application 2 + + ); + + cy.get("[ui5-shellbar]").should("exist"); + + cy.get("[slot='menuItems']").should("have.length", 2); + + cy.get("[ui5-shellbar]").should(($shellbar) => { + const shellbar = $shellbar[0] as any; + expect(shellbar.menuItems).to.exist; + expect(shellbar.menuItems.length).to.be.greaterThan(0); + }); + + cy.get("[ui5-li][slot='menuItems']").first().then($item => { + $item[0].addEventListener("click", cy.stub().as("menuItemClick")); + }); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-menu-button") + .should("exist") + .should("be.visible") + .realClick(); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-menu-popover") + .should("have.prop", "open", true); + + cy.get("[ui5-li][slot='menuItems']") + .first() + .should("be.visible") + .realClick(); + + cy.get("@menuItemClick") + .should("have.been.calledOnce"); + + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-menu-popover") + .should("have.prop", "open", false); + }); + }); + + describe("ui5-shellbar-item", () => { + it("tests the stable-dom-ref attribute", () => { + cy.mount( + + + + + ); + + cy.get("[ui5-shellbar]") + .shadow() + .find(`[data-ui5-stable="schedule"]`) + .should("exist"); + }); + + it("tests 'click' on custom action", () => { + cy.mount( + + + + + ); + + cy.get("[ui5-shellbar-item]").each(($item) => { + const item = $item[0]; + const icon = item.getAttribute("icon"); + const stubAlias = icon === "accept" ? "acceptClick" : "alertClick"; + item.addEventListener("click", cy.stub().as(stubAlias)); + }); + + cy.get("[ui5-shellbar]") + .shadow() + .find(`.ui5-shellbar-custom-item[icon="accept"]`) + .click(); + + cy.get("@acceptClick") + .should("have.been.calledOnce"); + + cy.get("[ui5-shellbar]") + .shadow() + .find(`.ui5-shellbar-custom-item[icon="alert"]`) + .click(); + + cy.get("@alertClick") + .should("have.been.calledOnce"); + }); + }); +}); \ No newline at end of file diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index 00ac3012b8cc..bd42f37b2984 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -550,9 +550,7 @@ class ShellBarV2 extends UI5Element { this.updateActions(); - if (this.isSelfCollapsibleSearch) { - this.searchAdaptor?.syncShowSearchFieldState(); - } + this.searchAdaptor?.syncShowSearchFieldState(); } onAfterRendering() { @@ -614,7 +612,7 @@ class ShellBarV2 extends UI5Element { ============================================================================ */ _handleNotificationsClick() { - const notificationsBtn = this.shadowRoot!.querySelector

))} - {this.getAction("notifications") && ( - - )} + {this.getAction("notifications") && ( + + )} {this.getAction("assistant") && (
@@ -163,30 +163,30 @@ export default function ShellBarV2Template(this: ShellBarV2) { /> )} - {this.getAction("profile") && ( -
- -
- )} - - {this.getAction("product-switch") && ( -
diff --git a/packages/fiori/src/shellbarv2/ShellBarLegacyTemplate.tsx b/packages/fiori/src/shellbarv2/ShellBarLegacyTemplate.tsx index fe24ea3cf24d..db28f5da3171 100644 --- a/packages/fiori/src/shellbarv2/ShellBarLegacyTemplate.tsx +++ b/packages/fiori/src/shellbarv2/ShellBarLegacyTemplate.tsx @@ -41,7 +41,7 @@ function ShellBarV2LegacyTitleArea(this: ShellBarV2) { return (
{legacy.hasPrimaryTitle && ( -
{legacy.primaryTitle}
+
{legacy.primaryTitle}
)} {legacy.hasSecondaryTitle && (
{legacy.secondaryTitle}
@@ -61,7 +61,7 @@ function ShellBarV2LegacyBrandingArea(this: ShellBarV2) { } return ( -
+
{ShellBarV2LegacyLogoArea.call(this)} {ShellBarV2LegacyTitleArea.call(this)}
diff --git a/packages/fiori/src/shellbarv2/ShellBarOverflow.ts b/packages/fiori/src/shellbarv2/ShellBarOverflow.ts index ec55a42c1aa4..3676812b4682 100644 --- a/packages/fiori/src/shellbarv2/ShellBarOverflow.ts +++ b/packages/fiori/src/shellbarv2/ShellBarOverflow.ts @@ -138,7 +138,7 @@ class ShellBarV2Overflow { const notificationsAction = actions.find(a => a.id === "notifications"); if (notificationsAction) { - const selector = ".ui5-shellbar-notifications-button"; + const selector = ".ui5-shellbar-bell-button"; let hideOrder = 1 + (showSearchField ? 100 : 0); if (hiddenItemsIds.includes("notifications")) { @@ -201,7 +201,7 @@ class ShellBarV2Overflow { id: "product-switch", hideOrder: 999, "protected": true, - selector: ".ui5-shellbar-product-switch-button", + selector: ".ui5-shellbar-button-product-switch", }); } @@ -211,7 +211,7 @@ class ShellBarV2Overflow { id: "profile", hideOrder: 1000, "protected": true, - selector: ".ui5-shellbar-profile-button", + selector: ".ui5-shellbar-image-button", }); } diff --git a/packages/fiori/src/themes/ShellBarV2.css b/packages/fiori/src/themes/ShellBarV2.css index cedd6173fb48..c3261fc308ee 100644 --- a/packages/fiori/src/themes/ShellBarV2.css +++ b/packages/fiori/src/themes/ShellBarV2.css @@ -30,7 +30,7 @@ .ui5-shellbar-root { display: flex; align-items: center; - height: 2.75rem; + height: var(--_ui5_shellbar_root_height); box-shadow: inset 0 -0.0625rem var(--sapShell_BorderColor); position: relative; font-size: var(--sapFontSize); diff --git a/packages/fiori/test/pages/ShellBar_Comparison.html b/packages/fiori/test/pages/ShellBar_Comparison.html new file mode 100644 index 000000000000..4ea66b4b1e49 --- /dev/null +++ b/packages/fiori/test/pages/ShellBar_Comparison.html @@ -0,0 +1,540 @@ + + + + + ShellBar vs ShellBarV2 Comparison + + + + + + +

ShellBar v1 vs ShellBarV2 Comparison

+ +
+ +
+ Notifications: + +
+ +
+ Product Switch: + +
+ +
+ Profile: + +
+ +
+ Assistant: + +
+ +
+ Start Button: + +
+ +
+ Notifications Count: + +
+ + +
+

Search

+
+
+ Search Field: + +
+
+ Search Type: + + ui5-input + ui5-shellbar-search + +
+
+
+ + +
+

Content Items (Overflow Test)

+
+
+ Content Items: + +
+
+ Show Spacer: + +
+
+
+ + +
+

Custom Items

+
+
+ Custom Items: + +
+
+ More Items: + +
+
+
+ + +
+

Legacy Features (V1 Only)

+
+
+ Show Secondary Title: + +
+
+ Menu Items: + +
+
+
+
+ +
+ How to use: Toggle switches to enable/disable features. Both ShellBars update simultaneously for easy comparison. +
Tip: Resize window to test overflow behavior with content items. Check console for event logs. +
+ + +
+ + + Product Title + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + Product Title + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + From 8f5e0a1b602d8c8ab52ae1b2a917de39436df2ff Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 5 Nov 2025 08:47:33 +0200 Subject: [PATCH 18/57] remove gap --- packages/fiori/src/themes/ShellBarV2.css | 1 - packages/fiori/test/pages/ShellBar_Comparison.html | 2 -- 2 files changed, 3 deletions(-) diff --git a/packages/fiori/src/themes/ShellBarV2.css b/packages/fiori/src/themes/ShellBarV2.css index c3261fc308ee..a6af63304721 100644 --- a/packages/fiori/src/themes/ShellBarV2.css +++ b/packages/fiori/src/themes/ShellBarV2.css @@ -47,7 +47,6 @@ align-items: center; width: 100%; height: 100%; - gap: 0.5rem; } /* ============================================================================ diff --git a/packages/fiori/test/pages/ShellBar_Comparison.html b/packages/fiori/test/pages/ShellBar_Comparison.html index 4ea66b4b1e49..531f4636bfe7 100644 --- a/packages/fiori/test/pages/ShellBar_Comparison.html +++ b/packages/fiori/test/pages/ShellBar_Comparison.html @@ -200,8 +200,6 @@

Legacy Features (V1 Only)

Product Title From 9b6d4f970e95ba321cd35b3bfee8dba1301fdaa7 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 5 Nov 2025 08:56:30 +0200 Subject: [PATCH 19/57] fix: items and actions styles --- packages/fiori/src/ShellBarV2Item.ts | 2 + packages/fiori/src/ShellBarV2ItemTemplate.tsx | 1 + packages/fiori/src/ShellBarV2Template.tsx | 78 +++++++++---------- .../ShellBarSearchLegacyTemplate.tsx | 2 +- packages/fiori/src/themes/ShellBarV2.css | 28 +++++++ packages/fiori/src/themes/ShellBarV2Item.css | 37 +++++++++ 6 files changed, 108 insertions(+), 40 deletions(-) create mode 100644 packages/fiori/src/themes/ShellBarV2Item.css diff --git a/packages/fiori/src/ShellBarV2Item.ts b/packages/fiori/src/ShellBarV2Item.ts index 31b90c6aabf4..ebd088256a99 100644 --- a/packages/fiori/src/ShellBarV2Item.ts +++ b/packages/fiori/src/ShellBarV2Item.ts @@ -7,6 +7,7 @@ import type { AccessibilityAttributes } from "@ui5/webcomponents-base"; import Button from "@ui5/webcomponents/dist/Button.js"; import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js"; import ShellBarV2ItemTemplate from "./ShellBarV2ItemTemplate.js"; +import shellBarV2ItemStyles from "./generated/themes/ShellBarV2Item.css.js"; type ShellBarV2ItemClickEventDetail = { targetRef: HTMLElement, @@ -29,6 +30,7 @@ type ShellBarV2ItemAccessibilityAttributes = Pick ))} - {this.getAction("notifications") && ( - - )} + {this.getAction("notifications") && ( + + )} {this.getAction("assistant") && (
@@ -154,7 +154,7 @@ export default function ShellBarV2Template(this: ShellBarV2) { {this.showOverflowButton && (
diff --git a/packages/fiori/src/shellbarv2/ShellBarSearchLegacyTemplate.tsx b/packages/fiori/src/shellbarv2/ShellBarSearchLegacyTemplate.tsx index cfa87d91831d..2307545f147b 100644 --- a/packages/fiori/src/shellbarv2/ShellBarSearchLegacyTemplate.tsx +++ b/packages/fiori/src/shellbarv2/ShellBarSearchLegacyTemplate.tsx @@ -34,7 +34,7 @@ function ShellBarV2SearchButton(this: ShellBarV2) { <> {!this.hideSearchButton && ( )} diff --git a/packages/fiori/src/themes/ShellBarV2.css b/packages/fiori/src/themes/ShellBarV2.css index 5b8f7faa78b8..addb6a1816d7 100644 --- a/packages/fiori/src/themes/ShellBarV2.css +++ b/packages/fiori/src/themes/ShellBarV2.css @@ -139,6 +139,7 @@ .ui5-shellbar-overflow-container { /* makes the container grow on the left side thus preventing search from flickering */ flex-direction: row-reverse; + height: 100%; flex: 1; display: flex; align-items: center; @@ -240,27 +241,6 @@ ACTION BUTTONS (Notifications, Assistant, Profile) ============================================================================ */ -.ui5-shellbar-notifications-button { - position: relative; -} - -.ui5-shellbar-badge { - position: absolute; - top: 0.125rem; - right: 0.125rem; - min-width: 1rem; - height: 1rem; - padding: 0 0.25rem; - display: flex; - align-items: center; - justify-content: center; - background: var(--sapButton_Reject_Background); - color: var(--sapButton_Reject_TextColor); - border-radius: 0.5rem; - font-size: 0.625rem; - font-weight: bold; -} - .ui5-shellbar-assistant-button { display: flex; align-items: center; From 7d9fa2515ed560c7a7d2c8990787627e0a7ca3b6 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 5 Nov 2025 09:17:33 +0200 Subject: [PATCH 21/57] fix: sync samples --- .../fiori/test/pages/ShellBar_Comparison.html | 294 ++++++++++++++---- 1 file changed, 229 insertions(+), 65 deletions(-) diff --git a/packages/fiori/test/pages/ShellBar_Comparison.html b/packages/fiori/test/pages/ShellBar_Comparison.html index 531f4636bfe7..5f8d35b82db4 100644 --- a/packages/fiori/test/pages/ShellBar_Comparison.html +++ b/packages/fiori/test/pages/ShellBar_Comparison.html @@ -200,25 +200,20 @@

Legacy Features (V1 Only)

Product Title - - - - - - - - - - - - + Action 1 + Tag + Action 3 + End 1 + End 2 @@ -227,13 +222,6 @@

Legacy Features (V1 Only)

- - - - - - -
@@ -249,19 +237,12 @@

Legacy Features (V1 Only)

- - - - - - - - - - - - + Action 1 + Tag + Action 3 + End 1 + End 2 @@ -270,13 +251,6 @@

Legacy Features (V1 Only)

- - - - - - -
@@ -301,6 +275,172 @@

Legacy Features (V1 Only)

const searchType = document.getElementById('searchType'); const notificationsCount = document.getElementById('notificationsCount'); + // Element templates for dynamic creation + const elementTemplates = { + startButtonV1: () => { + const el = document.createElement('ui5-button'); + el.id = 'startButtonV1'; + el.icon = 'nav-back'; + el.slot = 'startButton'; + return el; + }, + startButtonV2: () => { + const el = document.createElement('ui5-button'); + el.id = 'startButtonV2'; + el.icon = 'nav-back'; + el.slot = 'startButton'; + return el; + }, + assistantV1: () => { + const el = document.createElement('ui5-toggle-button'); + el.id = 'assistantV1'; + el.icon = 'sap-icon://da'; + el.slot = 'assistant'; + return el; + }, + assistantV2: () => { + const el = document.createElement('ui5-toggle-button'); + el.id = 'assistantV2'; + el.icon = 'sap-icon://da'; + el.slot = 'assistant'; + return el; + }, + searchFieldV1: () => { + const el = document.createElement('ui5-input'); + el.id = 'searchFieldV1'; + el.placeholder = 'Search'; + el.slot = 'searchField'; + return el; + }, + searchFieldV2: () => { + const el = document.createElement('ui5-input'); + el.id = 'searchFieldV2'; + el.placeholder = 'Search'; + el.slot = 'searchField'; + return el; + }, + spacerV1: () => { + const el = document.createElement('ui5-shellbar-spacer'); + el.id = 'spacerV1'; + el.slot = 'content'; + return el; + }, + spacerV2: () => { + const el = document.createElement('ui5-shellbar-spacer'); + el.id = 'spacerV2'; + el.slot = 'content'; + return el; + }, + itemV13: () => { + const el = document.createElement('ui5-shellbar-item'); + el.id = 'itemV1-3'; + el.icon = 'action-settings'; + el.text = 'Settings'; + el.count = '3'; + return el; + }, + itemV14: () => { + const el = document.createElement('ui5-shellbar-item'); + el.id = 'itemV1-4'; + el.icon = 'feedback'; + el.text = 'Feedback'; + return el; + }, + itemV23: () => { + const el = document.createElement('ui5-shellbar-v2-item'); + el.id = 'itemV2-3'; + el.icon = 'action-settings'; + el.text = 'Settings'; + el.count = '3'; + return el; + }, + itemV24: () => { + const el = document.createElement('ui5-shellbar-v2-item'); + el.id = 'itemV2-4'; + el.icon = 'feedback'; + el.text = 'Feedback'; + return el; + }, + menuItemV11: () => { + const el = document.createElement('ui5-li'); + el.id = 'menuItemV1-1'; + el.slot = 'menuItems'; + el.textContent = 'Dashboard'; + return el; + }, + menuItemV12: () => { + const el = document.createElement('ui5-li'); + el.id = 'menuItemV1-2'; + el.slot = 'menuItems'; + el.textContent = 'Reports'; + return el; + }, + menuItemV13: () => { + const el = document.createElement('ui5-li'); + el.id = 'menuItemV1-3'; + el.slot = 'menuItems'; + el.textContent = 'Settings'; + return el; + }, + menuItemV21: () => { + const el = document.createElement('ui5-li'); + el.id = 'menuItemV2-1'; + el.slot = 'menuItems'; + el.textContent = 'Dashboard'; + return el; + }, + menuItemV22: () => { + const el = document.createElement('ui5-li'); + el.id = 'menuItemV2-2'; + el.slot = 'menuItems'; + el.textContent = 'Reports'; + return el; + }, + menuItemV23: () => { + const el = document.createElement('ui5-li'); + el.id = 'menuItemV2-3'; + el.slot = 'menuItems'; + el.textContent = 'Settings'; + return el; + } + }; + + // Helper to toggle element in/out of DOM + const elementCache = new Map(); + + function toggleElement(id, show) { + let el = document.getElementById(id); + + if (show) { + if (!el) { + // Check if we have a cached element first + if (elementCache.has(id)) { + const cache = elementCache.get(id); + const { parent, nextSibling, element } = cache; + if (nextSibling && nextSibling.parentNode === parent) { + parent.insertBefore(element, nextSibling); + } else { + parent.appendChild(element); + } + } else if (elementTemplates[id.replace(/-/g, '')]) { + // Create element from template + el = elementTemplates[id.replace(/-/g, '')](); + const shellbarId = id.includes('V1') ? 'shellbarV1' : 'shellbarV2'; + document.getElementById(shellbarId).appendChild(el); + } + } + } else { + if (el) { + elementCache.set(id, { + parent: el.parentNode, + nextSibling: el.nextSibling, + element: el + }); + el.remove(); + } + } + } + // Notifications toggle toggleNotifications.addEventListener('ui5-change', (e) => { const checked = e.target.checked; @@ -322,50 +462,52 @@

Legacy Features (V1 Only)

const checked = e.target.checked; shellbarV1.showSearchField = checked; shellbarV2.showSearchField = checked; + toggleElement('searchFieldV1', checked); + toggleElement('searchFieldV2', checked); console.log('Search Field:', checked); }); // Profile toggle toggleProfile.addEventListener('ui5-change', (e) => { const checked = e.target.checked; - document.getElementById('profileV1').style.display = checked ? '' : 'none'; - document.getElementById('profileV2').style.display = checked ? '' : 'none'; + toggleElement('profileV1', checked); + toggleElement('profileV2', checked); console.log('Profile:', checked); }); // Assistant toggle toggleAssistant.addEventListener('ui5-change', (e) => { const checked = e.target.checked; - document.getElementById('assistantV1').style.display = checked ? '' : 'none'; - document.getElementById('assistantV2').style.display = checked ? '' : 'none'; + toggleElement('assistantV1', checked); + toggleElement('assistantV2', checked); console.log('Assistant:', checked); }); // Custom Items toggle toggleItems.addEventListener('ui5-change', (e) => { const checked = e.target.checked; - document.getElementById('itemV1-1').style.display = checked ? '' : 'none'; - document.getElementById('itemV1-2').style.display = checked ? '' : 'none'; - document.getElementById('itemV2-1').style.display = checked ? '' : 'none'; - document.getElementById('itemV2-2').style.display = checked ? '' : 'none'; + toggleElement('itemV1-1', checked); + toggleElement('itemV1-2', checked); + toggleElement('itemV2-1', checked); + toggleElement('itemV2-2', checked); console.log('Custom Items:', checked); }); // More Items toggle toggleMoreItems.addEventListener('ui5-change', (e) => { const checked = e.target.checked; - document.getElementById('itemV1-3').style.display = checked ? '' : 'none'; - document.getElementById('itemV1-4').style.display = checked ? '' : 'none'; - document.getElementById('itemV2-3').style.display = checked ? '' : 'none'; - document.getElementById('itemV2-4').style.display = checked ? '' : 'none'; + toggleElement('itemV1-3', checked); + toggleElement('itemV1-4', checked); + toggleElement('itemV2-3', checked); + toggleElement('itemV2-4', checked); console.log('More Items:', checked); }); // Start Button toggle toggleStartButton.addEventListener('ui5-change', (e) => { const checked = e.target.checked; - document.getElementById('startButtonV1').style.display = checked ? '' : 'none'; - document.getElementById('startButtonV2').style.display = checked ? '' : 'none'; + toggleElement('startButtonV1', checked); + toggleElement('startButtonV2', checked); console.log('Start Button:', checked); }); @@ -373,8 +515,8 @@

Legacy Features (V1 Only)

toggleContent.addEventListener('ui5-change', (e) => { const checked = e.target.checked; for (let i = 1; i <= 5; i++) { - document.getElementById(`contentV1-${i}`).style.display = checked ? '' : 'none'; - document.getElementById(`contentV2-${i}`).style.display = checked ? '' : 'none'; + toggleElement(`contentV1-${i}`, checked); + toggleElement(`contentV2-${i}`, checked); } console.log('Content Items:', checked); }); @@ -382,8 +524,8 @@

Legacy Features (V1 Only)

// Spacer toggle toggleSpacer.addEventListener('ui5-change', (e) => { const checked = e.target.checked; - document.getElementById('spacerV1').style.display = checked ? '' : 'none'; - document.getElementById('spacerV2').style.display = checked ? '' : 'none'; + toggleElement('spacerV1', checked); + toggleElement('spacerV2', checked); console.log('Spacer:', checked); }); @@ -399,8 +541,8 @@

Legacy Features (V1 Only)

toggleMenuItems.addEventListener('ui5-change', (e) => { const checked = e.target.checked; for (let i = 1; i <= 3; i++) { - document.getElementById(`menuItemV1-${i}`).style.display = checked ? '' : 'none'; - document.getElementById(`menuItemV2-${i}`).style.display = checked ? '' : 'none'; + toggleElement(`menuItemV1-${i}`, checked); + toggleElement(`menuItemV2-${i}`, checked); } console.log('Menu Items:', checked); }); @@ -408,11 +550,33 @@

Legacy Features (V1 Only)

// Search Type selector searchType.addEventListener('ui5-change', (e) => { const value = e.detail.selectedOption.value; + const searchVisible = toggleSearchField.checked; console.log('Search Type changed to:', value); - // Remove old search fields - const oldV1 = document.getElementById('searchFieldV1'); - const oldV2 = document.getElementById('searchFieldV2'); + // Helper to replace search field + function replaceSearchField(oldId, newElement, shellbarId) { + const oldEl = document.getElementById(oldId); + const cache = elementCache.get(oldId); + + if (oldEl) { + // Element is in DOM + oldEl.replaceWith(newElement); + if (!searchVisible) { + toggleElement(oldId, false); + } + } else if (cache) { + // Element is cached (not visible) + const { parent, nextSibling } = cache; + cache.element = newElement; + elementCache.set(oldId, cache); + } else { + // Element doesn't exist, add it + document.getElementById(shellbarId).appendChild(newElement); + if (!searchVisible) { + toggleElement(oldId, false); + } + } + } if (value === 'shellbar-search') { // Create ui5-shellbar-search elements @@ -428,8 +592,8 @@

Legacy Features (V1 Only)

newV2.setAttribute('placeholder', 'Search...'); newV2.setAttribute('collapsed', ''); - oldV1.replaceWith(newV1); - oldV2.replaceWith(newV2); + replaceSearchField('searchFieldV1', newV1, 'shellbarV1'); + replaceSearchField('searchFieldV2', newV2, 'shellbarV2'); } else { // Create ui5-input elements const newV1 = document.createElement('ui5-input'); @@ -442,8 +606,8 @@

Legacy Features (V1 Only)

newV2.setAttribute('slot', 'searchField'); newV2.setAttribute('placeholder', 'Search'); - oldV1.replaceWith(newV1); - oldV2.replaceWith(newV2); + replaceSearchField('searchFieldV1', newV1, 'shellbarV1'); + replaceSearchField('searchFieldV2', newV2, 'shellbarV2'); } }); From b042045f7d8daacb6bdd61162f62378cd018e319 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 5 Nov 2025 09:33:10 +0200 Subject: [PATCH 22/57] fix: restore gap --- packages/fiori/src/themes/ShellBarV2.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/fiori/src/themes/ShellBarV2.css b/packages/fiori/src/themes/ShellBarV2.css index addb6a1816d7..6cbdcd8de240 100644 --- a/packages/fiori/src/themes/ShellBarV2.css +++ b/packages/fiori/src/themes/ShellBarV2.css @@ -47,6 +47,7 @@ align-items: center; width: 100%; height: 100%; + gap: 0.5rem; } /* ============================================================================ From f3a05fe246de2c58bcfd868c4100a1c31d74df12 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 5 Nov 2025 09:59:18 +0200 Subject: [PATCH 23/57] fix: item badges and styles --- packages/fiori/src/ShellBarV2Item.ts | 3 +- packages/fiori/src/ShellBarV2ItemTemplate.tsx | 3 +- packages/fiori/src/ShellBarV2Template.tsx | 23 ++++++----- .../ShellBarLegacyTemplate.tsx | 2 +- .../ShellBarSearchLegacyTemplate.tsx | 2 +- .../ShellBarSearchTemplate.tsx | 2 +- packages/fiori/src/themes/ShellBarV2.css | 39 ++++++++++++------- packages/fiori/src/themes/ShellBarV2Item.css | 22 +---------- 8 files changed, 44 insertions(+), 52 deletions(-) rename packages/fiori/src/shellbarv2/{ => templates}/ShellBarLegacyTemplate.tsx (98%) rename packages/fiori/src/shellbarv2/{ => templates}/ShellBarSearchLegacyTemplate.tsx (96%) rename packages/fiori/src/shellbarv2/{ => templates}/ShellBarSearchTemplate.tsx (94%) diff --git a/packages/fiori/src/ShellBarV2Item.ts b/packages/fiori/src/ShellBarV2Item.ts index ebd088256a99..0137cc57571a 100644 --- a/packages/fiori/src/ShellBarV2Item.ts +++ b/packages/fiori/src/ShellBarV2Item.ts @@ -5,6 +5,7 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import type { AccessibilityAttributes } from "@ui5/webcomponents-base"; import Button from "@ui5/webcomponents/dist/Button.js"; +import ButtonBadge from "@ui5/webcomponents/dist/ButtonBadge.js"; import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js"; import ShellBarV2ItemTemplate from "./ShellBarV2ItemTemplate.js"; import shellBarV2ItemStyles from "./generated/themes/ShellBarV2Item.css.js"; @@ -31,7 +32,7 @@ type ShellBarV2ItemAccessibilityAttributes = Pick {this.count && ( - {this.count} + )} ); diff --git a/packages/fiori/src/ShellBarV2Template.tsx b/packages/fiori/src/ShellBarV2Template.tsx index 82300f244609..87077cf27fa5 100644 --- a/packages/fiori/src/ShellBarV2Template.tsx +++ b/packages/fiori/src/ShellBarV2Template.tsx @@ -9,19 +9,19 @@ import type ShellBarV2Item from "./ShellBarV2Item.js"; import { ShellBarV2SearchField, ShellBarV2SearchFieldFullWidth -} from "./shellbarv2/ShellBarSearchTemplate.js"; +} from "./shellbarv2/templates/ShellBarSearchTemplate.js"; import { ShellBarV2SearchField as ShellBarV2SearchFieldLegacy, ShellBarV2SearchButton as ShellBarV2SearchButtonLegacy, ShellBarV2SearchFieldFullWidth as ShellBarV2SearchFieldFullWidthLegacy, -} from "./shellbarv2/ShellBarSearchLegacyTemplate.js"; +} from "./shellbarv2/templates/ShellBarSearchLegacyTemplate.js"; import { ShellBarV2LegacyBrandingArea, ShellBarV2MenuButton, ShellBarV2MenuPopover, -} from "./shellbarv2/ShellBarLegacyTemplate.js"; +} from "./shellbarv2/templates/ShellBarLegacyTemplate.js"; export default function ShellBarV2Template(this: ShellBarV2) { const isLegacySearch = !this.isSelfCollapsibleSearch; @@ -165,22 +165,21 @@ export default function ShellBarV2Template(this: ShellBarV2) { )} {this.getAction("profile") && ( -
-
+ )} {this.getAction("product-switch") && (
diff --git a/packages/fiori/test/pages/ShellBar_evolution.html b/packages/fiori/test/pages/ShellBar_evolution.html index 765adda26094..19181048b51c 100644 --- a/packages/fiori/test/pages/ShellBar_evolution.html +++ b/packages/fiori/test/pages/ShellBar_evolution.html @@ -22,86 +22,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - S/4HANA Cloud - - - - - - - EMEA - Deliveries overdue for billing neeed more text because of a bug - - - -
- New Version - -
- - -
Instructions
-
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - SAP Labs Bulgaria - - - - - PR10 - - PR 4 - PR3 - - - - - - PR2 - - PR1 - - PR6 - - PR7 - - PR8 - - PR9 - -
- - -
- - - - - - - - -
+ @@ -294,93 +166,7 @@
- From 5fe9940c8595a6e81054b8f56c87f58d98c216a6 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 5 Nov 2025 13:57:57 +0200 Subject: [PATCH 25/57] update sample and restore search event subscription on before rendering --- packages/fiori/src/ShellBarV2.ts | 3 + .../fiori/test/pages/ShellBar_Comparison.html | 103 ++++++++++++++++-- 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index bd42f37b2984..c2156bf66d9b 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -551,6 +551,9 @@ class ShellBarV2 extends UI5Element { this.updateActions(); this.searchAdaptor?.syncShowSearchFieldState(); + // subscribe to search adaptor for cases when search is added dynamically + this.searchAdaptor?.unsubscribe(); + this.legacyAdaptor?.subscribe(); } onAfterRendering() { diff --git a/packages/fiori/test/pages/ShellBar_Comparison.html b/packages/fiori/test/pages/ShellBar_Comparison.html index 5f8d35b82db4..81b988c5a2e9 100644 --- a/packages/fiori/test/pages/ShellBar_Comparison.html +++ b/packages/fiori/test/pages/ShellBar_Comparison.html @@ -175,17 +175,25 @@

Custom Items

- +
-

Legacy Features (V1 Only)

+

Branding

+
+ Branding Mode: + + Branding Slot (New) + Logo + Primary Title (Legacy) + +
Show Secondary Title:
Menu Items: - + + (requires Legacy mode)
@@ -194,14 +202,13 @@

Legacy Features (V1 Only)

How to use: Toggle switches to enable/disable features. Both ShellBars update simultaneously for easy comparison.
Tip: Resize window to test overflow behavior with content items. Check console for event logs. +
Branding Mode: Switch between new Branding Slot and legacy Logo + Primary Title modes. Menu items only available in legacy mode.
Product Title @@ -229,8 +236,6 @@

Legacy Features (V1 Only)

Product Title @@ -274,6 +279,7 @@

Legacy Features (V1 Only)

const toggleMenuItems = document.getElementById('toggleMenuItems'); const searchType = document.getElementById('searchType'); const notificationsCount = document.getElementById('notificationsCount'); + const brandingMode = document.getElementById('brandingMode'); // Element templates for dynamic creation const elementTemplates = { @@ -402,6 +408,20 @@

Legacy Features (V1 Only)

el.slot = 'menuItems'; el.textContent = 'Settings'; return el; + }, + logoV1: () => { + const el = document.createElement('img'); + el.id = 'logoV1'; + el.src = 'https://upload.wikimedia.org/wikipedia/commons/5/59/SAP_2011_logo.svg'; + el.slot = 'logo'; + return el; + }, + logoV2: () => { + const el = document.createElement('img'); + el.id = 'logoV2'; + el.src = 'https://upload.wikimedia.org/wikipedia/commons/5/59/SAP_2011_logo.svg'; + el.slot = 'logo'; + return el; } }; @@ -619,6 +639,75 @@

Legacy Features (V1 Only)

console.log('Notifications Count:', value); }); + // Branding mode selector + brandingMode.addEventListener('ui5-change', (e) => { + const value = e.detail.selectedOption.value; + console.log('Branding Mode changed to:', value); + + const isLegacy = value === 'legacy'; + + // Enable/disable menu items toggle + toggleMenuItems.disabled = !isLegacy; + + // If switching to branding mode, clear menu items + if (!isLegacy && toggleMenuItems.checked) { + toggleMenuItems.checked = false; + for (let i = 1; i <= 3; i++) { + toggleElement(`menuItemV1-${i}`, false); + toggleElement(`menuItemV2-${i}`, false); + } + } + + // Switch branding mode + const brandingV1 = shellbarV1.querySelector('[slot="branding"]'); + const brandingV2 = shellbarV2.querySelector('[slot="branding"]'); + + if (isLegacy) { + // Switch to logo + primaryTitle mode + if (brandingV1) brandingV1.remove(); + if (brandingV2) brandingV2.remove(); + + // Add logos + toggleElement('logoV1', true); + toggleElement('logoV2', true); + + // Ensure primaryTitle is set + shellbarV1.primaryTitle = 'Product Title'; + shellbarV2.primaryTitle = 'Product Title'; + } else { + // Switch to branding slot mode + toggleElement('logoV1', false); + toggleElement('logoV2', false); + + // Add branding slots if they don't exist + if (!brandingV1) { + const newBrandingV1 = document.createElement('ui5-shellbar-branding'); + newBrandingV1.slot = 'branding'; + newBrandingV1.textContent = 'Product Title'; + const logoImg = document.createElement('img'); + logoImg.src = 'https://upload.wikimedia.org/wikipedia/commons/5/59/SAP_2011_logo.svg'; + logoImg.slot = 'logo'; + newBrandingV1.appendChild(logoImg); + shellbarV1.insertBefore(newBrandingV1, shellbarV1.firstChild); + } + + if (!brandingV2) { + const newBrandingV2 = document.createElement('ui5-shellbar-branding'); + newBrandingV2.slot = 'branding'; + newBrandingV2.textContent = 'Product Title'; + const logoImg = document.createElement('img'); + logoImg.src = 'https://upload.wikimedia.org/wikipedia/commons/5/59/SAP_2011_logo.svg'; + logoImg.slot = 'logo'; + newBrandingV2.appendChild(logoImg); + shellbarV2.insertBefore(newBrandingV2, shellbarV2.firstChild); + } + + // Clear primaryTitle + shellbarV1.primaryTitle = ''; + shellbarV2.primaryTitle = ''; + } + }); + // Event listeners for V1 shellbarV1.addEventListener('ui5-notifications-click', (e) => { console.log('V1 Notifications clicked', e.detail); From d76627cd1f7ee4c694f292ef472b17435fac311a Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 5 Nov 2025 15:14:01 +0200 Subject: [PATCH 26/57] fix: choose correct search adaptor --- packages/fiori/src/ShellBarV2.ts | 34 +++++++++++++----------- packages/fiori/src/themes/ShellBarV2.css | 2 -- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index c2156bf66d9b..9ca83ca16513 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -448,7 +448,6 @@ class ShellBarV2 extends UI5Element { private handleResizeBound: ResizeObserverCallback = this.handleResize.bind(this); - searchAdaptor?: IShellBarSearchController; itemNavigation = new ShellBarV2ItemNavigation({ getDomRef: () => this.getDomRef() || null, }); @@ -459,6 +458,12 @@ class ShellBarV2 extends UI5Element { overflowAdaptor = new ShellBarV2Overflow(); accessibilityAdaptor = new ShellBarV2Accessibility(); + _searchAdaptor = new ShellBarV2Search(this.getSearchDeps()); + _searchAdaptorLegacy = new ShellBarV2SearchLegacy({ + ...this.getSearchDeps(), + getDisableSearchCollapse: () => this.disableSearchCollapse, + }); + /* ========================================================================= Legacy Members ============================================================================ */ @@ -530,7 +535,6 @@ class ShellBarV2 extends UI5Element { ============================================================================ */ onEnterDOM() { - this.initSearchController(); this.initLegacyController(); ResizeHandler.register(this, this.handleResizeBound); this.searchAdaptor?.subscribe(); @@ -553,11 +557,12 @@ class ShellBarV2 extends UI5Element { this.searchAdaptor?.syncShowSearchFieldState(); // subscribe to search adaptor for cases when search is added dynamically this.searchAdaptor?.unsubscribe(); - this.legacyAdaptor?.subscribe(); + this.searchAdaptor?.subscribe(); } - onAfterRendering() { + async onAfterRendering() { this.updateBreakpoint(); + await renderFinished(); this.updateOverflow(); } @@ -762,22 +767,21 @@ class ShellBarV2 extends UI5Element { * Self-collapsible search (ui5-shellbar-search) → ShellBarV2Search * Legacy search (ui5-input, custom div) → ShellBarLegacySearch */ - private initSearchController() { - const deps = { + private getSearchDeps() { + return { getSearchField: () => this.search, getSearchState: () => this.showSearchField, getCSSVariable: (cssVar: string) => this.getCSSVariable(cssVar), setSearchState: (expanded: boolean) => this.setSearchState(expanded), getOverflowed: () => this.overflowAdaptor.isOverflowing(this.overflowOuter!, this.overflowInner!), }; + } + + get searchAdaptor(): IShellBarSearchController { if (this.isSelfCollapsibleSearch) { - this.searchAdaptor = new ShellBarV2Search(deps); - } else { - this.searchAdaptor = new ShellBarV2SearchLegacy({ - ...deps, - getDisableSearchCollapse: () => this.disableSearchCollapse, - }); + return this._searchAdaptor; } + return this._searchAdaptorLegacy; } handleSearchButtonClick() { @@ -810,14 +814,12 @@ class ShellBarV2 extends UI5Element { * Sets search field state and fires toggle event. * Component coordination: delegates to controller for logic, fires event for external listeners. */ - setSearchState(expanded: boolean) { + async setSearchState(expanded: boolean) { if (expanded === this.showSearchField) { return; } this.showSearchField = expanded; - requestAnimationFrame(() => { - this.updateOverflow(); - }); + await renderFinished(); this.fireDecoratorEvent("search-field-toggle", { expanded }); } diff --git a/packages/fiori/src/themes/ShellBarV2.css b/packages/fiori/src/themes/ShellBarV2.css index 805f2b7e781c..982ed094d8f0 100644 --- a/packages/fiori/src/themes/ShellBarV2.css +++ b/packages/fiori/src/themes/ShellBarV2.css @@ -167,7 +167,6 @@ min-width: 0; display: flex; align-items: center; - margin: 0 0.5rem; margin-left: auto; } @@ -269,7 +268,6 @@ .ui5-shellbar-assistant-button { display: flex; align-items: center; - margin-inline-start: var(--_ui5-shellbar-overflow-button-margin); } ::slotted([ui5-toggle-button][slot="assistant"]) { From e5dabf287c79bfcf9d81c7cd95a28a2b0c1338fc Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 5 Nov 2025 18:32:47 +0200 Subject: [PATCH 27/57] more legacy fixes --- packages/fiori/src/ShellBarV2.ts | 9 +- packages/fiori/src/ShellBarV2Template.tsx | 31 ++++- .../fiori/src/shellbarv2/ShellBarLegacy.ts | 62 ++++++---- .../templates/ShellBarLegacyTemplate.tsx | 115 ++++++++++++++++-- packages/fiori/src/themes/ShellBarV2.css | 1 + .../fiori/src/themes/ShellBarV2Legacy.css | 1 - .../fiori/test/pages/ShellBar_Comparison.html | 41 +++++-- 7 files changed, 208 insertions(+), 52 deletions(-) diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index 9ca83ca16513..db957dbcc9a1 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -535,7 +535,6 @@ class ShellBarV2 extends UI5Element { ============================================================================ */ onEnterDOM() { - this.initLegacyController(); ResizeHandler.register(this, this.handleResizeBound); this.searchAdaptor?.subscribe(); } @@ -543,10 +542,12 @@ class ShellBarV2 extends UI5Element { onExitDOM() { ResizeHandler.deregister(this, this.handleResizeBound); this.searchAdaptor?.unsubscribe(); - this.legacyAdaptor?.unsubscribe(); } onBeforeRendering() { + if (!this.legacyAdaptor && this.hasLegacyFeatures) { + this.initLegacyController(); + } // Sync branding breakpoint state this.branding.forEach(brandingEl => { brandingEl._isSBreakPoint = this.isSBreakPoint; @@ -560,9 +561,8 @@ class ShellBarV2 extends UI5Element { this.searchAdaptor?.subscribe(); } - async onAfterRendering() { + onAfterRendering() { this.updateBreakpoint(); - await renderFinished(); this.updateOverflow(); } @@ -854,7 +854,6 @@ class ShellBarV2 extends UI5Element { component: this, getShadowRoot: () => this.shadowRoot, }); - this.legacyAdaptor.subscribe(); } } diff --git a/packages/fiori/src/ShellBarV2Template.tsx b/packages/fiori/src/ShellBarV2Template.tsx index a26d586b458f..38329612ddb6 100644 --- a/packages/fiori/src/ShellBarV2Template.tsx +++ b/packages/fiori/src/ShellBarV2Template.tsx @@ -19,6 +19,10 @@ import { import { ShellBarV2LegacyBrandingArea, + ShellBarV2LegacySecondaryTitle, + ShellBarV2SeparateLogo, + ShellBarV2InteractiveMenuButton, + ShellBarV2SingleLogo, ShellBarV2MenuButton, ShellBarV2MenuPopover, } from "./shellbarv2/templates/ShellBarLegacyTemplate.js"; @@ -37,9 +41,6 @@ export default function ShellBarV2Template(this: ShellBarV2) {
- {/* Menu button (legacy, S breakpoint only) */} - {ShellBarV2MenuButton.call(this)} - {this.hasStartButton && (
@@ -52,9 +53,29 @@ export default function ShellBarV2Template(this: ShellBarV2) {
)} - {/* Legacy branding (logo + titles) */} + {/* Legacy menu button (S breakpoint only) - contains logo or title */} + {ShellBarV2MenuButton.call(this)} + + {/* Legacy: Single logo on S breakpoint when no menu items */} + {ShellBarV2SingleLogo.call(this)} + + {/* Legacy: Separate logo when menu items exist (non-S breakpoint) */} + {ShellBarV2SeparateLogo.call(this)} + + {/* Legacy: Hidden h1 for accessibility when title is in menu button */} + {this.legacyAdaptor?.showInteractiveMenuButton && ( +

{this.primaryTitle}

+ )} + + {/* Legacy: Interactive menu button (non-S breakpoint) */} + {ShellBarV2InteractiveMenuButton.call(this)} + + {/* Legacy branding (logo + primaryTitle) when no menu items */} {ShellBarV2LegacyBrandingArea.call(this)} + {/* Legacy secondaryTitle - rendered separately to match old shellbar */} + {ShellBarV2LegacySecondaryTitle.call(this)} +
@@ -86,7 +107,7 @@ export default function ShellBarV2Template(this: ShellBarV2) { ); })} - {/* Spacer: Grows to fill available space, used to measure if space is tight */} + {/* Spacer: Grows to fill available space, used to measure if space is tight, should be in DOM always */}
{/* End content items */} diff --git a/packages/fiori/src/shellbarv2/ShellBarLegacy.ts b/packages/fiori/src/shellbarv2/ShellBarLegacy.ts index c70a381fdecd..c1a8c4c2c966 100644 --- a/packages/fiori/src/shellbarv2/ShellBarLegacy.ts +++ b/packages/fiori/src/shellbarv2/ShellBarLegacy.ts @@ -4,6 +4,8 @@ import { } from "@ui5/webcomponents-base/dist/Keys.js"; import type { ListItemClickEventDetail } from "@ui5/webcomponents/dist/List.js"; import type ShellBarV2 from "../ShellBarV2.js"; +import List from "@ui5/webcomponents/dist/List.js"; +import type Popover from "@ui5/webcomponents/dist/Popover.js"; type ShellBarV2LegacyDeps = { component: ShellBarV2; @@ -32,20 +34,6 @@ class ShellBarV2Legacy { this.getShadowRoot = deps.getShadowRoot; } - /** - * Subscribe to events (if needed in the future). - */ - subscribe() { - // No subscription needed for now - } - - /** - * Unsubscribe from events. - */ - unsubscribe() { - // No unsubscription needed for now - } - /* ------------- Menu Management -------------- */ handleMenuButtonClick() { @@ -58,7 +46,7 @@ class ShellBarV2Legacy { const menuPopover = this.getMenuPopover(); if (menuPopover && menuButton) { - menuPopover.opener = menuButton; + menuPopover.opener = menuButton as HTMLElement; menuPopover.open = true; } } @@ -80,8 +68,8 @@ class ShellBarV2Legacy { this.component.menuPopoverOpen = true; const menuPopover = this.getMenuPopover(); if (menuPopover?.content && menuPopover.content.length) { - const list = menuPopover.content[0] as any; - if (list.focusFirstItem) { + const list = menuPopover.content[0]; + if (list instanceof List) { list.focusFirstItem(); } } @@ -93,7 +81,7 @@ class ShellBarV2Legacy { private getMenuPopover() { const shadowRoot = this.getShadowRoot(); - return shadowRoot?.querySelector(".ui5-shellbar-menu-popover") as any; + return shadowRoot?.querySelector(".ui5-shellbar-menu-popover"); } get hasMenuItems(): boolean { @@ -159,6 +147,21 @@ class ShellBarV2Legacy { return !!this.component.secondaryTitle; } + get showSecondaryTitle(): boolean { + // Secondary title only shown on non-S breakpoints (and when other conditions met) + if (this.component.isSBreakPoint) { + return false; + } + + // With menu items: show if secondary title exists + if (this.hasMenuItems) { + return this.hasSecondaryTitle; + } + + // Without menu items: show if secondary title exists and (primaryTitle or branding) + return this.hasSecondaryTitle && (this.hasPrimaryTitle || this.component.hasBranding); + } + get primaryTitle(): string { return this.component.primaryTitle || ""; } @@ -167,13 +170,23 @@ class ShellBarV2Legacy { return this.component.secondaryTitle || ""; } - /* ------------- Menu Button (Mobile) -------------- */ + /* ------------- Menu Button -------------- */ get showMenuButton(): boolean { // Show menu button on S breakpoint if we have menu items or logo/title return this.component.isSBreakPoint && (this.hasMenuItems || this.hasLogo || this.hasPrimaryTitle); } + get showInteractiveMenuButton(): boolean { + // Show interactive menu button (with primaryTitle) on non-S breakpoints when menu items exist + return this.hasMenuItems && this.hasPrimaryTitle && !this.component.isSBreakPoint; + } + + get showSeparateLogo(): boolean { + // Show logo separately when menu items exist and not on S breakpoint + return this.hasMenuItems && this.hasLogo && !this.showLogoInMenuButton; + } + get showLogoInMenuButton(): boolean { return this.hasLogo && this.component.isSBreakPoint; } @@ -192,10 +205,15 @@ class ShellBarV2Legacy { /* ------------- Common -------------- */ get shouldRenderLegacyBranding(): boolean { - // Only render legacy branding if no modern branding slot is used - return !this.component.hasBranding && (this.hasLogo || this.hasPrimaryTitle || this.hasSecondaryTitle); + // Only render legacy branding if: + // - no modern branding slot is used + // - no menu items (when menu items exist, logo and title are rendered separately) + // - not on S breakpoint (on S, logo/title go inside menu button) + return !this.component.hasBranding + && !this.hasMenuItems + && !this.component.isSBreakPoint + && (this.hasLogo || this.hasPrimaryTitle || this.hasSecondaryTitle); } } export default ShellBarV2Legacy; - diff --git a/packages/fiori/src/shellbarv2/templates/ShellBarLegacyTemplate.tsx b/packages/fiori/src/shellbarv2/templates/ShellBarLegacyTemplate.tsx index ae94490e4eff..5a40d2818b6b 100644 --- a/packages/fiori/src/shellbarv2/templates/ShellBarLegacyTemplate.tsx +++ b/packages/fiori/src/shellbarv2/templates/ShellBarLegacyTemplate.tsx @@ -1,6 +1,8 @@ import Button from "@ui5/webcomponents/dist/Button.js"; +import Icon from "@ui5/webcomponents/dist/Icon.js"; import List from "@ui5/webcomponents/dist/List.js"; import Popover from "@ui5/webcomponents/dist/Popover.js"; +import slimArrowDown from "@ui5/webcomponents-icons/dist/slim-arrow-down.js"; import type ShellBarV2 from "../../ShellBarV2.js"; /** @@ -29,23 +31,69 @@ function ShellBarV2LegacyLogoArea(this: ShellBarV2) { } /** - * Renders legacy title area. - * Used when primaryTitle/secondaryTitle properties are set but no branding slot. + * Renders separate logo when menu items exist. + * Used on non-S breakpoints when menu items are present. + */ +function ShellBarV2SeparateLogo(this: ShellBarV2) { + const legacy = this.legacyAdaptor; + if (!legacy || !legacy.showSeparateLogo) { + return null; + } + + return ( + + ); +} + +/** + * Renders interactive menu button for non-S breakpoints. + * Shows primaryTitle with arrow, opens menu popover. + */ +function ShellBarV2InteractiveMenuButton(this: ShellBarV2) { + const legacy = this.legacyAdaptor; + if (!legacy || !legacy.showInteractiveMenuButton) { + return null; + } + + return ( + + ); +} + +/** + * Renders legacy title area (primaryTitle only). + * Used when primaryTitle property is set but no branding slot. */ function ShellBarV2LegacyTitleArea(this: ShellBarV2) { const legacy = this.legacyAdaptor; - if (!legacy || (!legacy.hasPrimaryTitle && !legacy.hasSecondaryTitle)) { + if (!legacy || !legacy.hasPrimaryTitle) { return null; } return ( -
- {legacy.hasPrimaryTitle && ( -
{legacy.primaryTitle}
- )} - {legacy.hasSecondaryTitle && ( -
{legacy.secondaryTitle}
- )} +
+

+ {legacy.primaryTitle} +

); } @@ -68,6 +116,49 @@ function ShellBarV2LegacyBrandingArea(this: ShellBarV2) { ); } +/** + * Renders single logo on S breakpoint when no menu items. + * Used on S breakpoint when no menu items and no branding slot. + */ +function ShellBarV2SingleLogo(this: ShellBarV2) { + const legacy = this.legacyAdaptor; + if (!legacy || !legacy.hasLogo || !this.isSBreakPoint || legacy.hasMenuItems || this.hasBranding) { + return null; + } + + return ( + + ); +} + +/** + * Renders legacy secondaryTitle. + * Rendered separately from the logo area to match old shellbar structure. + * Hidden on S breakpoint when menu items exist. + */ +function ShellBarV2LegacySecondaryTitle(this: ShellBarV2) { + const legacy = this.legacyAdaptor; + if (!legacy || !legacy.showSecondaryTitle) { + return null; + } + + return ( +

+ {legacy.secondaryTitle} +

+ ); +} + /** * Renders the menu button for S breakpoint. * Shows logo or title and opens menu popover. @@ -122,8 +213,12 @@ function ShellBarV2MenuPopover(this: ShellBarV2) { export { ShellBarV2LegacyLogoArea, + ShellBarV2SeparateLogo, + ShellBarV2InteractiveMenuButton, + ShellBarV2SingleLogo, ShellBarV2LegacyTitleArea, ShellBarV2LegacyBrandingArea, + ShellBarV2LegacySecondaryTitle, ShellBarV2MenuButton, ShellBarV2MenuPopover, }; diff --git a/packages/fiori/src/themes/ShellBarV2.css b/packages/fiori/src/themes/ShellBarV2.css index 982ed094d8f0..21b79dff7c17 100644 --- a/packages/fiori/src/themes/ShellBarV2.css +++ b/packages/fiori/src/themes/ShellBarV2.css @@ -1,3 +1,4 @@ +@import "./InvisibleTextStyles.css"; @import "./ShellBarV2SearchLegacy.css"; /* ============================================================================ diff --git a/packages/fiori/src/themes/ShellBarV2Legacy.css b/packages/fiori/src/themes/ShellBarV2Legacy.css index bf7b56a68dc1..692a69cb6b7a 100644 --- a/packages/fiori/src/themes/ShellBarV2Legacy.css +++ b/packages/fiori/src/themes/ShellBarV2Legacy.css @@ -48,7 +48,6 @@ } /* Title Area */ -.ui5-shellbar-title-area, .ui5-shellbar-headings { display: flex; flex-direction: column; diff --git a/packages/fiori/test/pages/ShellBar_Comparison.html b/packages/fiori/test/pages/ShellBar_Comparison.html index 81b988c5a2e9..bc9da8b01399 100644 --- a/packages/fiori/test/pages/ShellBar_Comparison.html +++ b/packages/fiori/test/pages/ShellBar_Comparison.html @@ -209,6 +209,7 @@

Branding

Product Title @@ -236,6 +237,7 @@

Branding

Product Title @@ -639,6 +641,10 @@

Branding

console.log('Notifications Count:', value); }); + // Cache for branding elements + let cachedBrandingV1 = null; + let cachedBrandingV2 = null; + // Branding mode selector brandingMode.addEventListener('ui5-change', (e) => { const value = e.detail.selectedOption.value; @@ -658,14 +664,22 @@

Branding

} } - // Switch branding mode - const brandingV1 = shellbarV1.querySelector('[slot="branding"]'); - const brandingV2 = shellbarV2.querySelector('[slot="branding"]'); - if (isLegacy) { // Switch to logo + primaryTitle mode - if (brandingV1) brandingV1.remove(); - if (brandingV2) brandingV2.remove(); + // Cache and remove branding slots + const brandingV1 = shellbarV1.querySelector('[slot="branding"]'); + const brandingV2 = shellbarV2.querySelector('[slot="branding"]'); + + console.log('Switching to legacy mode - brandingV1:', brandingV1, 'brandingV2:', brandingV2); + + if (brandingV1) { + cachedBrandingV1 = brandingV1; + brandingV1.remove(); + } + if (brandingV2) { + cachedBrandingV2 = brandingV2; + brandingV2.remove(); + } // Add logos toggleElement('logoV1', true); @@ -674,13 +688,18 @@

Branding

// Ensure primaryTitle is set shellbarV1.primaryTitle = 'Product Title'; shellbarV2.primaryTitle = 'Product Title'; + + console.log('Legacy mode set - V1 logo:', shellbarV1.logo, 'V2 logo:', shellbarV2.logo); + console.log('Legacy mode set - V1 title:', shellbarV1.primaryTitle, 'V2 title:', shellbarV2.primaryTitle); } else { // Switch to branding slot mode toggleElement('logoV1', false); toggleElement('logoV2', false); - // Add branding slots if they don't exist - if (!brandingV1) { + // Restore cached branding or create new ones + if (cachedBrandingV1) { + shellbarV1.insertBefore(cachedBrandingV1, shellbarV1.firstChild); + } else { const newBrandingV1 = document.createElement('ui5-shellbar-branding'); newBrandingV1.slot = 'branding'; newBrandingV1.textContent = 'Product Title'; @@ -689,9 +708,12 @@

Branding

logoImg.slot = 'logo'; newBrandingV1.appendChild(logoImg); shellbarV1.insertBefore(newBrandingV1, shellbarV1.firstChild); + cachedBrandingV1 = newBrandingV1; } - if (!brandingV2) { + if (cachedBrandingV2) { + shellbarV2.insertBefore(cachedBrandingV2, shellbarV2.firstChild); + } else { const newBrandingV2 = document.createElement('ui5-shellbar-branding'); newBrandingV2.slot = 'branding'; newBrandingV2.textContent = 'Product Title'; @@ -700,6 +722,7 @@

Branding

logoImg.slot = 'logo'; newBrandingV2.appendChild(logoImg); shellbarV2.insertBefore(newBrandingV2, shellbarV2.firstChild); + cachedBrandingV2 = newBrandingV2; } // Clear primaryTitle From 92d7b103262a44e3fcfc0e94c36c9d604b5737d9 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 5 Nov 2025 21:19:17 +0200 Subject: [PATCH 28/57] implement full legacy support --- packages/fiori/src/ShellBarV2.ts | 3 +- packages/fiori/src/ShellBarV2Template.tsx | 31 +-- .../src/shellbarv2/ShellBarAccessibility.ts | 6 + .../fiori/src/shellbarv2/ShellBarLegacy.ts | 64 ++--- .../templates/ShellBarLegacyTemplate.tsx | 228 ++++++++---------- 5 files changed, 124 insertions(+), 208 deletions(-) diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index db957dbcc9a1..b1546025d96c 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -26,6 +26,7 @@ import Menu from "@ui5/webcomponents/dist/Menu.js"; import ShellBarV2Template from "./ShellBarV2Template.js"; import shellBarV2Styles from "./generated/themes/ShellBarV2.css.js"; +import ShellBarPopoverCss from "./generated/themes/ShellBarPopover.css.js"; import shellBarV2LegacyStyles from "./generated/themes/ShellBarV2Legacy.css.js"; import type { IShellBarSearchController } from "./shellbarv2/IShellBarSearchController.js"; @@ -134,7 +135,7 @@ interface IShellBarSearchField extends HTMLElement { */ @customElement({ tag: "ui5-shellbar-v2", - styles: [shellBarV2Styles, shellBarV2LegacyStyles], + styles: [shellBarV2Styles, shellBarV2LegacyStyles, ShellBarPopoverCss], renderer: jsxRenderer, template: ShellBarV2Template, fastNavigation: true, diff --git a/packages/fiori/src/ShellBarV2Template.tsx b/packages/fiori/src/ShellBarV2Template.tsx index 38329612ddb6..dbc9d931ea2c 100644 --- a/packages/fiori/src/ShellBarV2Template.tsx +++ b/packages/fiori/src/ShellBarV2Template.tsx @@ -19,12 +19,6 @@ import { import { ShellBarV2LegacyBrandingArea, - ShellBarV2LegacySecondaryTitle, - ShellBarV2SeparateLogo, - ShellBarV2InteractiveMenuButton, - ShellBarV2SingleLogo, - ShellBarV2MenuButton, - ShellBarV2MenuPopover, } from "./shellbarv2/templates/ShellBarLegacyTemplate.js"; export default function ShellBarV2Template(this: ShellBarV2) { @@ -53,28 +47,8 @@ export default function ShellBarV2Template(this: ShellBarV2) {
)} - {/* Legacy menu button (S breakpoint only) - contains logo or title */} - {ShellBarV2MenuButton.call(this)} - - {/* Legacy: Single logo on S breakpoint when no menu items */} - {ShellBarV2SingleLogo.call(this)} - - {/* Legacy: Separate logo when menu items exist (non-S breakpoint) */} - {ShellBarV2SeparateLogo.call(this)} - - {/* Legacy: Hidden h1 for accessibility when title is in menu button */} - {this.legacyAdaptor?.showInteractiveMenuButton && ( -

{this.primaryTitle}

- )} - - {/* Legacy: Interactive menu button (non-S breakpoint) */} - {ShellBarV2InteractiveMenuButton.call(this)} - {/* Legacy branding (logo + primaryTitle) when no menu items */} - {ShellBarV2LegacyBrandingArea.call(this)} - - {/* Legacy secondaryTitle - rendered separately to match old shellbar */} - {ShellBarV2LegacySecondaryTitle.call(this)} + {!this.hasBranding && ShellBarV2LegacyBrandingArea.call(this)}
@@ -212,9 +186,6 @@ export default function ShellBarV2Template(this: ShellBarV2) {
- {/* Menu Popover (legacy) */} - {ShellBarV2MenuPopover.call(this)} - {/* Overflow Popover */} ; +/** + * Accessibility attributes for branding area + */ +type ShellBarV2BrandingAccessibilityAttributes = Pick; + /** * Top-level accessibility configuration for ShellBarV2 */ @@ -28,6 +33,7 @@ type ShellBarV2AccessibilityAttributes = { product?: ShellBarV2AreaAccessibilityAttributes; search?: ShellBarV2AreaAccessibilityAttributes; overflow?: ShellBarV2AreaAccessibilityAttributes; + branding?: ShellBarV2BrandingAccessibilityAttributes; }; /** diff --git a/packages/fiori/src/shellbarv2/ShellBarLegacy.ts b/packages/fiori/src/shellbarv2/ShellBarLegacy.ts index c1a8c4c2c966..efc9124e4f9e 100644 --- a/packages/fiori/src/shellbarv2/ShellBarLegacy.ts +++ b/packages/fiori/src/shellbarv2/ShellBarLegacy.ts @@ -21,13 +21,13 @@ class ShellBarV2Legacy { private getShadowRoot: () => ShadowRoot | null; // Bound handlers for event listeners - private handleMenuButtonClickBound = this.handleMenuButtonClick.bind(this); - private handleLogoClickBound = this.handleLogoClick.bind(this); - private handleLogoKeydownBound = this.handleLogoKeydown.bind(this); - private handleLogoKeyupBound = this.handleLogoKeyup.bind(this); - private handleMenuItemClickBound = this.handleMenuItemClick.bind(this); - private handleMenuPopoverBeforeOpenBound = this.handleMenuPopoverBeforeOpen.bind(this); - private handleMenuPopoverAfterCloseBound = this.handleMenuPopoverAfterClose.bind(this); + handleLogoClickBound = this.handleLogoClick.bind(this); + handleLogoKeyupBound = this.handleLogoKeyup.bind(this); + handleLogoKeydownBound = this.handleLogoKeydown.bind(this); + handleMenuItemClickBound = this.handleMenuItemClick.bind(this); + handleMenuButtonClickBound = this.handleMenuButtonClick.bind(this); + handleMenuPopoverBeforeOpenBound = this.handleMenuPopoverBeforeOpen.bind(this); + handleMenuPopoverAfterCloseBound = this.handleMenuPopoverAfterClose.bind(this); constructor(deps: ShellBarV2LegacyDeps) { this.component = deps.component; @@ -137,6 +137,10 @@ class ShellBarV2Legacy { return this.component.accessibilityAttributes.logo?.name || "Logo"; } + get brandingText(): string { + return this.component.accessibilityAttributes.branding?.name || this.primaryTitle; + } + /* ------------- Title Management -------------- */ get hasPrimaryTitle(): boolean { @@ -148,18 +152,7 @@ class ShellBarV2Legacy { } get showSecondaryTitle(): boolean { - // Secondary title only shown on non-S breakpoints (and when other conditions met) - if (this.component.isSBreakPoint) { - return false; - } - - // With menu items: show if secondary title exists - if (this.hasMenuItems) { - return this.hasSecondaryTitle; - } - - // Without menu items: show if secondary title exists and (primaryTitle or branding) - return this.hasSecondaryTitle && (this.hasPrimaryTitle || this.component.hasBranding); + return this.hasSecondaryTitle && !this.component.isSBreakPoint; } get primaryTitle(): string { @@ -173,46 +166,21 @@ class ShellBarV2Legacy { /* ------------- Menu Button -------------- */ get showMenuButton(): boolean { - // Show menu button on S breakpoint if we have menu items or logo/title - return this.component.isSBreakPoint && (this.hasMenuItems || this.hasLogo || this.hasPrimaryTitle); - } - - get showInteractiveMenuButton(): boolean { - // Show interactive menu button (with primaryTitle) on non-S breakpoints when menu items exist - return this.hasMenuItems && this.hasPrimaryTitle && !this.component.isSBreakPoint; - } - - get showSeparateLogo(): boolean { - // Show logo separately when menu items exist and not on S breakpoint - return this.hasMenuItems && this.hasLogo && !this.showLogoInMenuButton; + return this.hasPrimaryTitle || this.showLogoInMenuButton; } get showLogoInMenuButton(): boolean { - return this.hasLogo && this.component.isSBreakPoint; + return this.hasLogo && this.isSBreakPoint; } get showTitleInMenuButton(): boolean { return this.hasPrimaryTitle && !this.showLogoInMenuButton; } - get menuButtonAccessibilityAttributes() { - return { - hasPopup: this.hasMenuItems ? "menu" as const : undefined, - expanded: this.hasMenuItems ? this.menuPopoverExpanded : undefined, - }; - } - /* ------------- Common -------------- */ - get shouldRenderLegacyBranding(): boolean { - // Only render legacy branding if: - // - no modern branding slot is used - // - no menu items (when menu items exist, logo and title are rendered separately) - // - not on S breakpoint (on S, logo/title go inside menu button) - return !this.component.hasBranding - && !this.hasMenuItems - && !this.component.isSBreakPoint - && (this.hasLogo || this.hasPrimaryTitle || this.hasSecondaryTitle); + get isSBreakPoint(): boolean { + return this.component.isSBreakPoint; } } diff --git a/packages/fiori/src/shellbarv2/templates/ShellBarLegacyTemplate.tsx b/packages/fiori/src/shellbarv2/templates/ShellBarLegacyTemplate.tsx index 5a40d2818b6b..3cfd4a8572a8 100644 --- a/packages/fiori/src/shellbarv2/templates/ShellBarLegacyTemplate.tsx +++ b/packages/fiori/src/shellbarv2/templates/ShellBarLegacyTemplate.tsx @@ -1,57 +1,47 @@ -import Button from "@ui5/webcomponents/dist/Button.js"; import Icon from "@ui5/webcomponents/dist/Icon.js"; import List from "@ui5/webcomponents/dist/List.js"; import Popover from "@ui5/webcomponents/dist/Popover.js"; import slimArrowDown from "@ui5/webcomponents-icons/dist/slim-arrow-down.js"; import type ShellBarV2 from "../../ShellBarV2.js"; -/** - * Renders the legacy logo area. - * Used when logo slot is provided but no branding slot. - */ -function ShellBarV2LegacyLogoArea(this: ShellBarV2) { +function ShellBarV2LegacyBrandingArea(this: ShellBarV2) { const legacy = this.legacyAdaptor; - if (!legacy || !legacy.hasLogo) { + if (!legacy) { return null; } return ( - + <> + {legacy.hasMenuItems && ShellBarV2InteractiveMenuButton.call(this)} + {legacy.hasMenuItems && ShellBarV2LegacySecondaryTitle.call(this)} + {!legacy.hasMenuItems && ShellBarV2LegacyTitleArea.call(this)} + + {/* Menu Popover (legacy) */} + {ShellBarV2MenuPopover.call(this)} + ); } -/** - * Renders separate logo when menu items exist. - * Used on non-S breakpoints when menu items are present. - */ -function ShellBarV2SeparateLogo(this: ShellBarV2) { +function ShellBarV2LegacyTitleArea(this: ShellBarV2) { const legacy = this.legacyAdaptor; - if (!legacy || !legacy.showSeparateLogo) { + if (!legacy) { return null; } return ( - + <> + {!!(legacy.isSBreakPoint && legacy.hasLogo) && ShellBarV2SingleLogo.call(this)} + {!legacy.isSBreakPoint && (legacy.hasLogo || legacy.primaryTitle) && ( + <> + {ShellBarV2CombinedLogo.call(this)} + {legacy.hasSecondaryTitle && legacy.hasPrimaryTitle && ( +

+ {legacy.secondaryTitle} +

+ )} + + )} + ); } @@ -61,58 +51,38 @@ function ShellBarV2SeparateLogo(this: ShellBarV2) { */ function ShellBarV2InteractiveMenuButton(this: ShellBarV2) { const legacy = this.legacyAdaptor; - if (!legacy || !legacy.showInteractiveMenuButton) { - return null; - } - - return ( - - ); -} - -/** - * Renders legacy title area (primaryTitle only). - * Used when primaryTitle property is set but no branding slot. - */ -function ShellBarV2LegacyTitleArea(this: ShellBarV2) { - const legacy = this.legacyAdaptor; - if (!legacy || !legacy.hasPrimaryTitle) { - return null; - } - - return ( -
-

- {legacy.primaryTitle} -

-
- ); -} - -/** - * Renders the legacy branding area (logo + titles). - * Only renders if no modern branding slot is used. - */ -function ShellBarV2LegacyBrandingArea(this: ShellBarV2) { - const legacy = this.legacyAdaptor; - if (!legacy || !legacy.shouldRenderLegacyBranding) { + if (!legacy) { return null; } return ( -
- {ShellBarV2LegacyLogoArea.call(this)} - {ShellBarV2LegacyTitleArea.call(this)} -
+ <> + {!legacy.showLogoInMenuButton && legacy.hasLogo && ShellBarV2SingleLogo.call(this)} + {legacy.showTitleInMenuButton &&

{legacy.primaryTitle}

} + {legacy.showMenuButton && ( + + )} + ); } @@ -122,67 +92,70 @@ function ShellBarV2LegacyBrandingArea(this: ShellBarV2) { */ function ShellBarV2SingleLogo(this: ShellBarV2) { const legacy = this.legacyAdaptor; - if (!legacy || !legacy.hasLogo || !this.isSBreakPoint || legacy.hasMenuItems || this.hasBranding) { + if (!legacy) { return null; } return ( ); } -/** - * Renders legacy secondaryTitle. - * Rendered separately from the logo area to match old shellbar structure. - * Hidden on S breakpoint when menu items exist. - */ -function ShellBarV2LegacySecondaryTitle(this: ShellBarV2) { +function ShellBarV2CombinedLogo(this: ShellBarV2) { const legacy = this.legacyAdaptor; - if (!legacy || !legacy.showSecondaryTitle) { + if (!legacy) { return null; } return ( -

- {legacy.secondaryTitle} -

+
+ {legacy.hasLogo && ( + + )} +
+ {legacy.primaryTitle && ( +

+ {legacy.primaryTitle} +

+ )} +
+
); } -/** - * Renders the menu button for S breakpoint. - * Shows logo or title and opens menu popover. - */ -function ShellBarV2MenuButton(this: ShellBarV2) { +function ShellBarV2LegacySecondaryTitle(this: ShellBarV2) { const legacy = this.legacyAdaptor; - if (!legacy || !legacy.showMenuButton) { + if (!legacy || !legacy.showSecondaryTitle) { return null; } return ( - +
+ {this.secondaryTitle} +
); } @@ -197,14 +170,14 @@ function ShellBarV2MenuPopover(this: ShellBarV2) { } return ( - - + @@ -212,13 +185,10 @@ function ShellBarV2MenuPopover(this: ShellBarV2) { } export { - ShellBarV2LegacyLogoArea, - ShellBarV2SeparateLogo, - ShellBarV2InteractiveMenuButton, ShellBarV2SingleLogo, + ShellBarV2MenuPopover, ShellBarV2LegacyTitleArea, ShellBarV2LegacyBrandingArea, ShellBarV2LegacySecondaryTitle, - ShellBarV2MenuButton, - ShellBarV2MenuPopover, + ShellBarV2InteractiveMenuButton, }; From f3a82ea2f01e3aba0cae72d2a703c5153ec0a847 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Thu, 6 Nov 2025 10:04:11 +0200 Subject: [PATCH 29/57] restore stable dom refs --- packages/fiori/cypress/specs/ShellBar.cy.tsx | 102 +++--- .../fiori/cypress/specs/ShellBarV2.cy.tsx | 299 +++++++++--------- packages/fiori/src/ShellBarV2.ts | 125 ++++++-- packages/fiori/src/ShellBarV2Template.tsx | 25 +- .../ShellBarSearchLegacyTemplate.tsx | 9 +- 5 files changed, 337 insertions(+), 223 deletions(-) diff --git a/packages/fiori/cypress/specs/ShellBar.cy.tsx b/packages/fiori/cypress/specs/ShellBar.cy.tsx index 3c04a6e7f6aa..9aeb4eb63545 100644 --- a/packages/fiori/cypress/specs/ShellBar.cy.tsx +++ b/packages/fiori/cypress/specs/ShellBar.cy.tsx @@ -600,7 +600,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -650,7 +650,7 @@ describe("Events", () => { // // ); - // cy.get("[ui5-shellbar]") + // cy.get("[ui5-shellbar-v2]") // .as("shellbar"); // // Set up event listener without preventing default @@ -689,7 +689,7 @@ describe("Events", () => { // // ); - // cy.get("[ui5-shellbar]") + // cy.get("[ui5-shellbar-v2]") // .as("shellbar"); // // Set up event listener that prevents default @@ -738,12 +738,12 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-button") .realClick(); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-popover") .should("have.prop", "open", true); @@ -756,7 +756,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -783,7 +783,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -807,7 +807,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -831,7 +831,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -858,7 +858,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -896,7 +896,7 @@ describe("Events", () => { item.addEventListener("click", cy.stub().as(`menuItemClick${item.getAttribute("data-key")}`)); }); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-button") .click(); @@ -906,7 +906,7 @@ describe("Events", () => { cy.get("@menuItemClickkey1") .should("have.been.calledOnce"); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-button") .click(); @@ -925,21 +925,21 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-search-field") .should("exist"); - cy.get("[ui5-shellbar]").invoke("prop", "showSearchField", false); + cy.get("[ui5-shellbar-v2]").invoke("prop", "showSearchField", false); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-search-field") .should("not.exist"); - cy.get("[ui5-shellbar]").invoke("prop", "showSearchField", true); + cy.get("[ui5-shellbar-v2]").invoke("prop", "showSearchField", true); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-search-field") .should("exist"); @@ -958,7 +958,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -984,12 +984,12 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-button") .realClick(); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-popover") .should("have.prop", "open", true); @@ -1005,7 +1005,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -1029,7 +1029,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -1066,29 +1066,29 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]").then(($shellbar) => { + cy.get("[ui5-shellbar-v2]").then(($shellbar) => { const shellbar = $shellbar[0] as HTMLElement; shellbar.addEventListener("ui5-notifications-click", (e: Event) => { e.preventDefault(); }); }); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-overflow-button") .realClick(); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-overflow-popover") .should("be.visible"); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-overflow-popover [ui5-list] [ui5-li]:nth-child(3)") .realClick(); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-overflow-popover") .should("be.visible"); @@ -1196,7 +1196,7 @@ describe("Keyboard Navigation", () => { cy.mount(); cy.wait(RESIZE_THROTTLE_RATE); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-logo-area") .should("not.exist"); @@ -1213,7 +1213,7 @@ describe("Keyboard Navigation", () => { cy.wait(RESIZE_THROTTLE_RATE); function placeAtStartOfInput() { - cy.get("[ui5-shellbar] [slot='searchField']") + cy.get("[ui5-shellbar-v2] [slot='searchField']") .shadow() .find("input") .then($input => { @@ -1221,7 +1221,7 @@ describe("Keyboard Navigation", () => { }); } function placeAtEndOfInput() { - cy.get("[ui5-shellbar] [slot='searchField']") + cy.get("[ui5-shellbar-v2] [slot='searchField']") .shadow() .find("input") .then($input => { @@ -1230,7 +1230,7 @@ describe("Keyboard Navigation", () => { }); } function placeInMiddleOfInput() { - cy.get("[ui5-shellbar] [slot='searchField']") + cy.get("[ui5-shellbar-v2] [slot='searchField']") .shadow() .find("input") .then($input => { @@ -1241,7 +1241,7 @@ describe("Keyboard Navigation", () => { } // Focus the search input - cy.get("[ui5-shellbar] [slot='searchField']") + cy.get("[ui5-shellbar-v2] [slot='searchField']") .realClick() .shadow() .find("input") @@ -1251,14 +1251,14 @@ describe("Keyboard Navigation", () => { // Press left arrow - should move focus away from input since cursor is at start cy.get("@nativeInput").type("{leftArrow}"); // Verify focus is now on the button - cy.get("[ui5-shellbar] [ui5-button]").should("be.focused"); + cy.get("[ui5-shellbar-v2] [ui5-button]").should("be.focused"); placeAtEndOfInput(); // Press right arrow - should move focus away from input since cursor is at end cy.get("@nativeInput").type("{rightArrow}"); // Verify focus is now on the ShellBarItem - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-custom-item") .should("be.focused"); @@ -1336,7 +1336,7 @@ describe("Component Behavior", () => { ); - cy.get("[ui5-shellbar]").then(($shellbar) => { + cy.get("[ui5-shellbar-v2]").then(($shellbar) => { $shellbar[0].accessibilityAttributes = { profile: { name: PROFILE_BTN_CUSTOM_TOOLTIP, @@ -1347,9 +1347,9 @@ describe("Component Behavior", () => { }; }); - cy.get("[ui5-shellbar]").should("have.prop", "_profileText", PROFILE_BTN_CUSTOM_TOOLTIP); + cy.get("[ui5-shellbar-v2]").should("have.prop", "_profileText", PROFILE_BTN_CUSTOM_TOOLTIP); - cy.get("[ui5-shellbar]").should("have.prop", "_logoText", LOGO_CUSTOM_TOOLTIP); + cy.get("[ui5-shellbar-v2]").should("have.prop", "_logoText", LOGO_CUSTOM_TOOLTIP); }); it("tests acc default roles", () => { @@ -1359,7 +1359,7 @@ describe("Component Behavior", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-logo-area") .should("have.attr", "role", "link"); @@ -1381,7 +1381,7 @@ describe("Component Behavior", () => { ); - cy.get("[ui5-shellbar]").then(($shellbar) => { + cy.get("[ui5-shellbar-v2]").then(($shellbar) => { $shellbar[0].accessibilityAttributes = { notifications: { hasPopup: NOTIFICATIONS_BTN_ARIA_HASPOPUP @@ -1389,7 +1389,7 @@ describe("Component Behavior", () => { }; }); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-bell-button") .shadow() @@ -1414,7 +1414,7 @@ describe("Component Behavior", () => { $item[0].addEventListener("click", cy.stub().as("menuItemClick")); }); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-button") .click(); @@ -1423,7 +1423,7 @@ describe("Component Behavior", () => { cy.get("@menuItemClick") .should("have.been.calledOnce"); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-popover") .should("have.prop", "open", true); @@ -1443,11 +1443,11 @@ describe("Component Behavior", () => { ); - cy.get("[ui5-shellbar]").should("exist"); + cy.get("[ui5-shellbar-v2]").should("exist"); cy.get("[slot='menuItems']").should("have.length", 2); - cy.get("[ui5-shellbar]").should(($shellbar) => { + cy.get("[ui5-shellbar-v2]").should(($shellbar) => { const shellbar = $shellbar[0] as any; expect(shellbar.menuItems).to.exist; expect(shellbar.menuItems.length).to.be.greaterThan(0); @@ -1457,14 +1457,14 @@ describe("Component Behavior", () => { $item[0].addEventListener("click", cy.stub().as("menuItemClick")); }); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-button") .should("exist") .should("be.visible") .realClick(); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-popover") .should("have.prop", "open", true); @@ -1477,7 +1477,7 @@ describe("Component Behavior", () => { cy.get("@menuItemClick") .should("have.been.calledOnce"); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-popover") .should("have.prop", "open", false); @@ -1493,7 +1493,7 @@ describe("Component Behavior", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(`[data-ui5-stable="schedule"]`) .should("exist"); @@ -1507,14 +1507,14 @@ describe("Component Behavior", () => { ); - cy.get("[ui5-shellbar-item]").each(($item) => { + cy.get("[ui5-shellbar-v2-item]").each(($item) => { const item = $item[0]; const icon = item.getAttribute("icon"); const stubAlias = icon === "accept" ? "acceptClick" : "alertClick"; item.addEventListener("click", cy.stub().as(stubAlias)); }); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(`.ui5-shellbar-custom-item[icon="accept"]`) .click(); @@ -1522,7 +1522,7 @@ describe("Component Behavior", () => { cy.get("@acceptClick") .should("have.been.calledOnce"); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(`.ui5-shellbar-custom-item[icon="alert"]`) .click(); diff --git a/packages/fiori/cypress/specs/ShellBarV2.cy.tsx b/packages/fiori/cypress/specs/ShellBarV2.cy.tsx index 0c9978deae50..610d4752a972 100644 --- a/packages/fiori/cypress/specs/ShellBarV2.cy.tsx +++ b/packages/fiori/cypress/specs/ShellBarV2.cy.tsx @@ -410,16 +410,17 @@ describe("Slots", () => { assertStartSeparatorVisibility(false); assertEndSeparatorVisibility(false); - // once items are hidden, both separators should be rendered with the last visible item - cy.get("#shellbar") - .shadow() - .find("div[id='content-2'] > .ui5-shellbar-separator-start") - .should("exist"); - - cy.get("#shellbar") - .shadow() - .find("div[id='content-6'] > .ui5-shellbar-separator-end") - .should("exist"); + // TODO: V2: separators are not rendered on S breakpoint at all + // // once items are hidden, both separators should be rendered with the last visible item + // cy.get("#shellbar") + // .shadow() + // .find("div[id='content-2'] > .ui5-shellbar-separator-start") + // .should("exist"); + + // cy.get("#shellbar") + // .shadow() + // .find("div[id='content-6'] > .ui5-shellbar-separator-end") + // .should("exist"); cy.viewport(1920, 1080); // both separators should be visible @@ -588,7 +589,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -615,7 +616,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -658,86 +659,86 @@ describe("Events", () => { .should("have.been.calledOnce"); }); - // it("Test search field clear event default behavior", () => { - // cy.mount( - // - // - // - // ); - - // cy.get("[ui5-shellbar]") - // .as("shellbar"); - - // // Set up event listener without preventing default - // cy.get("@shellbar") - // .then(shellbar => { - // shellbar.get(0).addEventListener("ui5-search-field-clear", cy.stub().as("searchFieldClear")); - // }); - - // // Trigger full width search mode by reducing viewport - // cy.viewport(400, 800); - - // // Manually call the cancel button handler - // cy.get("@shellbar").then(shellbar => { - // const shellbarInstance = shellbar.get(0); - // // Call the private method directly to simulate cancel button press - // shellbarInstance._handleCancelButtonPress(); - // }); - - // // Verify the event was fired - // cy.get("@searchFieldClear") - // .should("have.been.calledOnce"); - - // // Verify search field value is cleared (default behavior) - // cy.get("#search") - // .should("have.value", ""); - - // // Verify search is closed - // cy.get("@shellbar") - // .should("have.prop", "showSearchField", false); - // }); - - // it("Test search field clear event can be prevented", () => { - // cy.mount( - // - // - // - // ); - - // cy.get("[ui5-shellbar]") - // .as("shellbar"); - - // // Set up event listener that prevents default - // cy.get("@shellbar") - // .then(shellbar => { - // shellbar.get(0).addEventListener("ui5-search-field-clear", (event) => { - // event.preventDefault(); - // }); - // shellbar.get(0).addEventListener("ui5-search-field-clear", cy.stub().as("searchFieldClear")); - // }); - - // // Trigger full width search mode by reducing viewport - // cy.viewport(400, 800); - - // // Manually call the cancel button handler - // cy.get("@shellbar").then(shellbar => { - // const shellbarInstance = shellbar.get(0); - // // Call the private method directly to simulate cancel button press - // shellbarInstance._handleCancelButtonPress(); - // }); - - // // Verify the event was fired - // cy.get("@searchFieldClear") - // .should("have.been.calledOnce"); - - // // Verify search field value is preserved (due to preventDefault) - // cy.get("#search") - // .should("have.value", "test search text"); - - // // Verify search is closed - // cy.get("@shellbar") - // .should("have.prop", "showSearchField", false); - // }); + it.only("Test search field clear event default behavior", () => { + cy.mount( + + + + ); + + cy.get("[ui5-shellbar-v2]") + .as("shellbar"); + + // Set up event listener without preventing default + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-search-field-clear", cy.stub().as("searchFieldClear")); + }); + + // Trigger full width search mode by reducing viewport + cy.viewport(400, 800); + + // Manually call the cancel button handler + cy.get("@shellbar").then(shellbar => { + const shellbarInstance = shellbar.get(0); + // Call the private method directly to simulate cancel button press + shellbarInstance.handleCancelButtonClick(); + }); + + // Verify the event was fired + cy.get("@searchFieldClear") + .should("have.been.calledOnce"); + + // Verify search field value is cleared (default behavior) + cy.get("#search") + .should("have.value", ""); + + // Verify search is closed + cy.get("@shellbar") + .should("have.prop", "showSearchField", false); + }); + + it("Test search field clear event can be prevented", () => { + cy.mount( + + + + ); + + cy.get("[ui5-shellbar-v2]") + .as("shellbar"); + + // Set up event listener that prevents default + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-search-field-clear", (event) => { + event.preventDefault(); + }); + shellbar.get(0).addEventListener("ui5-search-field-clear", cy.stub().as("searchFieldClear")); + }); + + // Trigger full width search mode by reducing viewport + cy.viewport(400, 800); + + // Manually call the cancel button handler + cy.get("@shellbar").then(shellbar => { + const shellbarInstance = shellbar.get(0); + // Call the private method directly to simulate cancel button press + shellbarInstance.handleCancelButtonClick(); + }); + + // Verify the event was fired + cy.get("@searchFieldClear") + .should("have.been.calledOnce"); + + // Verify search field value is preserved (due to preventDefault) + cy.get("#search") + .should("have.value", "test search text"); + + // Verify search is closed + cy.get("@shellbar") + .should("have.prop", "showSearchField", false); + }); describe("Big screen", () => { beforeEach(() => { @@ -753,12 +754,12 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-button") .realClick(); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-popover") .should("have.prop", "open", true); @@ -771,7 +772,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -798,7 +799,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -822,7 +823,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -846,7 +847,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -873,7 +874,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -911,7 +912,7 @@ describe("Events", () => { item.addEventListener("click", cy.stub().as(`menuItemClick${item.getAttribute("data-key")}`)); }); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-button") .click(); @@ -921,7 +922,7 @@ describe("Events", () => { cy.get("@menuItemClickkey1") .should("have.been.calledOnce"); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-button") .click(); @@ -940,21 +941,21 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-search-field") .should("exist"); - cy.get("[ui5-shellbar]").invoke("prop", "showSearchField", false); + cy.get("[ui5-shellbar-v2]").invoke("prop", "showSearchField", false); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-search-field") .should("not.exist"); - cy.get("[ui5-shellbar]").invoke("prop", "showSearchField", true); + cy.get("[ui5-shellbar-v2]").invoke("prop", "showSearchField", true); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-search-field") .should("exist"); @@ -973,7 +974,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -999,12 +1000,12 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-button") .realClick(); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-popover") .should("have.prop", "open", true); @@ -1020,7 +1021,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -1044,7 +1045,7 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .as("shellbar"); cy.get("@shellbar") @@ -1081,29 +1082,29 @@ describe("Events", () => { ); - cy.get("[ui5-shellbar]").then(($shellbar) => { + cy.get("[ui5-shellbar-v2]").then(($shellbar) => { const shellbar = $shellbar[0] as HTMLElement; shellbar.addEventListener("ui5-notifications-click", (e: Event) => { e.preventDefault(); }); }); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-overflow-button") .realClick(); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-overflow-popover") .should("be.visible"); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-overflow-popover [ui5-list] [ui5-li]:nth-child(3)") .realClick(); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-overflow-popover") .should("be.visible"); @@ -1119,9 +1120,10 @@ describe("ButtonBadge in ShellBar", () => { ); - cy.get("#shellbarwithitems") + // V2: Badge is inside ShellBarV2Item's shadow DOM, not directly in ShellBar's shadow + cy.get("#test-item") .shadow() - .find(".ui5-shellbar-custom-item ui5-button-badge[slot='badge']") + .find("ui5-button-badge[slot='badge']") .should("exist") .should("have.attr", "text", "42"); }); @@ -1135,9 +1137,10 @@ describe("ButtonBadge in ShellBar", () => { cy.get("#test-invalidation-item").invoke("attr", "count", "3"); - cy.get("#test-invalidation") + // V2: Badge is inside ShellBarV2Item's shadow DOM + cy.get("#test-invalidation-item") .shadow() - .find(".ui5-shellbar-custom-item ui5-button-badge[slot='badge']") + .find("ui5-button-badge[slot='badge']") .should("have.attr", "text", "3"); }); @@ -1164,11 +1167,15 @@ describe("ButtonBadge in ShellBar", () => { cy.viewport(320, 800); + // Wait for overflow calculation to complete + cy.wait(RESIZE_THROTTLE_RATE); + cy.get("#shellbar-with-overflow") .shadow() .find(".ui5-shellbar-overflow-button") .should("be.visible"); + // V2: Overflow button badge - check if it's rendered cy.get("#shellbar-with-overflow") .shadow() .find(".ui5-shellbar-overflow-button ui5-button-badge[slot='badge']") @@ -1193,11 +1200,15 @@ describe("ButtonBadge in ShellBar", () => { cy.viewport(320, 800); + // Wait for overflow calculation to complete + cy.wait(RESIZE_THROTTLE_RATE); + cy.get("#shellbar-with-single-overflow") .shadow() .find(".ui5-shellbar-overflow-button") .should("be.visible"); + // V2: Overflow button badge - check if it's rendered cy.get("#shellbar-with-single-overflow") .shadow() .find(".ui5-shellbar-overflow-button ui5-button-badge[slot='badge']") @@ -1211,7 +1222,7 @@ describe("Keyboard Navigation", () => { cy.mount(); cy.wait(RESIZE_THROTTLE_RATE); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-logo-area") .should("not.exist"); @@ -1228,7 +1239,7 @@ describe("Keyboard Navigation", () => { cy.wait(RESIZE_THROTTLE_RATE); function placeAtStartOfInput() { - cy.get("[ui5-shellbar] [slot='searchField']") + cy.get("[ui5-shellbar-v2] [slot='searchField']") .shadow() .find("input") .then($input => { @@ -1236,7 +1247,7 @@ describe("Keyboard Navigation", () => { }); } function placeAtEndOfInput() { - cy.get("[ui5-shellbar] [slot='searchField']") + cy.get("[ui5-shellbar-v2] [slot='searchField']") .shadow() .find("input") .then($input => { @@ -1245,7 +1256,7 @@ describe("Keyboard Navigation", () => { }); } function placeInMiddleOfInput() { - cy.get("[ui5-shellbar] [slot='searchField']") + cy.get("[ui5-shellbar-v2] [slot='searchField']") .shadow() .find("input") .then($input => { @@ -1256,7 +1267,7 @@ describe("Keyboard Navigation", () => { } // Focus the search input - cy.get("[ui5-shellbar] [slot='searchField']") + cy.get("[ui5-shellbar-v2] [slot='searchField']") .realClick() .shadow() .find("input") @@ -1266,14 +1277,14 @@ describe("Keyboard Navigation", () => { // Press left arrow - should move focus away from input since cursor is at start cy.get("@nativeInput").type("{leftArrow}"); // Verify focus is now on the button - cy.get("[ui5-shellbar] [ui5-button]").should("be.focused"); + cy.get("[ui5-shellbar-v2] [ui5-button]").should("be.focused"); placeAtEndOfInput(); // Press right arrow - should move focus away from input since cursor is at end cy.get("@nativeInput").type("{rightArrow}"); // Verify focus is now on the ShellBarItem - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-custom-item") .should("be.focused"); @@ -1351,7 +1362,7 @@ describe("Component Behavior", () => { ); - cy.get("[ui5-shellbar]").then(($shellbar) => { + cy.get("[ui5-shellbar-v2]").then(($shellbar) => { $shellbar[0].accessibilityAttributes = { profile: { name: PROFILE_BTN_CUSTOM_TOOLTIP, @@ -1362,9 +1373,9 @@ describe("Component Behavior", () => { }; }); - cy.get("[ui5-shellbar]").should("have.prop", "_profileText", PROFILE_BTN_CUSTOM_TOOLTIP); + cy.get("[ui5-shellbar-v2]").should("have.prop", "_profileText", PROFILE_BTN_CUSTOM_TOOLTIP); - cy.get("[ui5-shellbar]").should("have.prop", "_logoText", LOGO_CUSTOM_TOOLTIP); + cy.get("[ui5-shellbar-v2]").should("have.prop", "_logoText", LOGO_CUSTOM_TOOLTIP); }); it("tests acc default roles", () => { @@ -1374,7 +1385,7 @@ describe("Component Behavior", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-logo-area") .should("have.attr", "role", "link"); @@ -1396,7 +1407,7 @@ describe("Component Behavior", () => { ); - cy.get("[ui5-shellbar]").then(($shellbar) => { + cy.get("[ui5-shellbar-v2]").then(($shellbar) => { $shellbar[0].accessibilityAttributes = { notifications: { hasPopup: NOTIFICATIONS_BTN_ARIA_HASPOPUP @@ -1404,7 +1415,7 @@ describe("Component Behavior", () => { }; }); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-bell-button") .shadow() @@ -1429,7 +1440,7 @@ describe("Component Behavior", () => { $item[0].addEventListener("click", cy.stub().as("menuItemClick")); }); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-button") .click(); @@ -1438,7 +1449,7 @@ describe("Component Behavior", () => { cy.get("@menuItemClick") .should("have.been.calledOnce"); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-popover") .should("have.prop", "open", true); @@ -1458,11 +1469,11 @@ describe("Component Behavior", () => { ); - cy.get("[ui5-shellbar]").should("exist"); + cy.get("[ui5-shellbar-v2]").should("exist"); cy.get("[slot='menuItems']").should("have.length", 2); - cy.get("[ui5-shellbar]").should(($shellbar) => { + cy.get("[ui5-shellbar-v2]").should(($shellbar) => { const shellbar = $shellbar[0] as any; expect(shellbar.menuItems).to.exist; expect(shellbar.menuItems.length).to.be.greaterThan(0); @@ -1472,14 +1483,14 @@ describe("Component Behavior", () => { $item[0].addEventListener("click", cy.stub().as("menuItemClick")); }); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-button") .should("exist") .should("be.visible") .realClick(); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-popover") .should("have.prop", "open", true); @@ -1492,7 +1503,7 @@ describe("Component Behavior", () => { cy.get("@menuItemClick") .should("have.been.calledOnce"); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-menu-popover") .should("have.prop", "open", false); @@ -1508,7 +1519,7 @@ describe("Component Behavior", () => { ); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(`[data-ui5-stable="schedule"]`) .should("exist"); @@ -1522,14 +1533,14 @@ describe("Component Behavior", () => { ); - cy.get("[ui5-shellbar-item]").each(($item) => { + cy.get("[ui5-shellbar-v2-item]").each(($item) => { const item = $item[0]; const icon = item.getAttribute("icon"); const stubAlias = icon === "accept" ? "acceptClick" : "alertClick"; item.addEventListener("click", cy.stub().as(stubAlias)); }); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(`.ui5-shellbar-custom-item[icon="accept"]`) .click(); @@ -1537,7 +1548,7 @@ describe("Component Behavior", () => { cy.get("@acceptClick") .should("have.been.calledOnce"); - cy.get("[ui5-shellbar]") + cy.get("[ui5-shellbar-v2]") .shadow() .find(`.ui5-shellbar-custom-item[icon="alert"]`) .click(); diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index b1546025d96c..8e9910bc8723 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -11,6 +11,8 @@ import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delega import { getScopedVarName } from "@ui5/webcomponents-base/dist/CustomElementsScopeUtils.js"; import arraysAreEqual from "@ui5/webcomponents-base/dist/util/arraysAreEqual.js"; import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; +import { renderFinished } from "@ui5/webcomponents-base"; +import throttle from "@ui5/webcomponents-base/dist/util/throttle.js"; import type { IButton } from "@ui5/webcomponents/dist/Button.js"; import Button from "@ui5/webcomponents/dist/Button.js"; @@ -38,8 +40,8 @@ import ShellBarV2Actions from "./shellbarv2/ShellBarActions.js"; import ShellBarV2Content from "./shellbarv2/ShellBarContent.js"; import ShellBarV2Overflow from "./shellbarv2/ShellBarOverflow.js"; import ShellBarV2Breakpoint from "./shellbarv2/ShellBarBreakpoint.js"; -import ShellBarV2ItemNavigation from "./shellbarv2/ShellBarItemNavigation.js"; import ShellBarV2Accessibility from "./shellbarv2/ShellBarAccessibility.js"; +import ShellBarV2ItemNavigation from "./shellbarv2/ShellBarItemNavigation.js"; import ShellBarV2Item from "./ShellBarV2Item.js"; import ShellBarSpacer from "./ShellBarSpacer.js"; @@ -47,12 +49,13 @@ import type ShellBarBranding from "./ShellBarBranding.js"; import type { ShellBarV2ActionItem } from "./shellbarv2/ShellBarActions.js"; import type { ShellBarV2BreakpointType } from "./shellbarv2/ShellBarBreakpoint.js"; import type { ShellBarV2OverflowResult } from "./shellbarv2/ShellBarOverflow.js"; + import type { - ShellBarV2AccessibilityAttributes, ShellBarV2AccessibilityInfo, + ShellBarV2AccessibilityAttributes, + ShellBarV2AreaAccessibilityAttributes, ShellBarV2LogoAccessibilityAttributes, ShellBarV2ProfileAccessibilityAttributes, - ShellBarV2AreaAccessibilityAttributes, } from "./shellbarv2/ShellBarAccessibility.js"; import { @@ -64,7 +67,6 @@ import { SHELLBAR_OVERFLOW, SHELLBAR_ADDITIONAL_CONTEXT, } from "./generated/i18n/i18n-defaults.js"; -import { renderFinished } from "@ui5/webcomponents-base"; type ShellBarV2MenuButtonClickEventDetail = { menuButton: HTMLElement; @@ -447,7 +449,8 @@ class ShellBarV2 extends UI5Element { @i18n("@ui5/webcomponents-fiori") static i18nBundle: I18nBundle; - private handleResizeBound: ResizeObserverCallback = this.handleResize.bind(this); + private readonly RESIZE_THROTTLE_RATE = 200; // ms + private handleResizeBound: ResizeObserverCallback = throttle(this.handleResize.bind(this), this.RESIZE_THROTTLE_RATE); itemNavigation = new ShellBarV2ItemNavigation({ getDomRef: () => this.getDomRef() || null, @@ -459,8 +462,8 @@ class ShellBarV2 extends UI5Element { overflowAdaptor = new ShellBarV2Overflow(); accessibilityAdaptor = new ShellBarV2Accessibility(); - _searchAdaptor = new ShellBarV2Search(this.getSearchDeps()); - _searchAdaptorLegacy = new ShellBarV2SearchLegacy({ + private _searchAdaptor = new ShellBarV2Search(this.getSearchDeps()); + private _searchAdaptorLegacy = new ShellBarV2SearchLegacy({ ...this.getSearchDeps(), getDisableSearchCollapse: () => this.disableSearchCollapse, }); @@ -593,9 +596,12 @@ class ShellBarV2 extends UI5Element { getActionText(actionId: string): string { const texts: Record = { - "notifications": "Notifications", + "profile": this._profileText, + "overflow": this._overflowText, "assistant": "Assistant", - "search-button": "Search", + "search-button": this._searchText, + "notifications": this._notificationsText, + "product-switch": this._productsText, }; return texts[actionId] || actionId; } @@ -759,6 +765,47 @@ class ShellBarV2 extends UI5Element { this.overflowPopoverOpen = false; } + get overflowItems() { + return this.overflowAdaptor.getOverflowItems({ + actions: this.actions, + customItems: this.items, + hiddenItemsIds: this.hiddenItemsIds, + }); + } + + /** + * Returns badge text for overflow button. + * Shows count if only one item with count is overflowed, otherwise shows attention dot. + */ + get overflowBadge(): string | undefined { + const overflowItems = this.overflowAdaptor.getOverflowItems({ + actions: this.actions, + customItems: this.items, + hiddenItemsIds: this.hiddenItemsIds, + }); + + const itemsWithCount = overflowItems.filter(item => { + if (item.type === "action") { + return item.data.count; + } + return (item.data as ShellBarV2Item).count; + }); + + if (itemsWithCount.length === 1) { + const item = itemsWithCount[0]; + if (item.type === "action") { + return item.data.count; + } + return (item.data as ShellBarV2Item).count; + } + + if (itemsWithCount.length > 1) { + return " "; // Attention dot + } + + return undefined; + } + /* ========================================================================= Search Management ============================================================================ */ @@ -909,14 +956,6 @@ class ShellBarV2 extends UI5Element { return this.searchField.length > 0; } - get overflowItems() { - return this.overflowAdaptor.getOverflowItems({ - actions: this.actions, - customItems: this.items, - hiddenItemsIds: this.hiddenItemsIds, - }); - } - get search() { return this.searchField.length ? this.searchField[0] : null; } @@ -929,13 +968,63 @@ class ShellBarV2 extends UI5Element { return false; } + /** + * Returns the `logo` DOM ref. + * @public + * @default null + * @since 1.0.0-rc.16 + */ + get logoDomRef(): HTMLElement | null { + return this.shadowRoot!.querySelector(`*[data-ui5-stable="logo"]`); + } + + /** + * Returns the `notifications` icon DOM ref. + * @public + * @default null + * @since 1.0.0-rc.16 + */ + get notificationsDomRef(): HTMLElement | null { + return this.shadowRoot!.querySelector(`*[data-ui5-stable="notifications"]`); + } + + /** + * Returns the `overflow` icon DOM ref. + * @public + * @default null + * @since 1.0.0-rc.16 + */ + get overflowDomRef(): HTMLElement | null { + return this.shadowRoot!.querySelector(`*[data-ui5-stable="overflow"]`); + } + + /** + * Returns the `profile` icon DOM ref. + * @public + * @default null + * @since 1.0.0-rc.16 + */ + get profileDomRef(): HTMLElement | null { + return this.shadowRoot!.querySelector(`*[data-ui5-stable="profile"]`); + } + + /** + * Returns the `product-switch` icon DOM ref. + * @public + * @default null + * @since 1.0.0-rc.16 + */ + get productSwitchDomRef(): HTMLElement | null { + return this.shadowRoot!.querySelector(`*[data-ui5-stable="product-switch"]`); + } + /** * Returns the search button DOM reference. * @public */ async getSearchButtonDomRef(): Promise { await renderFinished(); - return this.shadowRoot!.querySelector(".ui5-shellbar-search-button"); + return this.shadowRoot!.querySelector(`*[data-ui5-stable="toggle-search"]`); } /* ========================================================================= diff --git a/packages/fiori/src/ShellBarV2Template.tsx b/packages/fiori/src/ShellBarV2Template.tsx index dbc9d931ea2c..71ae3daaead7 100644 --- a/packages/fiori/src/ShellBarV2Template.tsx +++ b/packages/fiori/src/ShellBarV2Template.tsx @@ -55,7 +55,7 @@ export default function ShellBarV2Template(this: ShellBarV2) { {this.hasContent && (
@@ -125,7 +125,7 @@ export default function ShellBarV2Template(this: ShellBarV2) { icon="bell" design="Transparent" onClick={this._handleNotificationsClick} - tooltip={this._notificationsText} + tooltip={this.getActionText("notifications")} accessibilityAttributes={this.accInfo.notifications.accessibilityAttributes} > {this.getAction("notifications")?.count && ( @@ -138,7 +138,7 @@ export default function ShellBarV2Template(this: ShellBarV2) {
{!item.inOverflow ? : null}
@@ -149,23 +149,33 @@ export default function ShellBarV2Template(this: ShellBarV2) { {this.showOverflowButton && ( )} {this.getAction("profile") && ( )} + {/* Custom Items */} {this.items.map(item => (
Branding // Create element from template el = elementTemplates[id.replace(/-/g, '')](); const shellbarId = id.includes('V1') ? 'shellbarV1' : 'shellbarV2'; - document.getElementById(shellbarId).appendChild(el); + const shellbar = document.getElementById(shellbarId); + + // Special handling for spacer: insert after content-3 to split start/end groups + if (id.includes('spacer')) { + const insertAfter = document.getElementById(id.includes('V1') ? 'contentV1-3' : 'contentV2-3'); + if (insertAfter && insertAfter.nextSibling) { + shellbar.insertBefore(el, insertAfter.nextSibling); + } else { + shellbar.appendChild(el); + } + } else { + shellbar.appendChild(el); + } } } } else { From 83b54147c15f55b886e887d13b68c6577949f414 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Fri, 7 Nov 2025 12:32:38 +0200 Subject: [PATCH 31/57] overflow fixes --- .../fiori/cypress/specs/ShellBarV2.cy.tsx | 26 ++- packages/fiori/src/ShellBarV2.ts | 73 +++++--- packages/fiori/src/ShellBarV2Item.ts | 6 - packages/fiori/src/ShellBarV2ItemTemplate.tsx | 2 - packages/fiori/src/ShellBarV2Template.tsx | 32 ++-- .../fiori/src/shellbarv2/ShellBarOverflow.ts | 26 ++- .../ShellBarSearchLegacyTemplate.tsx | 6 +- .../templates/ShellBarSearchTemplate.tsx | 4 +- packages/fiori/src/themes/ShellBarV2.css | 61 +++---- packages/fiori/src/themes/ShellBarV2Item.css | 2 +- packages/fiori/test/pages/ShellBarV2.html | 156 +----------------- .../fiori/test/pages/ShellBar_Comparison.html | 13 +- 12 files changed, 141 insertions(+), 266 deletions(-) diff --git a/packages/fiori/cypress/specs/ShellBarV2.cy.tsx b/packages/fiori/cypress/specs/ShellBarV2.cy.tsx index 80210e0d1c13..fbaeddb5a47c 100644 --- a/packages/fiori/cypress/specs/ShellBarV2.cy.tsx +++ b/packages/fiori/cypress/specs/ShellBarV2.cy.tsx @@ -678,12 +678,10 @@ describe("Events", () => { // Trigger full width search mode by reducing viewport cy.viewport(400, 800); - // Manually call the cancel button handler - cy.get("@shellbar").then(shellbar => { - const shellbarInstance = shellbar.get(0); - // Call the private method directly to simulate cancel button press - shellbarInstance.handleCancelButtonClick(); - }); + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-cancel-button") + .click(); // Verify the event was fired cy.get("@searchFieldClear") @@ -720,12 +718,10 @@ describe("Events", () => { // Trigger full width search mode by reducing viewport cy.viewport(400, 800); - // Manually call the cancel button handler - cy.get("@shellbar").then(shellbar => { - const shellbarInstance = shellbar.get(0); - // Call the private method directly to simulate cancel button press - shellbarInstance.handleCancelButtonClick(); - }); + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-cancel-button") + .click(); // Verify the event was fired cy.get("@searchFieldClear") @@ -810,7 +806,7 @@ describe("Events", () => { cy.get("@shellbar") .shadow() .find("[data-profile-btn]") - .click({ force: true }); + .click({ force: true }); cy.get("@profileClick") .should("have.been.calledOnce"); @@ -1032,7 +1028,7 @@ describe("Events", () => { cy.get("@shellbar") .shadow() .find("[data-profile-btn]") - .click({ force: true }); + .click({ force: true }); cy.get("@profileClick") .should("have.been.calledOnce"); @@ -1101,7 +1097,7 @@ describe("Events", () => { cy.get("[ui5-shellbar-v2]") .shadow() - .find(".ui5-shellbar-overflow-popover [ui5-list] [ui5-li]:nth-child(3)") + .find(".ui5-shellbar-overflow-popover [ui5-list] [ui5-li]:nth-child(1)") .realClick(); cy.get("[ui5-shellbar-v2]") diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index 2f23dde06f21..5c86f9f4fa4f 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -19,7 +19,7 @@ import Button from "@ui5/webcomponents/dist/Button.js"; import Icon from "@ui5/webcomponents/dist/Icon.js"; import Popover from "@ui5/webcomponents/dist/Popover.js"; import Menu from "@ui5/webcomponents/dist/Menu.js"; -import List from "@ui5/webcomponents/dist/List.js"; +import List, { type ListItemClickEventDetail } from "@ui5/webcomponents/dist/List.js"; import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js"; import "@ui5/webcomponents-icons/dist/bell.js"; import "@ui5/webcomponents-icons/dist/grid.js"; @@ -468,8 +468,10 @@ class ShellBarV2 extends UI5Element { getDisableSearchCollapse: () => this.disableSearchCollapse, }); + private skipNextUpdateOverflow = false; + /* ========================================================================= - Legacy Members + Legacy Members ============================================================================ */ /** @@ -535,7 +537,7 @@ class ShellBarV2 extends UI5Element { legacyAdaptor?: ShellBarV2Legacy; /* ========================================================================= - Lifecycle Methods + Lifecycle Methods ============================================================================ */ onEnterDOM() { @@ -571,7 +573,7 @@ class ShellBarV2 extends UI5Element { } /* ========================================================================= - Actions Management + Actions Management ============================================================================ */ /** @@ -607,7 +609,7 @@ class ShellBarV2 extends UI5Element { } /* ========================================================================= - Breakpoint Management + Breakpoint Management ============================================================================ */ /** * Updates the breakpoint by delegating calculation to controller. @@ -623,40 +625,43 @@ class ShellBarV2 extends UI5Element { } /* ========================================================================= - Notifications Management + Notifications Management ============================================================================ */ - _handleNotificationsClick() { + handleNotificationsClick() { const notificationsBtn = this.shadowRoot!.querySelector )}
@@ -208,7 +211,7 @@ export default function ShellBarV2Template(this: ShellBarV2) { hideArrow={true} horizontalAlign="End" > - + {this.overflowItems.map(item => { if (item.type === "action") { return ( @@ -217,7 +220,6 @@ export default function ShellBarV2Template(this: ShellBarV2) { icon={item.data.icon ? `sap-icon://${item.data.icon}` : ""} data-action-id={item.id} type="Active" - onClick={this.handleOverflowItemClick} > {this.getActionText(item.id)} diff --git a/packages/fiori/src/shellbarv2/ShellBarOverflow.ts b/packages/fiori/src/shellbarv2/ShellBarOverflow.ts index 3676812b4682..d92346a67abf 100644 --- a/packages/fiori/src/shellbarv2/ShellBarOverflow.ts +++ b/packages/fiori/src/shellbarv2/ShellBarOverflow.ts @@ -49,30 +49,41 @@ class ShellBarV2Overflow { // Build hidable items from state const sortedItems = this.buildHidableItems(params); - // First, show all items + // First, hide overflow button + setVisible(".ui5-shellbar-overflow-button", false); + + // show all items sortedItems.forEach(item => { setVisible(item.selector, true); }); const hiddenItemsIds: string[] = []; let showOverflowButton = false; + let itemToHide = null; // Iteratively hide items until no overflow - for (let i = 0; i < sortedItems.length; i++) { - const item = sortedItems[i]; + for (let indexToHide = 0; indexToHide < sortedItems.length; indexToHide++) { + itemToHide = sortedItems[indexToHide]; if (!this.isOverflowing(overflowOuter, overflowInner)) { break; // No more overflow, stop hiding } - setVisible(item.selector, false); - hiddenItemsIds.push(item.id); + setVisible(itemToHide.selector, false); + hiddenItemsIds.push(itemToHide.id); - if (item.showInOverflow) { + if (itemToHide.showInOverflow) { + // show overflow button to account in isOverflowing calculation + setVisible(".ui5-shellbar-overflow-button", true); showOverflowButton = true; } } + // never hide just one item as overflow button also accounts for one item + if (hiddenItemsIds.length === 1 && itemToHide) { + hiddenItemsIds.push(itemToHide.id); + } + return { hiddenItemsIds, showOverflowButton, @@ -177,8 +188,7 @@ class ShellBarV2Overflow { // Custom items hide with actions (range: 100-199) // Custom items show in overflow popover when hidden customItems.forEach((item, index) => { - const slotName = (item as any)._individualSlot as string; - const selector = `[data-ui5-stable="${slotName}"]`; + const selector = `[data-ui5-stable="${item.stableDomRef}"]`; let hideOrder = 3 + index + (showSearchField ? 100 : 0); if (hiddenItemsIds.includes(item._id)) { diff --git a/packages/fiori/src/shellbarv2/templates/ShellBarSearchLegacyTemplate.tsx b/packages/fiori/src/shellbarv2/templates/ShellBarSearchLegacyTemplate.tsx index ae6de8b894e9..9149a193ea87 100644 --- a/packages/fiori/src/shellbarv2/templates/ShellBarSearchLegacyTemplate.tsx +++ b/packages/fiori/src/shellbarv2/templates/ShellBarSearchLegacyTemplate.tsx @@ -5,7 +5,7 @@ function ShellBarV2SearchField(this: ShellBarV2) { return ( // .ui5-shellbar-search-field-area is used to measure the width of // the search field. It must be present even if the search is in full-width mode. -
+
{this.showSearchField && !this.showFullWidthSearch && (
@@ -22,7 +22,7 @@ function ShellBarV2SearchFieldFullWidth(this: ShellBarV2) {
diff --git a/packages/fiori/src/shellbarv2/ShellBarContent.ts b/packages/fiori/src/shellbarv2/ShellBarContent.ts deleted file mode 100644 index e04e4ac78fd7..000000000000 --- a/packages/fiori/src/shellbarv2/ShellBarContent.ts +++ /dev/null @@ -1,99 +0,0 @@ -interface ShellBarContentParams { - content: readonly HTMLElement[]; - isSBreakPoint: boolean; - hiddenItemIds: readonly string[]; -} - -interface ContentGroup { - start: HTMLElement[]; - end: HTMLElement[]; -} - -interface SeparatorConfig { - showStartSeparator: boolean; - showEndSeparator: boolean; -} - -interface PackedSeparatorInfo { - shouldPack: boolean; -} - -/** - * Handles content area logic: splitting into start/end groups and separator visibility. - * Pure logic - no side effects. - */ -class ShellBarContent { - /** - * Splits content into start and end groups based on spacer element. - * Items before spacer = start (left-aligned) - * Items after spacer = end (right-aligned) - * Without spacer, all items are start content. - * - * Spacer can be detected by: - * - Component: - * - Attribute:
- */ - splitContent(content: readonly HTMLElement[]): ContentGroup { - const spacerIndex = content.findIndex( - child => child.hasAttribute("ui5-shellbar-spacer"), - ); - - if (spacerIndex === -1) { - return { start: [...content], end: [] }; - } - - return { - start: content.slice(0, spacerIndex), - end: content.slice(spacerIndex + 1), - }; - } - - /** - * Calculates whether separators should be shown. - * Separators appear between content groups when at least one item is visible. - * Hidden on S breakpoint (mobile). - */ - getSeparatorConfig(params: ShellBarContentParams): SeparatorConfig { - if (params.isSBreakPoint) { - return { showStartSeparator: false, showEndSeparator: false }; - } - - const { start, end } = this.splitContent(params.content); - - return { - showStartSeparator: start.some(item => !params.hiddenItemIds.includes((item as any)._individualSlot as string)), - showEndSeparator: end.some(item => !params.hiddenItemIds.includes((item as any)._individualSlot as string)), - }; - } - - /** - * Determines if a separator should be packed with this item. - * Separators are packed with the last visible item in a group. - * When that item hides, separator hides with it for proper measurement. - * - * Only applies on S breakpoint or when item is the last visible. - */ - shouldPackSeparator( - item: HTMLElement, - group: HTMLElement[], - hiddenIds: readonly string[], - isSBreakPoint: boolean, - ): PackedSeparatorInfo { - if (isSBreakPoint) { - return { shouldPack: false }; - } - - const isHidden = hiddenIds.includes((item as any)._individualSlot as string); - const isLastItem = group.at(-1) === item; - - return { shouldPack: isHidden && isLastItem }; - } -} - -export default ShellBarContent; -export type { - ShellBarContentParams, - ContentGroup, - SeparatorConfig, - PackedSeparatorInfo, -}; From da003831c1492b969e854db56dd756058e4d2572 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Mon, 10 Nov 2025 09:08:54 +0200 Subject: [PATCH 40/57] fix: remove shellbar breakpoint class --- packages/fiori/src/ShellBarV2.ts | 27 ++++++++++++++---- .../src/shellbarv2/ShellBarBreakpoint.ts | 28 ------------------- 2 files changed, 22 insertions(+), 33 deletions(-) delete mode 100644 packages/fiori/src/shellbarv2/ShellBarBreakpoint.ts diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index 2ce43f6b8b71..7cd1fb8ea258 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -38,7 +38,6 @@ import ShellBarV2Search from "./shellbarv2/ShellBarSearch.js"; import ShellBarV2SearchLegacy from "./shellbarv2/ShellBarSearchLegacy.js"; import ShellBarV2Actions from "./shellbarv2/ShellBarActions.js"; import ShellBarV2Overflow from "./shellbarv2/ShellBarOverflow.js"; -import ShellBarV2Breakpoint from "./shellbarv2/ShellBarBreakpoint.js"; import ShellBarV2Accessibility from "./shellbarv2/ShellBarAccessibility.js"; import ShellBarV2ItemNavigation from "./shellbarv2/ShellBarItemNavigation.js"; @@ -46,7 +45,6 @@ import ShellBarV2Item from "./ShellBarV2Item.js"; import ShellBarSpacer from "./ShellBarSpacer.js"; import type ShellBarBranding from "./ShellBarBranding.js"; import type { ShellBarV2ActionItem } from "./shellbarv2/ShellBarActions.js"; -import type { ShellBarV2BreakpointType } from "./shellbarv2/ShellBarBreakpoint.js"; import type { ShellBarV2OverflowResult } from "./shellbarv2/ShellBarOverflow.js"; import type { @@ -68,6 +66,8 @@ import { SHELLBAR_NOTIFICATIONS_NO_COUNT, } from "./generated/i18n/i18n-defaults.js"; +type ShellBarV2Breakpoint = "S" | "M" | "L" | "XL" | "XXL"; + type ShellBarV2MenuButtonClickEventDetail = { menuButton: HTMLElement; }; @@ -387,7 +387,7 @@ class ShellBarV2 extends UI5Element { * @private */ @property() - breakpointSize: ShellBarV2BreakpointType = "M"; + breakpointSize: ShellBarV2Breakpoint = "M"; /** * Actions computed from controllers. @@ -452,11 +452,20 @@ class ShellBarV2 extends UI5Element { private readonly RESIZE_THROTTLE_RATE = 200; // ms private handleResizeBound: ResizeObserverCallback = throttle(this.handleResize.bind(this), this.RESIZE_THROTTLE_RATE); + // Breakpoint constants + private readonly breakpoints = [599, 1023, 1439, 1919, 10000]; + private readonly breakpointMap: Record = { + 599: "S", + 1023: "M", + 1439: "L", + 1919: "XL", + 10000: "XXL", + }; + itemNavigation = new ShellBarV2ItemNavigation({ getDomRef: () => this.getDomRef() || null, }); - breakpoint = new ShellBarV2Breakpoint(); actionsAdaptor = new ShellBarV2Actions(); overflowAdaptor = new ShellBarV2Overflow(); accessibilityAdaptor = new ShellBarV2Accessibility(); @@ -609,13 +618,21 @@ class ShellBarV2 extends UI5Element { /* ========================================================================= Breakpoint Management ============================================================================ */ + /** + * Calculate breakpoint based on width + */ + private calculateBreakpoint(width: number): ShellBarV2Breakpoint { + const bp = this.breakpoints.find(b => width <= b) || 10000; + return this.breakpointMap[bp]; + } + /** * Updates the breakpoint by delegating calculation to controller. * This is the coordination logic - gather data, delegate, apply result. */ private updateBreakpoint() { const width = this.getBoundingClientRect().width; - const breakpoint = this.breakpoint.calculate({ width }); + const breakpoint = this.calculateBreakpoint(width); if (this.breakpointSize !== breakpoint) { this.breakpointSize = breakpoint; diff --git a/packages/fiori/src/shellbarv2/ShellBarBreakpoint.ts b/packages/fiori/src/shellbarv2/ShellBarBreakpoint.ts deleted file mode 100644 index 9b347ceec842..000000000000 --- a/packages/fiori/src/shellbarv2/ShellBarBreakpoint.ts +++ /dev/null @@ -1,28 +0,0 @@ -type ShellBarV2BreakpointType = "S" | "M" | "L" | "XL" | "XXL"; - -interface ShellBarV2BreakpointParams { - width: number; -} - -class ShellBarV2Breakpoint { - private readonly breakpoints = [599, 1023, 1439, 1919, 10000]; - private readonly breakpointMap: Record = { - 599: "S", - 1023: "M", - 1439: "L", - 1919: "XL", - 10000: "XXL", - }; - - calculate(params: ShellBarV2BreakpointParams): ShellBarV2BreakpointType { - const { width } = params; - const bp = this.breakpoints.find(b => width <= b) || 10000; - return this.breakpointMap[bp]; - } -} - -export default ShellBarV2Breakpoint; -export type { - ShellBarV2BreakpointType, - ShellBarV2BreakpointParams, -}; From 79b53ef3e0300e92f0ddf87a269c9071bfe6f7a3 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Mon, 10 Nov 2025 10:22:12 +0200 Subject: [PATCH 41/57] fix: remove shellbar actions class --- packages/fiori/src/ShellBarV2.ts | 86 +++++++++++++------ .../fiori/src/shellbarv2/ShellBarActions.ts | 62 ------------- .../fiori/src/shellbarv2/ShellBarOverflow.ts | 17 ++-- 3 files changed, 70 insertions(+), 95 deletions(-) delete mode 100644 packages/fiori/src/shellbarv2/ShellBarActions.ts diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index 7cd1fb8ea258..02cdf3242eb0 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -36,7 +36,6 @@ import type { IShellBarSearchController } from "./shellbarv2/IShellBarSearchCont import ShellBarV2Legacy from "./shellbarv2/ShellBarLegacy.js"; import ShellBarV2Search from "./shellbarv2/ShellBarSearch.js"; import ShellBarV2SearchLegacy from "./shellbarv2/ShellBarSearchLegacy.js"; -import ShellBarV2Actions from "./shellbarv2/ShellBarActions.js"; import ShellBarV2Overflow from "./shellbarv2/ShellBarOverflow.js"; import ShellBarV2Accessibility from "./shellbarv2/ShellBarAccessibility.js"; import ShellBarV2ItemNavigation from "./shellbarv2/ShellBarItemNavigation.js"; @@ -44,7 +43,6 @@ import ShellBarV2ItemNavigation from "./shellbarv2/ShellBarItemNavigation.js"; import ShellBarV2Item from "./ShellBarV2Item.js"; import ShellBarSpacer from "./ShellBarSpacer.js"; import type ShellBarBranding from "./ShellBarBranding.js"; -import type { ShellBarV2ActionItem } from "./shellbarv2/ShellBarActions.js"; import type { ShellBarV2OverflowResult } from "./shellbarv2/ShellBarOverflow.js"; import type { @@ -68,6 +66,24 @@ import { type ShellBarV2Breakpoint = "S" | "M" | "L" | "XL" | "XXL"; +const ACTION_IDS = { + SEARCH: "search", + PROFILE: "profile", + ASSISTANT: "assistant", + NOTIFICATIONS: "notifications", + PRODUCT_SWITCH: "product-switch", + OVERFLOW: "overflow", +} as const; + +type ActionId = typeof ACTION_IDS[keyof typeof ACTION_IDS]; + +type ShellBarV2ActionItem = { + id: ActionId; + icon?: string; + count?: string; + visible: boolean; +}; + type ShellBarV2MenuButtonClickEventDetail = { menuButton: HTMLElement; }; @@ -466,7 +482,6 @@ class ShellBarV2 extends UI5Element { getDomRef: () => this.getDomRef() || null, }); - actionsAdaptor = new ShellBarV2Actions(); overflowAdaptor = new ShellBarV2Overflow(); accessibilityAdaptor = new ShellBarV2Accessibility(); @@ -583,34 +598,50 @@ class ShellBarV2 extends UI5Element { ============================================================================ */ /** - * Updates actions by delegating to controller. - * Demonstrates: Component gathers params, controller returns data, component applies. + * Updates actions array based on current state. */ private updateActions() { - const params = { - hasSearch: this.hasSearchField, - showNotifications: this.showNotifications, - notificationsCount: this.notificationsCount, - showProductSwitch: this.showProductSwitch, - hasAssistant: this.hasAssistant, - showProfile: this.hasProfile, - }; - - this.actions = this.actionsAdaptor.getActions(params); + this.actions = [ + { + id: ACTION_IDS.SEARCH, + visible: this.hasSearchField, + icon: "search", + }, + { + id: ACTION_IDS.PROFILE, + visible: this.hasProfile, + }, + { + id: ACTION_IDS.ASSISTANT, + visible: this.hasAssistant, + icon: "da", + }, + { + id: ACTION_IDS.NOTIFICATIONS, + visible: this.showNotifications, + count: this.notificationsCount, + icon: "bell", + }, + { + id: ACTION_IDS.PRODUCT_SWITCH, + visible: this.showProductSwitch, + icon: "grid", + }, + ].filter(action => action.visible); } - getAction(actionId: string) { + getAction(actionId: ActionId) { return this.actions.find(action => action.id === actionId); } - getActionText(actionId: string): string { + getActionText(actionId: ActionId): string { const texts: Record = { - "search": this.texts.search, - "profile": this.texts.profile, - "overflow": this.texts.overflow, - "assistant": "Assistant", - "notifications": this.texts.notificationsNoCount, - "product-switch": this.texts.products, + [ACTION_IDS.SEARCH]: this.texts.search, + [ACTION_IDS.PROFILE]: this.texts.profile, + [ACTION_IDS.OVERFLOW]: this.texts.overflow, + [ACTION_IDS.ASSISTANT]: "Assistant", + [ACTION_IDS.NOTIFICATIONS]: this.texts.notificationsNoCount, + [ACTION_IDS.PRODUCT_SWITCH]: this.texts.products, }; return texts[actionId] || actionId; } @@ -749,9 +780,9 @@ class ShellBarV2 extends UI5Element { let prevented = false; // Trigger the appropriate action handler - if (actionId === "notifications") { + if (actionId === ACTION_IDS.NOTIFICATIONS) { prevented = this.handleNotificationsClick(); - } else if (actionId === "search") { + } else if (actionId === ACTION_IDS.SEARCH) { prevented = this.handleSearchButtonClick(); } @@ -1174,7 +1205,12 @@ class ShellBarV2 extends UI5Element { ShellBarV2.define(); export default ShellBarV2; +export { + ACTION_IDS, +}; export type { + ActionId, + ShellBarV2ActionItem, ShellBarV2MenuButtonClickEventDetail, ShellBarV2NotificationsClickEventDetail, ShellBarV2ProfileClickEventDetail, diff --git a/packages/fiori/src/shellbarv2/ShellBarActions.ts b/packages/fiori/src/shellbarv2/ShellBarActions.ts deleted file mode 100644 index ef7fcdab29cb..000000000000 --- a/packages/fiori/src/shellbarv2/ShellBarActions.ts +++ /dev/null @@ -1,62 +0,0 @@ -interface ShellBarV2ActionItem { - id: string; - icon?: string; - count?: string; - visible: boolean; -} - -interface ShellBarV2ActionsParams { - hasSearch: boolean; - showProfile: boolean; - hasAssistant: boolean; - showProductSwitch: boolean; - showNotifications: boolean; - notificationsCount?: string; -} - -class ShellBarV2Actions { - getActions(params: ShellBarV2ActionsParams): ShellBarV2ActionItem[] { - const { - hasSearch, - showProfile, - hasAssistant, - showProductSwitch, - showNotifications, - notificationsCount, - } = params; - - return [ - { - id: "search", - visible: hasSearch, - icon: "search", - }, - { - id: "profile", - visible: showProfile, - }, - { - id: "assistant", - visible: hasAssistant, - icon: "da", - }, - { - id: "notifications", - visible: showNotifications, - count: notificationsCount, - icon: "bell", - }, - { - id: "product-switch", - visible: showProductSwitch, - icon: "grid", - }, - ].filter(action => action.visible); - } -} - -export default ShellBarV2Actions; -export type { - ShellBarV2ActionItem, - ShellBarV2ActionsParams, -}; diff --git a/packages/fiori/src/shellbarv2/ShellBarOverflow.ts b/packages/fiori/src/shellbarv2/ShellBarOverflow.ts index 591d64832b77..6bbb53e0e7af 100644 --- a/packages/fiori/src/shellbarv2/ShellBarOverflow.ts +++ b/packages/fiori/src/shellbarv2/ShellBarOverflow.ts @@ -1,5 +1,6 @@ import type ShellBarV2Item from "../ShellBarV2Item.js"; -import type { ShellBarV2ActionItem } from "./ShellBarActions.js"; +import { ACTION_IDS } from "../ShellBarV2.js"; +import type { ActionId, ShellBarV2ActionItem } from "../ShellBarV2.js"; interface ShellBarV2HidableItem { id: string; @@ -27,7 +28,7 @@ interface ShellBarV2OverflowResult { type ShellBarV2OverflowItem = { type: "action"; - id: string; + id: ActionId; data: ShellBarV2ActionItem order: number; } | { @@ -179,7 +180,7 @@ class ShellBarV2Overflow { }); }); - const notificationAction = actions.find(action => action.id === "notifications"); + const notificationAction = actions.find(action => action.id === ACTION_IDS.NOTIFICATIONS); if (notificationAction) { addItem({ id: notificationAction.id, @@ -189,7 +190,7 @@ class ShellBarV2Overflow { }); } - const assistantAction = actions.find(action => action.id === "assistant"); + const assistantAction = actions.find(action => action.id === ACTION_IDS.ASSISTANT); if (assistantAction) { addItem({ id: assistantAction.id, @@ -202,7 +203,7 @@ class ShellBarV2Overflow { // only when search is closed if (!showSearchField) { addItem({ - id: "search", + id: ACTION_IDS.SEARCH, selector: this.SELECTORS.search, hideOrder: priorityStrategy.SEARCH + actionIndex++, showInOverflow: true, @@ -226,11 +227,11 @@ class ShellBarV2Overflow { const hiddenActions = actions.filter(action => hiddenItemsIds.includes(action.id)); hiddenActions.forEach(action => { let order = 0; - if (action.id === "search") { + if (action.id === ACTION_IDS.SEARCH) { order = 0; - } else if (action.id === "notifications") { + } else if (action.id === ACTION_IDS.NOTIFICATIONS) { order = 1; - } else if (action.id === "assistant") { + } else if (action.id === ACTION_IDS.ASSISTANT) { order = 2; } From 39e7dcb2c21b191158082c868823e67131b24b41 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Mon, 10 Nov 2025 10:59:17 +0200 Subject: [PATCH 42/57] fix: better type safe for actions --- packages/fiori/src/ShellBarV2.ts | 28 +++++++++++----- packages/fiori/src/ShellBarV2Template.tsx | 33 +++++++++++-------- .../ShellBarSearchLegacyTemplate.tsx | 5 +-- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index 02cdf3242eb0..ef1d74c890e2 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -21,10 +21,11 @@ import Popover from "@ui5/webcomponents/dist/Popover.js"; import Menu from "@ui5/webcomponents/dist/Menu.js"; import List from "@ui5/webcomponents/dist/List.js"; import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js"; -import "@ui5/webcomponents-icons/dist/bell.js"; -import "@ui5/webcomponents-icons/dist/grid.js"; -import "@ui5/webcomponents-icons/dist/da.js"; -import "@ui5/webcomponents-icons/dist/overflow.js"; +import searchIcon from "@ui5/webcomponents-icons/dist/search.js"; +import bellIcon from "@ui5/webcomponents-icons/dist/bell.js"; +import gridIcon from "@ui5/webcomponents-icons/dist/grid.js"; +import daIcon from "@ui5/webcomponents-icons/dist/da.js"; +import overflowIcon from "@ui5/webcomponents-icons/dist/overflow.js"; import ShellBarV2Template from "./ShellBarV2Template.js"; import shellBarV2Styles from "./generated/themes/ShellBarV2.css.js"; @@ -82,6 +83,7 @@ type ShellBarV2ActionItem = { icon?: string; count?: string; visible: boolean; + stableDomRef?: string; }; type ShellBarV2MenuButtonClickEventDetail = { @@ -605,27 +607,37 @@ class ShellBarV2 extends UI5Element { { id: ACTION_IDS.SEARCH, visible: this.hasSearchField, - icon: "search", + icon: searchIcon, + stableDomRef: "toggle-search", }, { id: ACTION_IDS.PROFILE, visible: this.hasProfile, + stableDomRef: "profile", }, { id: ACTION_IDS.ASSISTANT, visible: this.hasAssistant, - icon: "da", + icon: daIcon, }, { id: ACTION_IDS.NOTIFICATIONS, visible: this.showNotifications, count: this.notificationsCount, - icon: "bell", + icon: bellIcon, + stableDomRef: "notifications", }, { id: ACTION_IDS.PRODUCT_SWITCH, visible: this.showProductSwitch, - icon: "grid", + icon: gridIcon, + stableDomRef: "product-switch", + }, + { + id: ACTION_IDS.OVERFLOW, + visible: this.showOverflowButton, + icon: overflowIcon, + stableDomRef: "overflow", }, ].filter(action => action.visible); } diff --git a/packages/fiori/src/ShellBarV2Template.tsx b/packages/fiori/src/ShellBarV2Template.tsx index c6b7c7a5be64..28c467360645 100644 --- a/packages/fiori/src/ShellBarV2Template.tsx +++ b/packages/fiori/src/ShellBarV2Template.tsx @@ -26,6 +26,12 @@ export default function ShellBarV2Template(this: ShellBarV2) { const SearchInBarTemplate = isLegacySearch ? ShellBarV2SearchFieldLegacy : ShellBarV2SearchField; const SearchFullWidthTemplate = isLegacySearch ? ShellBarV2SearchFieldFullWidthLegacy : ShellBarV2SearchFieldFullWidth; + const profileAction = this.getAction("profile"); + const overflowAction = this.getAction("overflow"); + const assistantAction = this.getAction("assistant"); + const notificationsAction = this.getAction("notifications"); + const productSwitchAction = this.getAction("product-switch"); + return ( <>
@@ -115,23 +121,24 @@ export default function ShellBarV2Template(this: ShellBarV2) {
- {this.getAction("assistant") && ( + {assistantAction && (
)} - {this.getAction("notifications") && ( + {notificationsAction && ( )} @@ -150,12 +157,12 @@ export default function ShellBarV2Template(this: ShellBarV2) {
- {this.showOverflowButton && ( + {overflowAction && ( @@ -1093,17 +1076,20 @@ describe("Events", () => { cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-overflow-popover") - .should("be.visible"); + .should("to.exist") + .invoke("prop", "open", true); cy.get("[ui5-shellbar-v2]") .shadow() - .find(".ui5-shellbar-overflow-popover [ui5-list] [ui5-li]:nth-child(1)") + .find(".ui5-shellbar-overflow-popover [ui5-list] [ui5-shellbar-v2-item]:nth-child(1)") .realClick(); cy.get("[ui5-shellbar-v2]") .shadow() .find(".ui5-shellbar-overflow-popover") - .should("be.visible"); + .should("to.exist") + .invoke("prop", "open", true); + }); }); }); @@ -1280,10 +1266,8 @@ describe("Keyboard Navigation", () => { // Press right arrow - should move focus away from input since cursor is at end cy.get("@nativeInput").type("{rightArrow}"); // Verify focus is now on the ShellBarItem - cy.get("[ui5-shellbar-v2]") - .shadow() - .find(".ui5-shellbar-custom-item") - .should("be.focused"); + cy.get("[ui5-shellbar-v2-item]") + .should("have.focus"); placeInMiddleOfInput(); // Press left arrow - should stay focused on input since cursor is in the middle @@ -1536,17 +1520,13 @@ describe("Component Behavior", () => { item.addEventListener("click", cy.stub().as(stubAlias)); }); - cy.get("[ui5-shellbar-v2]") - .shadow() - .find(`.ui5-shellbar-custom-item[icon="accept"]`) + cy.get("[ui5-shellbar-v2-item][icon='accept']") .click(); cy.get("@acceptClick") .should("have.been.calledOnce"); - cy.get("[ui5-shellbar-v2]") - .shadow() - .find(`.ui5-shellbar-custom-item[icon="alert"]`) + cy.get("[ui5-shellbar-v2-item][icon='alert']") .click(); cy.get("@alertClick") diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index 54b6392e97d5..b5908b53c1d6 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -61,6 +61,7 @@ import { SHELLBAR_PRODUCTS, SHELLBAR_SEARCH, SHELLBAR_OVERFLOW, + SHELLBAR_ASSISTANT, SHELLBAR_ADDITIONAL_CONTEXT, SHELLBAR_NOTIFICATIONS_NO_COUNT, } from "./generated/i18n/i18n-defaults.js"; @@ -73,7 +74,7 @@ const ShellBarV2Actions = { Overflow: "overflow", Assistant: "assistant", Notifications: "notifications", - ProductSwitch: "product-switch", + ProductSwitch: "products", } as const; type ShellBarV2ActionId = typeof ShellBarV2Actions[keyof typeof ShellBarV2Actions]; @@ -93,9 +94,7 @@ interface IShellBarSearchField extends HTMLElement { open?: boolean; } -/* ============================================================================= -Event Types -================================================================================ */ +// Event Types type ShellBarV2NotificationsClickEventDetail = { targetRef: HTMLElement; @@ -126,9 +125,7 @@ type ShellBarV2ContentItemVisibilityChangeEventDetail = { items: Array; }; -/* ============================================================================= -Legacy Event Types (DELETE WHEN REMOVING LEGACY) -================================================================================ */ +// Legacy Event Types (DELETE WHEN REMOVING LEGACY) type ShellBarV2LogoClickEventDetail = { targetRef: HTMLElement; @@ -233,9 +230,7 @@ type ShellBarV2MenuItemClickEventDetail = { @event("content-item-visibility-change", { bubbles: true, }) -/* ============================================================================= -Legacy Events (DELETE WHEN REMOVING LEGACY) -================================================================================ */ +// Legacy Events (DELETE WHEN REMOVING LEGACY) /** * Fired when the logo is clicked. * @param {HTMLElement} targetRef dom ref of the logo element @@ -475,8 +470,8 @@ class ShellBarV2 extends UI5Element { getDomRef: () => this.getDomRef() || null, }); - overflowAdaptor = new ShellBarV2Overflow(); - accessibilityAdaptor = new ShellBarV2Accessibility(); + overflow = new ShellBarV2Overflow(); + accessibility = new ShellBarV2Accessibility(); private _searchAdaptor = new ShellBarV2Search(this.getSearchDeps()); private _searchAdaptorLegacy = new ShellBarV2SearchLegacy({ @@ -484,9 +479,7 @@ class ShellBarV2 extends UI5Element { getDisableSearchCollapse: () => this.disableSearchCollapse, }); - /* ========================================================================= - Legacy Members - ============================================================================ */ + // Legacy Members /** * Defines the logo slot (legacy). @@ -550,9 +543,7 @@ class ShellBarV2 extends UI5Element { legacyAdaptor?: ShellBarV2Legacy; - /* ========================================================================= - Lifecycle Methods - ============================================================================ */ + // Lifecycle Methods onEnterDOM() { ResizeHandler.register(this, this.handleResizeBound); @@ -586,13 +577,8 @@ class ShellBarV2 extends UI5Element { this.updateOverflow(); } - /* ========================================================================= - Actions Management - ============================================================================ */ + // Actions Management - /** - * Updates actions array based on current state. - */ private buildActions() { this.actions = [ { @@ -637,60 +623,47 @@ class ShellBarV2 extends UI5Element { return this.actions.find(action => action.id === actionId); } - getActionText(actionId: ShellBarV2ActionId): string { + getActionOverflowText(actionId: ShellBarV2ActionId): string { const texts: Record = { [ShellBarV2Actions.Search]: this.texts.search, [ShellBarV2Actions.Profile]: this.texts.profile, [ShellBarV2Actions.Overflow]: this.texts.overflow, - [ShellBarV2Actions.Assistant]: "Assistant", + [ShellBarV2Actions.Assistant]: this.texts.assistant, [ShellBarV2Actions.Notifications]: this.texts.notificationsNoCount, [ShellBarV2Actions.ProductSwitch]: this.texts.products, }; return texts[actionId] || actionId; } - /* ========================================================================= - Breakpoint Management - ============================================================================ */ + // Breakpoint Management get isSBreakPoint() { return this.breakpointSize === "S"; } - private calculateBreakpoint(width: number): ShellBarV2Breakpoint { - const bp = this.breakpoints.find(b => width <= b) || 10000; - return this.breakpointMap[bp]; - } - private updateBreakpoint() { const width = this.getBoundingClientRect().width; - const breakpoint = this.calculateBreakpoint(width); + const bp = this.breakpoints.find(b => width <= b) || 10000; + const breakpoint = this.breakpointMap[bp]; if (this.breakpointSize !== breakpoint) { this.breakpointSize = breakpoint; } } - /* ========================================================================= - Overflow Management - ============================================================================ */ + // Overflow Management - /** - * Updates overflow by delegating to controller. - * Controller measures DOM, hides items iteratively, returns result. - * Triggers rerender via property update to enable conditional rendering. - */ private updateOverflow() { - if (!this.overflowAdaptor) { + if (!this.overflow) { return; } - const result = this.overflowAdaptor.updateOverflow({ + const result = this.overflow.updateOverflow({ actions: this.actions, content: this.content, customItems: this.items, hiddenItemsIds: this.hiddenItemsIds, - showSearchField: this.showSearchField, + showSearchField: this.enabledFeatures.search && this.showSearchField, overflowOuter: this.overflowOuter!, overflowInner: this.overflowInner!, setVisible: (selector: string, visible: boolean) => { @@ -727,13 +700,9 @@ class ShellBarV2 extends UI5Element { } private handleContentVisibilityChanged(oldHiddenItemsIds: string[], newHiddenItemsIds: string[]) { - // Compare with previous state - const oldHiddenContentIds = oldHiddenItemsIds - .filter(id => this.content - .some(item => (item as any)._individualSlot as string === id)); - const newHiddenContentIds = newHiddenItemsIds - .filter(id => this.content - .some(item => (item as any)._individualSlot as string === id)); + const filterContentIds = (ids: string[]) => ids.filter(id => this.content.some(item => (item as any)._individualSlot as string === id)); + const oldHiddenContentIds = filterContentIds(oldHiddenItemsIds); + const newHiddenContentIds = filterContentIds(newHiddenItemsIds); if (!arraysAreEqual(oldHiddenContentIds, newHiddenContentIds)) { this.fireDecoratorEvent("content-item-visibility-change", { @@ -770,7 +739,7 @@ class ShellBarV2 extends UI5Element { const target = e.target as HTMLElement; const actionId = target.getAttribute("data-action-id"); - let prevented = false; + let prevented = e.defaultPrevented; // for custom actions if (actionId === ShellBarV2Actions.Notifications) { prevented = this.handleNotificationsClick(); @@ -784,7 +753,7 @@ class ShellBarV2 extends UI5Element { } get overflowItems() { - return this.overflowAdaptor.getOverflowItems({ + return this.overflow.getOverflowItems({ actions: this.actions, customItems: this.items, hiddenItemsIds: this.hiddenItemsIds, @@ -796,37 +765,17 @@ class ShellBarV2 extends UI5Element { * Shows count if only one item with count is overflowed, otherwise shows attention dot. */ get overflowBadge(): string | undefined { - const overflowItems = this.overflowAdaptor.getOverflowItems({ - actions: this.actions, - customItems: this.items, - hiddenItemsIds: this.hiddenItemsIds, - }); - - const itemsWithCount = overflowItems.filter(item => { - if (item.type === "action") { - return item.data.count; - } - return item.data.count; - }); - + const itemsWithCount = this.overflowItems.filter(item => item.data.count); if (itemsWithCount.length === 1) { - const item = itemsWithCount[0]; - if (item.type === "action") { - return item.data.count; - } - return item.data.count; + return itemsWithCount[0].data.count; } - if (itemsWithCount.length > 1) { return " "; // Attention dot } - return undefined; } - /* ========================================================================= - Search Management - ============================================================================ */ + // Search Management get search() { return this.searchField.length ? this.searchField[0] : null; @@ -843,10 +792,10 @@ class ShellBarV2 extends UI5Element { private getSearchDeps() { return { getSearchField: () => this.search, - getSearchState: () => this.showSearchField, + getSearchState: () => this.enabledFeatures.search && this.showSearchField, getCSSVariable: (cssVar: string) => this.getCSSVariable(cssVar), setSearchState: (expanded: boolean) => this.setSearchState(expanded), - getOverflowed: () => this.overflowAdaptor.isOverflowing(this.overflowOuter!, this.overflowInner!), + getOverflowed: () => this.overflow.isOverflowing(this.overflowOuter!, this.overflowInner!), }; } @@ -884,10 +833,6 @@ class ShellBarV2 extends UI5Element { return defaultPrevented; } - /** - * Use this method to change the state of the search filed according to internal logic. - * An event is fired to notify the change. - */ async setSearchState(expanded: boolean) { if (expanded === this.showSearchField) { return; @@ -915,13 +860,8 @@ class ShellBarV2 extends UI5Element { } } - /* ========================================================================= - Legacy Features Management - ============================================================================ */ + // Legacy Features Management - /** - * Initialize the legacy controller if legacy features are used. - */ private initLegacyController() { if (this.hasLegacyFeatures) { this.legacyAdaptor = new ShellBarV2Legacy({ @@ -938,17 +878,13 @@ class ShellBarV2 extends UI5Element { || this.menuItems.length > 0; } - /* ========================================================================= - Keyboard Navigation - ============================================================================ */ + // Keyboard Navigation _onKeyDown(e: KeyboardEvent) { this.itemNavigation.handleKeyDown(e); } - /* ========================================================================= - Content Management - ============================================================================ */ + // Content Management get startContent(): HTMLElement[] { return this.splitContent(this.content).start; @@ -971,16 +907,6 @@ class ShellBarV2 extends UI5Element { }; } - /** - * Splits content into start and end groups based on spacer element. - * Items before spacer = start (left-aligned) - * Items after spacer = end (right-aligned) - * Without spacer, all items are start content. - * - * Spacer can be detected by: - * - Component: - * - Attribute:
- */ splitContent(content: readonly HTMLElement[]) { const spacerIndex = content.findIndex( child => child.hasAttribute("ui5-shellbar-spacer"), @@ -996,11 +922,6 @@ class ShellBarV2 extends UI5Element { }; } - /** - * Returns packed separator info for a content item. - * If the item is hidden and it is the last item in the group, the separator should be packed with the item. - * This is to ensure that the separator size is going to be accounted for in the overflow measurement. - */ getPackedSeparatorInfo(item: HTMLElement, isStartGroup: boolean) { const group = isStartGroup ? this.startContent : this.endContent; if (this.isSBreakPoint) { @@ -1013,43 +934,40 @@ class ShellBarV2 extends UI5Element { return { shouldPack: isHidden && isLastItem }; } - /* ========================================================================= - Accessibility - ============================================================================ */ + // Accessibility - /** - * Returns accessibility info for all interactive areas. - * Used by template for aria attributes. - */ - get accInfo(): ShellBarV2AccessibilityInfo { - return this.accessibilityAdaptor.getAccessibilityInfo({ - searchText: this.texts.search, - profileText: this.texts.profile, - productsText: this.texts.products, - overflowText: this.texts.overflow, - notificationsText: this.texts.notificationsNoCount, + get actionsAccessibilityInfo(): ShellBarV2AccessibilityInfo { + return this.accessibility.getActionsAccessibilityInfo(this.texts, { overflowPopoverOpen: this.overflowPopoverOpen, accessibilityAttributes: this.accessibilityAttributes, }); } - /** - * Returns toolbar role for actions area based on visible items count. - */ get actionsRole(): "toolbar" | undefined { const visibleCount = this.actions.filter(a => !this.hiddenItemsIds.includes(a.id)).length; - return this.accessibilityAdaptor.getActionsRole(visibleCount); + return this.accessibility.getActionsRole(visibleCount); } - /** - * Returns group role for content area based on visible items count. - */ get contentRole(): "group" | undefined { const visibleItemsCount = this.content.filter(item => !this.hiddenItemsIds.includes((item as any)._individualSlot as string)).length; - return this.accessibilityAdaptor.getContentRole(visibleItemsCount); + return this.accessibility.getContentRole(visibleItemsCount); } - // i18n text getters + // Common Members + + get enabledFeatures() { + return { + search: this.searchField.length > 0, + profile: this.profile.length > 0, + content: this.content.length > 0, + branding: this.branding.length > 0, + overflow: this.showOverflowButton, + assistant: this.assistant.length > 0, + startButton: this.startButton.length > 0, + notifications: this.showNotifications, + productSwitch: this.showProductSwitch, + }; + } get texts() { return { @@ -1058,37 +976,17 @@ class ShellBarV2 extends UI5Element { shellbar: ShellBarV2.i18nBundle.getText(SHELLBAR_LABEL), products: ShellBarV2.i18nBundle.getText(SHELLBAR_PRODUCTS), overflow: ShellBarV2.i18nBundle.getText(SHELLBAR_OVERFLOW), + assistant: ShellBarV2.i18nBundle.getText(SHELLBAR_ASSISTANT), notifications: ShellBarV2.i18nBundle.getText(SHELLBAR_NOTIFICATIONS, this.notificationsCount || 0), notificationsNoCount: ShellBarV2.i18nBundle.getText(SHELLBAR_NOTIFICATIONS_NO_COUNT), contentItems: this.content.length > 1 ? ShellBarV2.i18nBundle.getText(SHELLBAR_ADDITIONAL_CONTEXT) : undefined, }; } - /** - * Used by overflow popover and legacy menu popover. - */ get popoverHorizontalAlign(): "Start" | "End" { return this.effectiveDir === "rtl" ? "Start" : "End"; } - /* ========================================================================= - Common Methods - ============================================================================ */ - - get enabledFeatures() { - return { - search: this.searchField.length > 0, - profile: this.profile.length > 0, - content: this.content.length > 0, - branding: this.branding.length > 0, - overflow: this.showOverflowButton, - assistant: this.assistant.length > 0, - startButton: this.startButton.length > 0, - notifications: this.showNotifications, - productSwitch: this.showProductSwitch, - }; - } - /** * Returns the `logo` DOM ref. * @public @@ -1148,28 +1046,20 @@ class ShellBarV2 extends UI5Element { return this.shadowRoot!.querySelector(`*[data-ui5-stable="toggle-search"]`); } + private _fireClickEvent(eventName: string, domRef: HTMLElement | null): boolean { + return domRef ? !this.fireDecoratorEvent(eventName as any, { targetRef: domRef }) : false; + } + handleNotificationsClick() { - const notificationsBtn = this.shadowRoot!.querySelector @@ -199,8 +201,8 @@ export default function ShellBarV2Template(this: ShellBarV2) { icon={productSwitchAction.icon} design="Transparent" onClick={this.handleProductSwitchClick} - tooltip={this.getActionText("product-switch")} - accessibilityAttributes={this.accInfo.products.accessibilityAttributes} + tooltip={actionsAccInfo.products.title} + accessibilityAttributes={actionsAccInfo.products.accessibilityAttributes} > )} @@ -228,7 +230,7 @@ export default function ShellBarV2Template(this: ShellBarV2) { data-action-id={item.id} count={actionData.count} inOverflow={true} - text={this.getActionText(item.id)} + text={this.getActionOverflowText(item.id)} /> ); } diff --git a/packages/fiori/src/i18n/messagebundle.properties b/packages/fiori/src/i18n/messagebundle.properties index bfbdaf2e90a9..6a57fc9650ba 100644 --- a/packages/fiori/src/i18n/messagebundle.properties +++ b/packages/fiori/src/i18n/messagebundle.properties @@ -200,6 +200,9 @@ SEARCH_ITEM_DELETE_BUTTON=Remove Suggestion #XACT: ARIA announcement for the more button SHELLBAR_OVERFLOW = More +#XACT: ARIA announcement for the assistant button +SHELLBAR_ASSISTANT=Assistant + #XACT: ARIA announcement for the cancel button SHELLBAR_CANCEL = Cancel diff --git a/packages/fiori/src/shellbarv2/ShellBarAccessibility.ts b/packages/fiori/src/shellbarv2/ShellBarAccessibility.ts index 4daa3a105adf..c9bc981a35b0 100644 --- a/packages/fiori/src/shellbarv2/ShellBarAccessibility.ts +++ b/packages/fiori/src/shellbarv2/ShellBarAccessibility.ts @@ -1,4 +1,6 @@ import type { AccessibilityAttributes, AriaRole } from "@ui5/webcomponents-base"; +import { ShellBarV2Actions } from "../ShellBarV2.js"; +import type { ShellBarV2ActionId } from "../ShellBarV2.js"; /** * Accessibility attributes for logo area (legacy) @@ -64,13 +66,10 @@ interface ShellBarV2AccessibilityInfo { interface ShellBarV2AccessibilityParams { accessibilityAttributes: ShellBarV2AccessibilityAttributes; overflowPopoverOpen: boolean; - notificationsText: string; - profileText: string; - productsText: string; - searchText: string; - overflowText: string; } +type ShellBarV2AccessibilityDefaultTexts = Record; + /** * Controller for ShellBarV2 accessibility features. * Manages accessibility attributes and generates aria properties for template. @@ -80,13 +79,8 @@ class ShellBarV2Accessibility { * Computes accessibility info for all interactive areas. * Merges user-provided attributes with defaults and dynamic state. */ - getAccessibilityInfo(params: ShellBarV2AccessibilityParams): ShellBarV2AccessibilityInfo { + getActionsAccessibilityInfo(defaultTexts: ShellBarV2AccessibilityDefaultTexts, params: ShellBarV2AccessibilityParams): ShellBarV2AccessibilityInfo { const { - searchText, - profileText, - overflowText, - productsText, - notificationsText, overflowPopoverOpen, accessibilityAttributes, } = params; @@ -94,36 +88,36 @@ class ShellBarV2Accessibility { const overflowExpanded = accessibilityAttributes.overflow?.expanded; return { - notifications: { - title: notificationsText, + [ShellBarV2Actions.Notifications]: { + title: defaultTexts[ShellBarV2Actions.Notifications], accessibilityAttributes: { expanded: accessibilityAttributes.notifications?.expanded, hasPopup: accessibilityAttributes.notifications?.hasPopup, }, }, - profile: { - title: profileText, + [ShellBarV2Actions.Profile]: { + title: defaultTexts[ShellBarV2Actions.Profile], accessibilityAttributes: { hasPopup: accessibilityAttributes.profile?.hasPopup, expanded: accessibilityAttributes.profile?.expanded, ...(accessibilityAttributes.profile?.name ? { name: accessibilityAttributes.profile.name } : {}), }, }, - products: { - title: productsText, + [ShellBarV2Actions.ProductSwitch]: { + title: defaultTexts[ShellBarV2Actions.ProductSwitch], accessibilityAttributes: { hasPopup: accessibilityAttributes.product?.hasPopup, expanded: accessibilityAttributes.product?.expanded, }, }, - search: { - title: searchText, + [ShellBarV2Actions.Search]: { + title: defaultTexts[ShellBarV2Actions.Search], accessibilityAttributes: { hasPopup: accessibilityAttributes.search?.hasPopup, }, }, - overflow: { - title: overflowText, + [ShellBarV2Actions.Overflow]: { + title: defaultTexts[ShellBarV2Actions.Overflow], accessibilityAttributes: { hasPopup: accessibilityAttributes.overflow?.hasPopup || "menu", expanded: overflowExpanded === undefined ? overflowPopoverOpen : overflowExpanded, diff --git a/packages/fiori/src/shellbarv2/ShellBarOverflow.ts b/packages/fiori/src/shellbarv2/ShellBarOverflow.ts index c00ca7c9be32..ab7bbdc78fe3 100644 --- a/packages/fiori/src/shellbarv2/ShellBarOverflow.ts +++ b/packages/fiori/src/shellbarv2/ShellBarOverflow.ts @@ -60,10 +60,6 @@ class ShellBarV2Overflow { notifications: ".ui5-shellbar-bell-button", }; - /** - * Performs overflow calculation by iteratively hiding items until no overflow. - * Measures DOM after each hide to determine if more hiding is needed. - */ updateOverflow(params: ShellBarV2OverflowParams): ShellBarV2OverflowResult { const { overflowOuter, overflowInner, setVisible, @@ -73,14 +69,12 @@ class ShellBarV2Overflow { return { hiddenItemsIds: [], showOverflowButton: false }; } - // Build hidable items from state const sortedItems = this.buildHidableItems(params); - // First, hide overflow button + // set initial state, to account for isOverflowing calculation setVisible(this.SELECTORS.overflow, false); - - // show all items sortedItems.forEach(item => { + // show all items to account for isOverflowing calculation setVisible(item.selector, true); }); @@ -117,26 +111,10 @@ class ShellBarV2Overflow { }; } - /** - * Checks if inner is overflowing wrapper. - */ isOverflowing(overflowOuter: HTMLElement, overflowInner: HTMLElement): boolean { return overflowInner.offsetWidth > overflowOuter.offsetWidth; } - /** - * Builds list of hidable items from state. - * - * Priority when search closed: - * 1. Action items - * 2. Content items (except last) - * 3. Search button - * 4. Last content item (protected) - * - * Priority when search open: - * 1. All content items - * 2. Action items (including search) - */ private buildHidableItems(params: ShellBarV2OverflowParams): ShellBarV2HidableItem[] { const { content, customItems, actions, showSearchField, hiddenItemsIds, @@ -168,10 +146,8 @@ class ShellBarV2Overflow { }); }); - // Build action items let actionIndex = 0; - // Build custom items customItems.forEach(item => { addItem({ id: item._id, @@ -181,28 +157,24 @@ class ShellBarV2Overflow { }); }); - const notificationAction = actions.find(action => action.id === ShellBarV2Actions.Notifications); - if (notificationAction) { - addItem({ - id: notificationAction.id, - selector: this.SELECTORS.notifications, - hideOrder: priorityStrategy.ACTIONS + actionIndex++, - showInOverflow: true, - }); - } - - const assistantAction = actions.find(action => action.id === ShellBarV2Actions.Assistant); - if (assistantAction) { - addItem({ - id: assistantAction.id, - selector: this.SELECTORS.assistant, - hideOrder: priorityStrategy.ACTIONS + actionIndex++, - showInOverflow: true, - }); - } + const actionConfigs = [ + { id: ShellBarV2Actions.Notifications, selector: this.SELECTORS.notifications }, + { id: ShellBarV2Actions.Assistant, selector: this.SELECTORS.assistant }, + ]; + + actionConfigs.forEach(config => { + if (actions.find(action => action.id === config.id)) { + addItem({ + id: config.id, + selector: config.selector, + hideOrder: priorityStrategy.ACTIONS + actionIndex++, + showInOverflow: true, + }); + } + }); - // only when search is closed if (!showSearchField) { + // Only move search to overflow if it's closed addItem({ id: ShellBarV2Actions.Search, selector: this.SELECTORS.search, @@ -213,9 +185,6 @@ class ShellBarV2Overflow { return items.sort((a, b) => a.hideOrder - b.hideOrder); } - /** - * Returns list of items to be shown in overflow popover. - */ getOverflowItems(params: { actions: readonly ShellBarV2ActionItem[]; customItems: readonly ShellBarV2Item[]; @@ -224,20 +193,19 @@ class ShellBarV2Overflow { const { actions, customItems, hiddenItemsIds } = params; const result: ShellBarV2OverflowItem[] = []; - // Add hidden actions + const actionOrder: Record = { + [ShellBarV2Actions.Search]: 0, + [ShellBarV2Actions.Notifications]: 1, + [ShellBarV2Actions.Assistant]: 2, + }; + const hiddenActions = actions.filter(action => hiddenItemsIds.includes(action.id)); hiddenActions.forEach(action => { - let order = 0; - if (action.id === ShellBarV2Actions.Search) { - order = 0; - } else if (action.id === ShellBarV2Actions.Notifications) { - order = 1; - } else if (action.id === ShellBarV2Actions.Assistant) { - order = 2; - } - result.push({ - type: "action", id: action.id, data: action, order, + type: "action", + id: action.id, + data: action, + order: actionOrder[action.id] ?? 0, }); }); @@ -249,9 +217,7 @@ class ShellBarV2Overflow { }); }); - // Sort by order - result.sort((a, b) => a.order - b.order); - return result; + return result.sort((a, b) => a.order - b.order); } } diff --git a/packages/fiori/src/shellbarv2/templates/ShellBarSearchLegacyTemplate.tsx b/packages/fiori/src/shellbarv2/templates/ShellBarSearchLegacyTemplate.tsx index e47a05b001d2..ee24bf1a0b04 100644 --- a/packages/fiori/src/shellbarv2/templates/ShellBarSearchLegacyTemplate.tsx +++ b/packages/fiori/src/shellbarv2/templates/ShellBarSearchLegacyTemplate.tsx @@ -42,10 +42,9 @@ function ShellBarV2SearchButton(this: ShellBarV2) { icon={searchAction?.icon} design="Transparent" onClick={this.handleSearchButtonClick} - tooltip={this.getActionText("search")} - aria-label={this.getActionText("search")} + tooltip={this.actionsAccessibilityInfo.search.title} aria-expanded={this.showSearchField} - accessibilityAttributes={this.accInfo.search.accessibilityAttributes} + accessibilityAttributes={this.actionsAccessibilityInfo.search.accessibilityAttributes} /> )} diff --git a/packages/fiori/test/pages/ShellBarV2.html b/packages/fiori/test/pages/ShellBarV2.html index d0dd5693440b..8c7ab3031ef3 100644 --- a/packages/fiori/test/pages/ShellBarV2.html +++ b/packages/fiori/test/pages/ShellBarV2.html @@ -15,20 +15,35 @@ - - - - + + + + + + + + ui5-button 1 + ui5-button 2 + + + + diff --git a/packages/fiori/test/pages/ShellBar_Comparison.html b/packages/fiori/test/pages/ShellBar_Comparison.html index 4ba884886641..22b825251473 100644 --- a/packages/fiori/test/pages/ShellBar_Comparison.html +++ b/packages/fiori/test/pages/ShellBar_Comparison.html @@ -818,6 +818,45 @@

ShellBar v1 vs ShellBarV2 Comparison

}); }); + // V1 item click listeners + const itemV11 = document.getElementById('itemV1-1'); + const itemV12 = document.getElementById('itemV1-2'); + if (itemV11) itemV11.addEventListener('click', (e) => console.log('V1 Item 1 clicked', e.detail)); + if (itemV12) itemV12.addEventListener('click', (e) => console.log('V1 Item 2 clicked', e.detail)); + + // V2 item click listeners + const itemV21 = document.getElementById('itemV2-1'); + const itemV22 = document.getElementById('itemV2-2'); + if (itemV21) itemV21.addEventListener('click', (e) => { + e.preventDefault(); + console.log('V2 Item 1 clicked', e.detail) + }); + if (itemV22) itemV22.addEventListener('click', (e) => { + e.preventDefault(); + console.log('V2 Item 2 clicked', e.detail) + }); + + // Attach listeners to dynamic items when they are added + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.id === 'itemV1-3') node.addEventListener('click', (e) => console.log('V1 Item 3 clicked', e.detail)); + if (node.id === 'itemV1-4') node.addEventListener('click', (e) => console.log('V1 Item 4 clicked', e.detail)); + if (node.id === 'itemV2-3') node.addEventListener('click', (e) => { + e.preventDefault(); + console.log('V2 Item 3 clicked', e.detail) + }); + if (node.id === 'itemV2-4') node.addEventListener('click', (e) => { + e.preventDefault(); + console.log('V2 Item 4 clicked', e.detail) + }); + }); + }); + }); + + observer.observe(shellbarV1, { childList: true }); + observer.observe(shellbarV2, { childList: true }); + console.log('ShellBar Comparison Page loaded'); console.log('Open browser console to see event logs'); console.log('Resize window to test overflow behavior!'); From 26cf0a40a87b16634149d127c8839abd713eaeb5 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Tue, 11 Nov 2025 22:01:11 +0200 Subject: [PATCH 45/57] fix: search fixes --- .gitignore | 2 + AGENTS.md | 63 ------- .../fiori/cypress/specs/ShellBarV2.cy.tsx | 56 ++++++- packages/fiori/src/ShellBarV2Template.tsx | 7 +- .../fiori/src/shellbarv2/ShellBarSearch.ts | 10 +- .../src/shellbarv2/ShellBarSearchLegacy.ts | 10 +- .../ShellBarSearchLegacyTemplate.tsx | 2 + .../templates/ShellBarSearchTemplate.tsx | 2 + packages/fiori/src/themes/ShellBarV2.css | 10 -- packages/fiori/test/pages/ShellBarV2.html | 156 ++++++++++++++---- .../fiori/test/pages/ShellBar_Comparison.html | 25 +++ 11 files changed, 233 insertions(+), 110 deletions(-) delete mode 100644 AGENTS.md diff --git a/.gitignore b/.gitignore index b608cf95c56a..3c6ff22bbcbd 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,5 @@ packages/playground/assets/*.gif packages/playground/assets/*.css packages/playground/assets/test/pages/*.css packages/playground/assets/test/pages/*.js + +AGENTS.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 040d5424bd45..000000000000 --- a/AGENTS.md +++ /dev/null @@ -1,63 +0,0 @@ -YOUR NAME IS LINUS TORVALDS. YOU FOLLOW THESE SIMPLE RULES: - -- DO NOT OVER-ENGINEER -- FOLLOW KISS -- FOLLOW YAGNI -- FAVOR SIMPLICITY OVER ABSTRACTION -- MORE CODE === MORE BUGS - -THIS IS A MINIMALISTIC CODEBASE. WRITE AS LESS CODE AS POSSIBLE TO ACHIEVE AS MUCH AS POSSIBLE. - -Before you start working on a task, explore the relevant codebase parts and list for me at most 3 prioritized points what you still need from me and what is still unclear to you. If useful, suggest 1–2 concrete response options per point and what you recommend. Explain your reasoning in a few words. If I say Go it means I approve you recommendations. - -Writing Style Prompt - -Focus on clarity: Make your message really easy to understand. -Example: "Please send the file by Monday." - -Be direct and concise: Get to the point; remove unnecessary words. -Example: "We should meet tomorrow." - -Use simple language: Write plainly with short sentences. -Example: "I need help with this issue." - -Stay away from fluff: -Avoid unnecessary adjectives and adverbs. -Example: "We finished the task." - -Avoid marketing language: Don't use hype or promotional words. -Avoid: "This revolutionary product will transform your life." -Use instead: "This product can help you." - -Keep it real: Be honest; don't force friendliness. -Example: "I don't think that's the best idea." - -Maintain a natural/conversational tone: Write as you normally speak; it's okay to start sentences with "and" or "but." -Example: "And that's why it matters." - -Simplify grammar: Don't stress about perfect grammar; it's fine not to capitalize "i" if that's your style. -Example: "i guess we can try that." - -Avoid AI-giveaway phrases: Don't use clichés like "dive into," "unleash your potential," etc. -Avoid: "Let's dive into this game-changing solution." -Use instead: "Here's how it works." - -Vary sentence structures (short, medium, long) to create rhythm Address readers directly with "you" and "your" -Example: "This technique works best when you apply it consistently." - -Use active voice Instead of: "The report was submitted by the team." -Use: "The team submitted the report." - -Avoid: Filler phrases Instead of: "It's important to note that the deadline is approaching." -Use: "The deadline is approaching." - -Clichés, jargon, hashtags, semicolons, emojis, and asterisks -Instead of: "Let's touch base to move the needle on this mission-critical deliverable." -Use: "Let's meet to discuss how to improve this important project." - -Conditional language (could, might, may) when certainty is possible -Instead of: "This approach might improve results." -Use: "This approach improves results." - -Redundancy and repetition (remove fluff!) -Forced keyword placement that disrupts natural reading \ No newline at end of file diff --git a/packages/fiori/cypress/specs/ShellBarV2.cy.tsx b/packages/fiori/cypress/specs/ShellBarV2.cy.tsx index f37af558e83d..78f4830e8f4f 100644 --- a/packages/fiori/cypress/specs/ShellBarV2.cy.tsx +++ b/packages/fiori/cypress/specs/ShellBarV2.cy.tsx @@ -1089,7 +1089,7 @@ describe("Events", () => { .find(".ui5-shellbar-overflow-popover") .should("to.exist") .invoke("prop", "open", true); - + }); }); }); @@ -1199,6 +1199,60 @@ describe("ButtonBadge in ShellBar", () => { }); }); +describe("Search Controllers", () => { + it("Test search doesn't collapse in full-screen mode during resize", () => { + cy.mount( + + + + + + + ); + + // search not focused + cy.get("#search").should("not.be.focused"); + // search field is empty + cy.get("#search").should("have.value", ""); + + cy.viewport(400, 800); + cy.wait(RESIZE_THROTTLE_RATE); + + cy.get("#shellbar").should("have.prop", "showSearchField", true); + + cy.viewport(360, 800); + cy.wait(RESIZE_THROTTLE_RATE); + + cy.get("#shellbar").should("have.prop", "showSearchField", true); + }); + + it("Test legacy search doesn't collapse in full-screen mode during resize", () => { + cy.mount( + + + + + + + ); + + // search not focused + cy.get("#search").should("not.be.focused"); + // search field is empty + cy.get("#search").should("have.value", ""); + + cy.viewport(400, 800); + cy.wait(RESIZE_THROTTLE_RATE); + + cy.get("#shellbar").should("have.prop", "showSearchField", true); + + cy.viewport(360, 800); + cy.wait(RESIZE_THROTTLE_RATE); + + cy.get("#shellbar").should("have.prop", "showSearchField", true); + }); +}); + describe("Keyboard Navigation", () => { it("Test logo area elements are not rendered when no logo and primaryTitle are provided", () => { cy.mount(); diff --git a/packages/fiori/src/ShellBarV2Template.tsx b/packages/fiori/src/ShellBarV2Template.tsx index dfc93a03ffcc..25f0187170db 100644 --- a/packages/fiori/src/ShellBarV2Template.tsx +++ b/packages/fiori/src/ShellBarV2Template.tsx @@ -43,16 +43,13 @@ export default function ShellBarV2Template(this: ShellBarV2) {
{this.enabledFeatures.startButton && ( -
+
)} {this.enabledFeatures.branding && ( -
+
)} diff --git a/packages/fiori/src/shellbarv2/ShellBarSearch.ts b/packages/fiori/src/shellbarv2/ShellBarSearch.ts index d51edc49edf5..2ae29bb62eb3 100644 --- a/packages/fiori/src/shellbarv2/ShellBarSearch.ts +++ b/packages/fiori/src/shellbarv2/ShellBarSearch.ts @@ -27,6 +27,7 @@ class ShellBarV2Search implements IShellBarSearchController { private getSearchState: () => boolean; private setSearchState: (expanded: boolean) => void; private getCSSVariable: (variable: string) => string; + private initialRender = true; constructor({ getOverflowed, @@ -74,13 +75,20 @@ class ShellBarV2Search implements IShellBarSearchController { const searchHasFocus = document.activeElement === this.getSearchField(); const searchHasValue = !!this.getSearchField()?.value; - const preventCollapse = searchHasFocus || searchHasValue; + + // On initial load, allow search to collapse even if it would trigger full-screen mode. + // This prevents search from showing in full-screen when page loads on small screens. + // After initial render, prevent collapse in full-screen mode during resize. + const inFullScreen = !this.initialRender && this.shouldShowFullScreen(); + const preventCollapse = searchHasFocus || searchHasValue || inFullScreen; if (hiddenItems > 0 && !preventCollapse) { this.setSearchState(false); } else if (availableSpace + this.getSearchButtonSize() > searchFieldWidth) { this.setSearchState(true); } + + this.initialRender = false; } /** diff --git a/packages/fiori/src/shellbarv2/ShellBarSearchLegacy.ts b/packages/fiori/src/shellbarv2/ShellBarSearchLegacy.ts index 4f57aa084690..d19ababa8fbb 100644 --- a/packages/fiori/src/shellbarv2/ShellBarSearchLegacy.ts +++ b/packages/fiori/src/shellbarv2/ShellBarSearchLegacy.ts @@ -24,6 +24,7 @@ class ShellBarV2SearchLegacy implements IShellBarSearchController { private setSearchState: (expanded: boolean) => void; private getCSSVariable: (variable: string) => string; private getDisableSearchCollapse: () => boolean; + private initialRender = true; constructor({ getOverflowed, @@ -75,13 +76,20 @@ class ShellBarV2SearchLegacy implements IShellBarSearchController { const searchField = this.getSearchField(); const searchHasFocus = searchField?.contains(document.activeElement) || false; const searchHasValue = this.hasValue(searchField); - const preventCollapse = searchHasFocus || searchHasValue; + + // On initial load, allow search to collapse even if it would trigger full-screen mode. + // This prevents search from showing in full-screen when page loads on small screens. + // After initial render, prevent collapse in full-screen mode during resize. + const inFullScreen = !this.initialRender && this.shouldShowFullScreen(); + const preventCollapse = searchHasFocus || searchHasValue || inFullScreen; if (hiddenItems > 0 && !preventCollapse) { this.setSearchState(false); } else if (availableSpace + this.getSearchButtonSize() > searchFieldWidth) { this.setSearchState(true); } + + this.initialRender = false; } /** diff --git a/packages/fiori/src/shellbarv2/templates/ShellBarSearchLegacyTemplate.tsx b/packages/fiori/src/shellbarv2/templates/ShellBarSearchLegacyTemplate.tsx index ee24bf1a0b04..75ccd230f0d6 100644 --- a/packages/fiori/src/shellbarv2/templates/ShellBarSearchLegacyTemplate.tsx +++ b/packages/fiori/src/shellbarv2/templates/ShellBarSearchLegacyTemplate.tsx @@ -1,4 +1,5 @@ import Button from "@ui5/webcomponents/dist/Button.js"; +import ButtonDesign from "@ui5/webcomponents/dist/types/ButtonDesign.js"; import type ShellBarV2 from "../../ShellBarV2.js"; function ShellBarV2SearchField(this: ShellBarV2) { @@ -23,6 +24,7 @@ function ShellBarV2SearchFieldFullWidth(this: ShellBarV2) {
+ + + + + + + + + + + + + + + + + + ); + + cy.viewport(1000, 800); + cy.wait(RESIZE_THROTTLE_RATE); + + // Verify actions are hidden in overflow + cy.get("#shellbar").shadow().find(".ui5-shellbar-overflow-button").should("exist"); + cy.get("#item1").should("not.be.visible"); + + // Expand search + cy.get("#shellbar").invoke("prop", "showSearchField", true); + cy.wait(RESIZE_THROTTLE_RATE); + + // Hidden actions should stay hidden (no flicker) + cy.get("#item1").should("not.be.visible"); + }); +}); + describe("Keyboard Navigation", () => { it("Test logo area elements are not rendered when no logo and primaryTitle are provided", () => { cy.mount(); diff --git a/packages/fiori/src/shellbarv2/ShellBarOverflow.ts b/packages/fiori/src/shellbarv2/ShellBarOverflow.ts index ab7bbdc78fe3..a606c72ff5af 100644 --- a/packages/fiori/src/shellbarv2/ShellBarOverflow.ts +++ b/packages/fiori/src/shellbarv2/ShellBarOverflow.ts @@ -123,12 +123,7 @@ class ShellBarV2Overflow { const items: ShellBarV2HidableItem[] = []; const priorityStrategy = showSearchField ? this.OPEN_SEARCH_STRATEGY : this.CLOSED_SEARCH_STRATEGY; - const addItem = (itemData: Omit) => { - items.push({ - keepHidden: hiddenItemsIds.includes(itemData.id), - ...itemData, - }); - }; + const addItem = (itemData: ShellBarV2HidableItem) => items.push(itemData); // Build content items content.forEach((item, index) => { @@ -142,6 +137,7 @@ class ShellBarV2Overflow { id: slotName, selector: `#${slotName}`, hideOrder: priority + dataHideOrder, + keepHidden: false, showInOverflow: false, }); }); @@ -153,6 +149,7 @@ class ShellBarV2Overflow { id: item._id, selector: `[data-ui5-stable="${item.stableDomRef}"]`, hideOrder: priorityStrategy.ACTIONS + actionIndex++, + keepHidden: hiddenItemsIds.includes(item._id), showInOverflow: true, }); }); @@ -168,6 +165,7 @@ class ShellBarV2Overflow { id: config.id, selector: config.selector, hideOrder: priorityStrategy.ACTIONS + actionIndex++, + keepHidden: hiddenItemsIds.includes(config.id), showInOverflow: true, }); } @@ -179,10 +177,20 @@ class ShellBarV2Overflow { id: ShellBarV2Actions.Search, selector: this.SELECTORS.search, hideOrder: priorityStrategy.SEARCH + actionIndex++, + keepHidden: false, showInOverflow: true, }); } - return items.sort((a, b) => a.hideOrder - b.hideOrder); + // sort by hideOrder first then by keepHidden keepHidden items are at the start + return items.sort((a, b) => { + if (a.keepHidden && !b.keepHidden) { + return -1; + } + if (!a.keepHidden && b.keepHidden) { + return 1; + } + return a.hideOrder - b.hideOrder; + }); } getOverflowItems(params: { From fdf30f53947d05340aee6e4d9e9fa0d807ac6a3c Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Tue, 11 Nov 2025 23:16:34 +0200 Subject: [PATCH 49/57] fix: add comments for css hack --- packages/fiori/src/themes/ShellBarV2.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/fiori/src/themes/ShellBarV2.css b/packages/fiori/src/themes/ShellBarV2.css index 3002ca863dac..44f8f1eb8e5b 100644 --- a/packages/fiori/src/themes/ShellBarV2.css +++ b/packages/fiori/src/themes/ShellBarV2.css @@ -174,6 +174,10 @@ } :host([show-search-field]) ::slotted([slot="searchField"]), +/* Search field displays in full mode if there's not enough space in bar. +Once in full screen mode the search field is rendered in another DOM. +To account for correct measurements in overflow, we should keep the min width +of the search field container in the bar even when the search is in full mode. */ :host([show-full-width-search]) .ui5-shellbar-search-field-area { min-width: var(--_ui5_shellbar_search_field_width); } From 634ac0800108cfa9ee742a2b20b5c65009e7f4bb Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 12 Nov 2025 00:04:23 +0200 Subject: [PATCH 50/57] fix: acc tests --- .../fiori/cypress/specs/ShellBarV2.cy.tsx | 66 +++++------ packages/fiori/src/ShellBarV2.ts | 4 +- .../src/shellbarv2/ShellBarAccessibility.ts | 109 +++++------------- 3 files changed, 56 insertions(+), 123 deletions(-) diff --git a/packages/fiori/cypress/specs/ShellBarV2.cy.tsx b/packages/fiori/cypress/specs/ShellBarV2.cy.tsx index 513e56e66485..ddf1e90afc95 100644 --- a/packages/fiori/cypress/specs/ShellBarV2.cy.tsx +++ b/packages/fiori/cypress/specs/ShellBarV2.cy.tsx @@ -296,17 +296,16 @@ describe("Responsiveness", () => { .should("not.exist"); }); - // TODO: V2 uses _individualSlot instead of stableDomRef - need to implement stableDomRef support - it.skip("Test accessibility attributes on custom action buttons", () => { + it("Test accessibility attributes on custom action buttons", () => { cy.mount(basicTemplate()).as("html"); - // V2: ShellBarV2Item element can be found by stable-dom-ref attribute - // It renders a ui5-button in its shadow root + // V2: ShellBarV2Item properly supports accessibilityAttributes property + // which are passed through to the ui5-button in its shadow root cy.get("@shellbar") .find(`[stable-dom-ref="call"]`) .as("call-item") .then($el => { - $el.get(0).accessibilityAttributes = { "hasPopup": "dialog", "expanded": "true" }; + ($el.get(0) as any).accessibilityAttributes = { "hasPopup": "dialog", "expanded": "true" }; }); cy.get("@call-item") .shadow() @@ -314,7 +313,7 @@ describe("Responsiveness", () => { .shadow() .find("button") .should("have.attr", "aria-expanded", "true") - .should("have.attr", "aria-hasPopup", "dialog"); + .should("have.attr", "aria-haspopup", "dialog"); }); }); @@ -1426,45 +1425,34 @@ describe("Branding slot", () => { describe("Component Behavior", () => { describe("Accessibility", () => { - it("tests accessibilityTexts property", () => { - const PROFILE_BTN_CUSTOM_TOOLTIP = "John Dow"; - const LOGO_CUSTOM_TOOLTIP = "Custom logo title"; - cy.mount( - - - - - ); - - cy.get("[ui5-shellbar-v2]").then(($shellbar) => { - $shellbar[0].accessibilityAttributes = { - profile: { - name: PROFILE_BTN_CUSTOM_TOOLTIP, - }, - logo: { - name: LOGO_CUSTOM_TOOLTIP - }, - }; - }); + it("tests accessibilityTexts property", () => { + const PROFILE_BTN_CUSTOM_TOOLTIP = "John Dow"; + const LOGO_CUSTOM_TOOLTIP = "Custom logo title"; - cy.get("[ui5-shellbar-v2]").should("have.prop", "_profileText", PROFILE_BTN_CUSTOM_TOOLTIP); + cy.mount( + + + + + ); - cy.get("[ui5-shellbar-v2]").should("have.prop", "_logoText", LOGO_CUSTOM_TOOLTIP); + cy.get("[ui5-shellbar-v2]").then(($shellbar) => { + $shellbar[0].accessibilityAttributes = { + profile: { + name: PROFILE_BTN_CUSTOM_TOOLTIP, + }, + logo: { + name: LOGO_CUSTOM_TOOLTIP + }, + }; }); - it("tests acc default roles", () => { - cy.mount( - - - - ); - - cy.get("[ui5-shellbar-v2]") - .shadow() - .find(".ui5-shellbar-logo-area") - .should("have.attr", "role", "link"); + cy.get("[ui5-shellbar-v2]").then(($shellbar) => { + expect($shellbar[0].actionsAccessibilityInfo.profile.title).to.equal(PROFILE_BTN_CUSTOM_TOOLTIP); + expect($shellbar[0].legacyAdaptor.logoAriaLabel).to.equal(LOGO_CUSTOM_TOOLTIP); }); + }); it("tests accessibilityAttributes property", () => { const NOTIFICATIONS_BTN_ARIA_HASPOPUP = "dialog"; diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index b5908b53c1d6..cd0ce6e30adc 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -471,7 +471,7 @@ class ShellBarV2 extends UI5Element { }); overflow = new ShellBarV2Overflow(); - accessibility = new ShellBarV2Accessibility(); + accessibility: ShellBarV2Accessibility = new ShellBarV2Accessibility(); private _searchAdaptor = new ShellBarV2Search(this.getSearchDeps()); private _searchAdaptorLegacy = new ShellBarV2SearchLegacy({ @@ -937,7 +937,7 @@ class ShellBarV2 extends UI5Element { // Accessibility get actionsAccessibilityInfo(): ShellBarV2AccessibilityInfo { - return this.accessibility.getActionsAccessibilityInfo(this.texts, { + return this.accessibility.getActionsAccessibilityAttributes(this.texts, { overflowPopoverOpen: this.overflowPopoverOpen, accessibilityAttributes: this.accessibilityAttributes, }); diff --git a/packages/fiori/src/shellbarv2/ShellBarAccessibility.ts b/packages/fiori/src/shellbarv2/ShellBarAccessibility.ts index c9bc981a35b0..2422a78f8109 100644 --- a/packages/fiori/src/shellbarv2/ShellBarAccessibility.ts +++ b/packages/fiori/src/shellbarv2/ShellBarAccessibility.ts @@ -1,33 +1,15 @@ import type { AccessibilityAttributes, AriaRole } from "@ui5/webcomponents-base"; -import { ShellBarV2Actions } from "../ShellBarV2.js"; -import type { ShellBarV2ActionId } from "../ShellBarV2.js"; -/** - * Accessibility attributes for logo area (legacy) - */ +// Legacy Type logo accessibility attributes type ShellBarV2LogoAccessibilityAttributes = { role?: Extract; name?: string; }; -/** - * Accessibility attributes for profile area - */ type ShellBarV2ProfileAccessibilityAttributes = Pick; - -/** - * Accessibility attributes for action areas (notifications, product, search, overflow) - */ type ShellBarV2AreaAccessibilityAttributes = Pick; - -/** - * Accessibility attributes for branding area - */ type ShellBarV2BrandingAccessibilityAttributes = Pick; -/** - * Top-level accessibility configuration for ShellBarV2 - */ type ShellBarV2AccessibilityAttributes = { logo?: ShellBarV2LogoAccessibilityAttributes; notifications?: ShellBarV2AreaAccessibilityAttributes; @@ -38,114 +20,78 @@ type ShellBarV2AccessibilityAttributes = { branding?: ShellBarV2BrandingAccessibilityAttributes; }; -/** - * Accessibility info for a single area - */ interface ShellBarV2AreaAccessibilityInfo { - title: string; + title: string | undefined; accessibilityAttributes: { + name?: string; hasPopup?: AccessibilityAttributes["hasPopup"]; expanded?: AccessibilityAttributes["expanded"]; }; } -/** - * Complete accessibility info object returned by the support controller - */ -interface ShellBarV2AccessibilityInfo { +type ShellBarV2AccessibilityInfo = { notifications: ShellBarV2AreaAccessibilityInfo; profile: ShellBarV2AreaAccessibilityInfo; products: ShellBarV2AreaAccessibilityInfo; - search: ShellBarV2AreaAccessibilityInfo; overflow: ShellBarV2AreaAccessibilityInfo; -} - -/** - * Parameters for computing accessibility info - */ -interface ShellBarV2AccessibilityParams { - accessibilityAttributes: ShellBarV2AccessibilityAttributes; - overflowPopoverOpen: boolean; -} - -type ShellBarV2AccessibilityDefaultTexts = Record; + search: ShellBarV2AreaAccessibilityInfo; +}; -/** - * Controller for ShellBarV2 accessibility features. - * Manages accessibility attributes and generates aria properties for template. - */ class ShellBarV2Accessibility { - /** - * Computes accessibility info for all interactive areas. - * Merges user-provided attributes with defaults and dynamic state. - */ - getActionsAccessibilityInfo(defaultTexts: ShellBarV2AccessibilityDefaultTexts, params: ShellBarV2AccessibilityParams): ShellBarV2AccessibilityInfo { - const { - overflowPopoverOpen, - accessibilityAttributes, - } = params; - + getActionsAccessibilityAttributes( + defaultTexts: Record, + params: { + accessibilityAttributes: ShellBarV2AccessibilityAttributes; + overflowPopoverOpen: boolean; + }, + ): ShellBarV2AccessibilityInfo { + const { overflowPopoverOpen, accessibilityAttributes } = params; const overflowExpanded = accessibilityAttributes.overflow?.expanded; return { - [ShellBarV2Actions.Notifications]: { - title: defaultTexts[ShellBarV2Actions.Notifications], + notifications: { + title: defaultTexts.notifications, accessibilityAttributes: { expanded: accessibilityAttributes.notifications?.expanded, hasPopup: accessibilityAttributes.notifications?.hasPopup, }, }, - [ShellBarV2Actions.Profile]: { - title: defaultTexts[ShellBarV2Actions.Profile], + profile: { + title: accessibilityAttributes.profile?.name || defaultTexts.profile, accessibilityAttributes: { hasPopup: accessibilityAttributes.profile?.hasPopup, expanded: accessibilityAttributes.profile?.expanded, - ...(accessibilityAttributes.profile?.name ? { name: accessibilityAttributes.profile.name } : {}), }, }, - [ShellBarV2Actions.ProductSwitch]: { - title: defaultTexts[ShellBarV2Actions.ProductSwitch], + products: { + title: defaultTexts.products, accessibilityAttributes: { hasPopup: accessibilityAttributes.product?.hasPopup, expanded: accessibilityAttributes.product?.expanded, }, }, - [ShellBarV2Actions.Search]: { - title: defaultTexts[ShellBarV2Actions.Search], + search: { + title: defaultTexts.search, accessibilityAttributes: { hasPopup: accessibilityAttributes.search?.hasPopup, }, }, - [ShellBarV2Actions.Overflow]: { - title: defaultTexts[ShellBarV2Actions.Overflow], + overflow: { + title: defaultTexts.overflow, accessibilityAttributes: { - hasPopup: accessibilityAttributes.overflow?.hasPopup || "menu", + hasPopup: accessibilityAttributes.overflow?.hasPopup || "menu" as const, expanded: overflowExpanded === undefined ? overflowPopoverOpen : overflowExpanded, }, }, }; } - /** - * Gets role for actions toolbar area. - * Returns "toolbar" when multiple visible items exist. - */ getActionsRole(visibleItemsCount: number): "toolbar" | undefined { - if (visibleItemsCount <= 1) { - return undefined; - } - return "toolbar"; + return visibleItemsCount > 1 ? "toolbar" : undefined; } - /** - * Returns accessibility role for content area. - * Only group if multiple items exist. - */ getContentRole(visibleItemsCount: number): "group" | undefined { - if (visibleItemsCount <= 1) { - return undefined; - } - return "group"; + return visibleItemsCount > 1 ? "group" : undefined; } } @@ -153,7 +99,6 @@ export default ShellBarV2Accessibility; export type { ShellBarV2AccessibilityInfo, - ShellBarV2AccessibilityParams, ShellBarV2AreaAccessibilityInfo, ShellBarV2AccessibilityAttributes, ShellBarV2LogoAccessibilityAttributes, From 3c5db617d423391e4e7b66aad6353ab6b9595113 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Wed, 12 Nov 2025 00:09:45 +0200 Subject: [PATCH 51/57] fix: remove old file --- packages/fiori/src/ShellBarV0.ts | 1885 ------------------------------ 1 file changed, 1885 deletions(-) delete mode 100644 packages/fiori/src/ShellBarV0.ts diff --git a/packages/fiori/src/ShellBarV0.ts b/packages/fiori/src/ShellBarV0.ts deleted file mode 100644 index cdfa9fdd7aa7..000000000000 --- a/packages/fiori/src/ShellBarV0.ts +++ /dev/null @@ -1,1885 +0,0 @@ -import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; -import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js"; -import property from "@ui5/webcomponents-base/dist/decorators/property.js"; -import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; -import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; -import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; -import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; -import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; -import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js"; -import { - isSpace, - isEnter, - isLeft, - isRight, - isHome, - isEnd, -} from "@ui5/webcomponents-base/dist/Keys.js"; -import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js"; -import { getTabbableElements } from "@ui5/webcomponents-base/dist/util/TabbableElements.js"; -import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js"; -import List from "@ui5/webcomponents/dist/List.js"; -import type { ListItemClickEventDetail } from "@ui5/webcomponents/dist/List.js"; -import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js"; -import Popover from "@ui5/webcomponents/dist/Popover.js"; -import Button from "@ui5/webcomponents/dist/Button.js"; -import ButtonBadge from "@ui5/webcomponents/dist/ButtonBadge.js"; -import Menu from "@ui5/webcomponents/dist/Menu.js"; -import Icon from "@ui5/webcomponents/dist/Icon.js"; -import type { IButton } from "@ui5/webcomponents/dist/Button.js"; -import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; -import { isDesktop, isPhone } from "@ui5/webcomponents-base/dist/Device.js"; -import search from "@ui5/webcomponents-icons/dist/search.js"; -import da from "@ui5/webcomponents-icons/dist/da.js"; -import bell from "@ui5/webcomponents-icons/dist/bell.js"; -import overflow from "@ui5/webcomponents-icons/dist/overflow.js"; -import grid from "@ui5/webcomponents-icons/dist/grid.js"; -import type { - ClassMap, - AccessibilityAttributes, - AriaRole, - UI5CustomEvent, -} from "@ui5/webcomponents-base"; -import type ListItemBase from "@ui5/webcomponents/dist/ListItemBase.js"; -import type PopoverHorizontalAlign from "@ui5/webcomponents/dist/types/PopoverHorizontalAlign.js"; -import throttle from "@ui5/webcomponents-base/dist/util/throttle.js"; -import { getScopedVarName } from "@ui5/webcomponents-base/dist/CustomElementsScope.js"; -import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js"; -import type ShellBarItem from "./ShellBarItem.js"; -import type { ShellBarItemAccessibilityAttributes } from "./ShellBarItem.js"; -import type ShellBarBranding from "./ShellBarBranding.js"; - -// Templates -import ShellBarTemplate from "./ShellBarTemplate.js"; - -// Styles -import shellBarStyles from "./generated/themes/ShellBar.css.js"; -import ShellBarPopoverCss from "./generated/themes/ShellBarPopover.css.js"; - -import { - SHELLBAR_LABEL, - SHELLBAR_LOGO, - SHELLBAR_NOTIFICATIONS, - SHELLBAR_NOTIFICATIONS_NO_COUNT, - SHELLBAR_CANCEL, - SHELLBAR_PROFILE, - SHELLBAR_PRODUCTS, - SHELLBAR_SEARCH, - SHELLBAR_SEARCH_FIELD, - SHELLBAR_OVERFLOW, - SHELLBAR_LOGO_AREA, - SHELLBAR_ADDITIONAL_CONTEXT, - SHELLBAR_SEARCHFIELD_DESCRIPTION, - SHELLBAR_SEARCH_BTN_OPEN, - SHELLBAR_PRODUCT_SWITCH_BTN, -} from "./generated/i18n/i18n-defaults.js"; - -type ShellBarLogoAccessibilityAttributes = { - role?: Extract, - name?: string, -} -type ShellBarProfileAccessibilityAttributes = Pick; -type ShellBarAreaAccessibilityAttributes = Pick; -type ShellBarBrandingAccessibilityAttributes = Pick; -type ShellBarAccessibilityAttributes = { - logo?: ShellBarLogoAccessibilityAttributes - notifications?: ShellBarAreaAccessibilityAttributes - profile?: ShellBarProfileAccessibilityAttributes, - product?: ShellBarAreaAccessibilityAttributes - search?: ShellBarAreaAccessibilityAttributes - overflow?: ShellBarAreaAccessibilityAttributes - branding?: ShellBarBrandingAccessibilityAttributes -}; - -type ShellBarNotificationsClickEventDetail = { - targetRef: HTMLElement; -}; - -type ShellBarProfileClickEventDetail = { - targetRef: HTMLElement; -}; - -type ShellBarProductSwitchClickEventDetail = { - targetRef: HTMLElement; -}; - -type ShellBarLogoClickEventDetail = { - targetRef: HTMLElement; -}; - -type ShellBarMenuItemClickEventDetail = { - item: HTMLElement; -}; - -type ShellBarContentItemVisibilityChangeEventDetail = { - items: Array -}; - -type ShellBarSearchButtonEventDetail = { - targetRef: HTMLElement; - searchFieldVisible: boolean; -}; - -type ShellBarSearchFieldToggleEventDetail = { - expanded: boolean; -}; - -type ShellBarSearchFieldClearEventDetail = { - targetRef: HTMLElement; -}; - -interface IShellBarSearchField extends HTMLElement { - focused: boolean; - value: string; - collapsed?: boolean; - open?: boolean; -} - -interface IShellBarHidableItem { - classes: string, - id: string, - show: boolean, -} - -interface IShelBarItemInfo extends IShellBarHidableItem { - icon?: string, - text?: string, - count?: string, - custom?: boolean, - title?: string, - stableDomRef?: string, - refItemid?: string, - press: (e: UI5CustomEvent) => void, - order?: number, - profile?: boolean, - tooltip?: string, - accessibilityAttributes?: ShellBarItemAccessibilityAttributes, - accessibleName?: string, -} - -interface IShellBarContentItem extends IShellBarHidableItem { - hideOrder: number, -} - -const RESIZE_THROTTLE_RATE = 200; // ms - -// actions always visible in lean mode, order is important -const PREDEFINED_PLACE_ACTIONS = ["feedback", "sys-help"]; - -/** - * @class - * ### Overview - * - * The `ui5-shellbar` is meant to serve as an application header - * and includes numerous built-in features, such as: logo, profile image/icon, title, search field, notifications and so on. - * - * ### Stable DOM Refs - * - * You can use the following stable DOM refs for the `ui5-shellbar`: - * - * - logo - * - notifications - * - overflow - * - profile - * - product-switch - * - * ### Keyboard Handling - * - * #### Fast Navigation - * This component provides a build in fast navigation group which can be used via [F6] / [Shift] + [F6] / [Ctrl] + [Alt/Option] / [Down] or [Ctrl] + [Alt/Option] + [Up]. - * In order to use this functionality, you need to import the following module: - * `import "@ui5/webcomponents-base/dist/features/F6Navigation.js"` - * - * ### ES6 Module Import - * `import "@ui5/webcomponents-fiori/dist/ShellBar.js";` - * @csspart root - Used to style the outermost wrapper of the `ui5-shellbar` - * @constructor - * @extends UI5Element - * @public - * @since 0.8.0 - */ - -@customElement({ - tag: "ui5-shellbar", - fastNavigation: true, - languageAware: true, - renderer: jsxRenderer, - template: ShellBarTemplate, - styles: [shellBarStyles, ShellBarPopoverCss], - dependencies: [ - Button, - Icon, - List, - Popover, - ListItemStandard, - Menu, - ButtonBadge, - ], -}) -/** - * - * Fired, when the notification icon is activated. - * @param {HTMLElement} targetRef dom ref of the activated element - * @public - */ -@event("notifications-click", { - cancelable: true, - bubbles: true, -}) - -/** - * Fired, when the profile slot is present. - * @param {HTMLElement} targetRef dom ref of the activated element - * @public - */ -@event("profile-click", { - bubbles: true, -}) - -/** - * Fired, when the product switch icon is activated. - * - * **Note:** You can prevent closing of overflow popover by calling `event.preventDefault()`. - * @param {HTMLElement} targetRef dom ref of the activated element - * @public - */ -@event("product-switch-click", { - cancelable: true, - bubbles: true, -}) - -/** - * Fired, when the logo is activated. - * @param {HTMLElement} targetRef dom ref of the activated element - * @since 0.10 - * @public - */ -@event("logo-click", { - bubbles: true, -}) - -/** - * Fired, when a menu item is activated - * - * **Note:** You can prevent closing of overflow popover by calling `event.preventDefault()`. - * @param {HTMLElement} item DOM ref of the activated list item - * @since 0.10 - * @public - */ -@event("menu-item-click", { - bubbles: true, - cancelable: true, -}) - -/** - * Fired, when the search button is activated. - * - * **Note:** You can prevent expanding/collapsing of the search field by calling `event.preventDefault()`. - * @param {HTMLElement} targetRef dom ref of the activated element - * @param {Boolean} searchFieldVisible whether the search field is visible - * @public - */ - -@event("search-button-click", { - cancelable: true, - bubbles: true, -}) - -/** - * Fired, when the search field is expanded or collapsed. - * @since 2.10.0 - * @param {Boolean} expanded whether the search field is expanded - * @public - */ -@event("search-field-toggle", { - bubbles: true, -}) - -/** - * Fired, when the search cancel button is activated. - * - * **Note:** You can prevent the default behavior (clearing the search field value) by calling `event.preventDefault()`. The search will still be closed. - * **Note:** The `search-field-clear` event is in an experimental state and is a subject to change. - * @param {HTMLElement} targetRef dom ref of the cancel button element - * @since 2.14.0 - * @public - */ -@event("search-field-clear", { - cancelable: true, - bubbles: true, -}) - -/** - * Fired, when an item from the content slot is hidden or shown. - * **Note:** The `content-item-visibility-change` event is in an experimental state and is a subject to change. - * - * @param {Array} array of all the items that are hidden - * @public - * @since 2.7.0 - */ -@event("content-item-visibility-change", { - bubbles: true, -}) - -class ShellBar extends UI5Element { - eventDetails!: { - "notifications-click": ShellBarNotificationsClickEventDetail, - "profile-click": ShellBarProfileClickEventDetail, - "product-switch-click": ShellBarProductSwitchClickEventDetail, - "logo-click": ShellBarLogoClickEventDetail, - "menu-item-click": ShellBarMenuItemClickEventDetail, - "search-button-click": ShellBarSearchButtonEventDetail, - "search-field-toggle": ShellBarSearchFieldToggleEventDetail, - "search-field-clear": ShellBarSearchFieldClearEventDetail, - "content-item-visibility-change": ShellBarContentItemVisibilityChangeEventDetail - } - - /** - * Defines the visibility state of the search button. - * - * **Note:** The `hideSearchButton` property is in an experimental state and is a subject to change. - * @default false - * @public - */ - @property({ type: Boolean }) - hideSearchButton = false; - - /** - * Disables the automatic search field expansion/collapse when the available space is not enough. - * - * **Note:** The `disableSearchCollapse` property is in an experimental state and is a subject to change. - * @default false - * @public - */ - @property({ type: Boolean }) - disableSearchCollapse = false; - - /** - * Defines the `primaryTitle`. - * - * **Note:** The `primaryTitle` would be hidden on S screen size (less than approx. 700px). - * @default undefined - * @public - */ - @property() - primaryTitle?: string; - - /** - * Defines the `secondaryTitle`. - * - * **Note:** The `secondaryTitle` would be hidden on S and M screen sizes (less than approx. 1300px). - * @default undefined - * @public - */ - @property() - secondaryTitle?: string; - - /** - * Defines the `notificationsCount`, - * displayed in the notification icon top-right corner. - * @default undefined - * @public - */ - @property() - notificationsCount?: string; - - /** - * Defines, if the notification icon would be displayed. - * @default false - * @public - */ - @property({ type: Boolean }) - showNotifications = false; - - /** - * Defines, if the product switch icon would be displayed. - * @default false - * @public - */ - @property({ type: Boolean }) - showProductSwitch = false; - - /** - * Defines, if the Search Field would be displayed when there is a valid `searchField` slot. - * - * **Note:** By default the Search Field is not displayed. - * @default false - * @public - */ - @property({ type: Boolean }) - showSearchField = false; - - /** - * Defines additional accessibility attributes on different areas of the component. - * - * The accessibilityAttributes object has the following fields, - * where each field is an object supporting one or more accessibility attributes: - * - * - **logo** - `logo.role` and `logo.name`. - * - **notifications** - `notifications.expanded` and `notifications.hasPopup`. - * - **profile** - `profile.expanded`, `profile.hasPopup` and `profile.name`. - * - **product** - `product.expanded` and `product.hasPopup`. - * - **search** - `search.hasPopup`. - * - **overflow** - `overflow.expanded` and `overflow.hasPopup`. - * - **branding** - `branding.name`. - * - * The accessibility attributes support the following values: - * - * - **role**: Defines the accessible ARIA role of the logo area. - * Accepts the following string values: `button` or `link`. - * - * - **expanded**: Indicates whether the button, or another grouping element it controls, - * is currently expanded or collapsed. - * Accepts the following string values: `true` or `false`. - * - * - **hasPopup**: Indicates the availability and type of interactive popup element, - * such as menu or dialog, that can be triggered by the button. - * - * Accepts the following string values: `dialog`, `grid`, `listbox`, `menu` or `tree`. - * - **name**: Defines the accessible ARIA name of the area. - * Accepts any string. - * - * @default {} - * @public - * @since 1.10.0 - */ - @property({ type: Object }) - accessibilityAttributes: ShellBarAccessibilityAttributes = {}; - - /** - * @private - */ - @property() - breakpointSize = "S"; - - /** - * @private - */ - @property({ type: Boolean }) - withLogo = false; - - @property({ type: Object }) - _itemsInfo: Array = []; - - @property({ type: Object }) - _contentInfo: Array = []; - - @property({ type: Boolean, noAttribute: true }) - _menuPopoverExpanded = false; - - @property({ type: Boolean, noAttribute: true }) - _overflowPopoverExpanded = false; - - @property({ type: Boolean, noAttribute: true }) - showFullWidthSearch = false; - - _cachedHiddenContent: Array = []; - - /** - * Defines the assistant slot. - * - * @since 2.0.0 - * @public - */ - @slot() - assistant!: Array; - - /** - * Defines the branding slot. - * The `ui5-shellbar-branding` component is intended to be placed inside this slot. - * Content placed here takes precedence over the `primaryTitle` property and the `logo` content slot. - * - * **Note:** The `branding` slot is in an experimental state and is a subject to change. - * - * @since 2.12.0 - * @public - */ - @slot() - branding!: Array; - - /** - * Defines the `ui5-shellbar` additional items. - * - * **Note:** - * You can use the ``. - * @public - */ - @slot({ type: HTMLElement, "default": true, invalidateOnChildChange: true }) - items!: Array; - - /** - * You can pass `ui5-avatar` to set the profile image/icon. - * If no profile slot is set - profile will be excluded from actions. - * - * **Note:** We recommend not using the `size` attribute of `ui5-avatar` because - * it should have specific size by design in the context of `ui5-shellbar` profile. - * @since 1.0.0-rc.6 - * @public - */ - @slot() - profile!: Array; - - /** - * Defines the logo of the `ui5-shellbar`. - * For example, you can use `ui5-avatar` or `img` elements as logo. - * @since 1.0.0-rc.8 - * @public - */ - @slot() - logo!: Array; - - /** - * Defines the items displayed in menu after a click on a start button. - * - * **Note:** You can use the `` and its ancestors. - * @since 0.10 - * @public - */ - @slot() - menuItems!: Array; - - /** - * Defines the `ui5-input`, that will be used as a search field. - * @public - */ - @slot({ - type: HTMLElement, - invalidateOnChildChange: true, - }) - searchField!: Array; - - /** - * Defines a `ui5-button` in the bar that will be placed in the beginning. - * We encourage this slot to be used for a menu button. - * It gets overstyled to match ShellBar's styling. - * @public - */ - @slot() - startButton!: Array; - - /** - * The container is positioned in the center of the `ui5-shellbar` and occupies one-third of the total length of the `ui5-shellbar`. - * - * **Note:** If set, the `searchField` slot is not rendered. - * @private - */ - @slot() - midContent!: Array; - - /** - * Define the items displayed in the content area. - * - * Use the `data-hide-order` attribute with numeric value to specify the order of the items to be hidden when the space is not enough. - * Lower values will be hidden first. - * - * **Note:** The `content` slot is in an experimental state and is a subject to change. - * - * @public - * @since 2.7.0 - */ - @slot({ type: HTMLElement, individualSlots: true }) - content!: Array; - - @i18n("@ui5/webcomponents-fiori") - static i18nBundle: I18nBundle; - - /* ------------- Search properties -------------- */ - _autoRestoreSearchField = false; - _onSearchOpenBound = this._onSearchOpen.bind(this); - _onSearchCloseBound = this._onSearchClose.bind(this); - _onSearchBound = this._onSearch.bind(this); - - /* ------------- Overflow properties -------------- */ - overflowPopover?: Popover | null; - _lastOffsetWidth = 0; - _overflowNotifications: string | null; - _handleResize: ResizeObserverCallback; - _hiddenIcons: Array; - - /* ------------- Menu properties -------------- */ - menuPopover?: Popover | null; - _headerPress: () => void; - - /* ------------- Content properties -------------- */ - _observableContent: Array = []; - contentItemsObserver: MutationObserver; - - /* ------------- Common properties -------------- */ - _isInitialRendering: boolean; - - /* ------------- Actions properties -------------- */ - _defaultItemPressPrevented: boolean; - - static get FIORI_3_BREAKPOINTS() { - return [ - 599, - 1023, - 1439, - 1919, - 10000, - ]; - } - - static get FIORI_3_BREAKPOINTS_MAP(): Record { - return { - "599": "S", - "1023": "M", - "1439": "L", - "1919": "XL", - "10000": "XXL", - }; - } - - constructor() { - super(); - - this._hiddenIcons = []; - this._isInitialRendering = true; - this._overflowNotifications = null; - - // marks if preventDefault() is called in item's press handler - this._defaultItemPressPrevented = false; - - this.contentItemsObserver = new MutationObserver(() => { - this._handleActionsOverflow(); - }); - - this._headerPress = () => { - if (this.hasMenuItems) { - const menuPopover = this._getMenuPopover(); - menuPopover.opener = this.shadowRoot!.querySelector
))} -
-
-
- {overflowAction && ( - - )} + {overflowAction && ( + + )} - {profileAction && ( - - )} + {profileAction && ( + + )} - {productSwitchAction && ( - - )} + {productSwitchAction && ( + + )} +
+
+
diff --git a/packages/fiori/src/shellbarv2/ShellBarOverflow.ts b/packages/fiori/src/shellbarv2/ShellBarOverflow.ts index a606c72ff5af..19914ec5b4dd 100644 --- a/packages/fiori/src/shellbarv2/ShellBarOverflow.ts +++ b/packages/fiori/src/shellbarv2/ShellBarOverflow.ts @@ -1,5 +1,5 @@ import type ShellBarV2Item from "../ShellBarV2Item.js"; -import { ShellBarV2Actions } from "../ShellBarV2.js"; +import { ShellBarV2Actions, ShellBarV2ActionsSelectors } from "../ShellBarV2.js"; import type { ShellBarV2ActionId, ShellBarV2ActionItem } from "../ShellBarV2.js"; interface ShellBarV2HidableItem { @@ -53,13 +53,6 @@ class ShellBarV2Overflow { LAST_CONTENT: 3000, // Last content item protected }; - private readonly SELECTORS = { - search: ".ui5-shellbar-search-toggle", - overflow: ".ui5-shellbar-overflow-button", - assistant: ".ui5-shellbar-assistant-button", - notifications: ".ui5-shellbar-bell-button", - }; - updateOverflow(params: ShellBarV2OverflowParams): ShellBarV2OverflowResult { const { overflowOuter, overflowInner, setVisible, @@ -72,7 +65,7 @@ class ShellBarV2Overflow { const sortedItems = this.buildHidableItems(params); // set initial state, to account for isOverflowing calculation - setVisible(this.SELECTORS.overflow, false); + setVisible(ShellBarV2ActionsSelectors.Overflow, false); sortedItems.forEach(item => { // show all items to account for isOverflowing calculation setVisible(item.selector, true); @@ -95,7 +88,7 @@ class ShellBarV2Overflow { if (nextItemToHide.showInOverflow) { // show overflow button to account in isOverflowing calculation - setVisible(this.SELECTORS.overflow, true); + setVisible(ShellBarV2ActionsSelectors.Overflow, true); showOverflowButton = true; } } @@ -123,8 +116,6 @@ class ShellBarV2Overflow { const items: ShellBarV2HidableItem[] = []; const priorityStrategy = showSearchField ? this.OPEN_SEARCH_STRATEGY : this.CLOSED_SEARCH_STRATEGY; - const addItem = (itemData: ShellBarV2HidableItem) => items.push(itemData); - // Build content items content.forEach((item, index) => { const slotName = (item as any)._individualSlot as string; @@ -133,7 +124,7 @@ class ShellBarV2Overflow { const priority = isLast ? priorityStrategy.LAST_CONTENT : priorityStrategy.CONTENT; - addItem({ + items.push({ id: slotName, selector: `#${slotName}`, hideOrder: priority + dataHideOrder, @@ -145,7 +136,7 @@ class ShellBarV2Overflow { let actionIndex = 0; customItems.forEach(item => { - addItem({ + items.push({ id: item._id, selector: `[data-ui5-stable="${item.stableDomRef}"]`, hideOrder: priorityStrategy.ACTIONS + actionIndex++, @@ -154,28 +145,23 @@ class ShellBarV2Overflow { }); }); - const actionConfigs = [ - { id: ShellBarV2Actions.Notifications, selector: this.SELECTORS.notifications }, - { id: ShellBarV2Actions.Assistant, selector: this.SELECTORS.assistant }, - ]; - - actionConfigs.forEach(config => { - if (actions.find(action => action.id === config.id)) { - addItem({ + actions + .filter(a => !a.isProtected && a.id !== ShellBarV2Actions.Search) + .forEach(config => { + items.push({ id: config.id, selector: config.selector, hideOrder: priorityStrategy.ACTIONS + actionIndex++, keepHidden: hiddenItemsIds.includes(config.id), showInOverflow: true, }); - } - }); + }); if (!showSearchField) { // Only move search to overflow if it's closed - addItem({ + items.push({ id: ShellBarV2Actions.Search, - selector: this.SELECTORS.search, + selector: ShellBarV2ActionsSelectors.Search, hideOrder: priorityStrategy.SEARCH + actionIndex++, keepHidden: false, showInOverflow: true, diff --git a/packages/fiori/test/pages/ShellBar_Comparison.html b/packages/fiori/test/pages/ShellBar_Comparison.html index 0389d43c0a05..7e0a7a973e32 100644 --- a/packages/fiori/test/pages/ShellBar_Comparison.html +++ b/packages/fiori/test/pages/ShellBar_Comparison.html @@ -232,11 +232,11 @@

ShellBar v1 vs ShellBarV2 Comparison

- Action 1 - Tag - Action 3 - End 1 - End 2 + Action 1 + Tag + Action 3 + End 1 + End 2 @@ -260,11 +260,11 @@

ShellBar v1 vs ShellBarV2 Comparison

- Action 1 - Tag - Action 3 - End 1 - End 2 + Action 1 + Tag + Action 3 + End 1 + End 2 From 05946a06ae279dd7804bc49eba9e09c4bfd3816a Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Thu, 13 Nov 2025 21:09:37 +0200 Subject: [PATCH 54/57] fix: remove DOM wrapper --- packages/fiori/src/ShellBarV2Template.tsx | 301 +++++++++++----------- packages/fiori/src/themes/ShellBarV2.css | 12 - 2 files changed, 147 insertions(+), 166 deletions(-) diff --git a/packages/fiori/src/ShellBarV2Template.tsx b/packages/fiori/src/ShellBarV2Template.tsx index 618a07f94271..8d8422a35800 100644 --- a/packages/fiori/src/ShellBarV2Template.tsx +++ b/packages/fiori/src/ShellBarV2Template.tsx @@ -40,168 +40,161 @@ export default function ShellBarV2Template(this: ShellBarV2) { {/* Full-width search overlay */} {this.showFullWidthSearch && SearchFullWidthTemplate.call(this)} -
- - {this.enabledFeatures.startButton && ( -
- -
- )} - - {this.enabledFeatures.branding && ( -
- -
- )} - - {/* Legacy branding (logo + primaryTitle) when no menu items */} - {!this.enabledFeatures.branding && ShellBarV2LegacyBrandingArea.call(this)} - -
-
- - {this.enabledFeatures.content && ( -
- {/* Start separator */} - {this.separatorConfig.showStartSeparator && ( -
- )} - - {/* Start content items */} - {this.startContent.map(item => { - const packedSep = this.getPackedSeparatorInfo(item, true); - return ( -
- {packedSep.shouldPack && ( -
- )} - -
- ); - })} - - {/* Spacer: Grows to fill available space, used to measure if space is tight, should be in DOM always */} -
- - {/* End content items */} - {this.endContent.map(item => { - const packedSep = this.getPackedSeparatorInfo(item, false); - return ( -
- - {packedSep.shouldPack && ( -
- )} -
- ); - })} - - {/* End separator */} - {this.separatorConfig.showEndSeparator && ( -
- )} -
- )} - - {this.enabledFeatures.search && SearchInBarTemplate.call(this)} - {this.enabledFeatures.search && isLegacySearch && ShellBarV2SearchButtonLegacy.call(this)} - -
- - {assistantAction && ( -
- -
- )} + {this.enabledFeatures.startButton && ( +
+ +
+ )} - {notificationsAction && ( - + {this.enabledFeatures.branding && ( +
+ +
+ )} + + {/* Legacy branding (logo + primaryTitle) when no menu items */} + {!this.enabledFeatures.branding && ShellBarV2LegacyBrandingArea.call(this)} + +
+
+ + {this.enabledFeatures.content && ( +
+ {/* Start separator */} + {this.separatorConfig.showStartSeparator && ( +
)} - {/* Custom Items */} - {this.items.map(item => ( -
- {!item.inOverflow ? : null} -
- ))} - - {overflowAction && ( - + {/* Start content items */} + {this.startContent.map(item => { + const packedSep = this.getPackedSeparatorInfo(item, true); + return ( +
+ {packedSep.shouldPack && ( +
+ )} + +
+ ); + })} + + {/* Spacer: Grows to fill available space, used to measure if space is tight, should be in DOM always */} +
+ + {/* End content items */} + {this.endContent.map(item => { + const packedSep = this.getPackedSeparatorInfo(item, false); + return ( +
+ + {packedSep.shouldPack && ( +
+ )} +
+ ); + })} + + {/* End separator */} + {this.separatorConfig.showEndSeparator && ( +
)} +
+ )} - {profileAction && ( - - )} + {this.enabledFeatures.search && SearchInBarTemplate.call(this)} + {this.enabledFeatures.search && isLegacySearch && ShellBarV2SearchButtonLegacy.call(this)} - {productSwitchAction && ( - + {assistantAction && ( +
+ +
+ )} + + {notificationsAction && ( + + )} + + {/* Custom Items */} + {this.items.map(item => ( +
+ {!item.inOverflow ? : null}
-
+ ))} + + {overflowAction && ( + + )} + + {profileAction && ( + + )} + + {productSwitchAction && ( + + )}
diff --git a/packages/fiori/src/themes/ShellBarV2.css b/packages/fiori/src/themes/ShellBarV2.css index 44f8f1eb8e5b..0209820c2e8b 100644 --- a/packages/fiori/src/themes/ShellBarV2.css +++ b/packages/fiori/src/themes/ShellBarV2.css @@ -40,18 +40,6 @@ font-weight: normal; } -/* ============================================================================ - WRAPPER & MAIN LAYOUT - ============================================================================ */ - -.ui5-shellbar-wrapper { - box-sizing: border-box; - display: flex; - align-items: center; - width: 100%; - height: 100%; -} - /* ============================================================================ SLOTTED BUTTONS (General Styles) Assistant and Start Button slots ============================================================================ */ From fea5b10bacc34764fb436a938b73365e91b3d5ca Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Thu, 13 Nov 2025 21:30:19 +0200 Subject: [PATCH 55/57] fix: simple dom --- packages/fiori/src/themes/ShellBarV2.css | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/fiori/src/themes/ShellBarV2.css b/packages/fiori/src/themes/ShellBarV2.css index 0209820c2e8b..f45de2c2256b 100644 --- a/packages/fiori/src/themes/ShellBarV2.css +++ b/packages/fiori/src/themes/ShellBarV2.css @@ -201,20 +201,6 @@ of the search field container in the bar even when the search is in full mode. * background-color: var(--_ui5-shellbar_separator-color); } -/* ============================================================================ - ACTIONS AREA - ============================================================================ */ - -.ui5-shellbar-actions-area { - flex-shrink: 0; - display: flex; - align-items: center; -} - -.ui5-shellbar-actions-area--no-search { - margin-left: auto; -} - /* ============================================================================ CUSTOM ITEMS ============================================================================ */ From 8cc0c579e49e749d683e7f787f0537345ddb5951 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Fri, 14 Nov 2025 09:21:25 +0200 Subject: [PATCH 56/57] fix: sort items --- packages/fiori/src/ShellBarV2.ts | 44 ++-- packages/fiori/src/ShellBarV2Template.tsx | 2 +- .../fiori/src/shellbarv2/ShellBarOverflow.ts | 16 +- .../templates/ShellBarLegacyTemplate.tsx | 2 +- .../ShellBarSearchLegacyTemplate.tsx | 4 +- .../fiori/test/pages/ShellBar_evolution.html | 209 +++++++++++++++--- 6 files changed, 224 insertions(+), 53 deletions(-) diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts index 18f48ef43d2d..ae61a28c7954 100644 --- a/packages/fiori/src/ShellBarV2.ts +++ b/packages/fiori/src/ShellBarV2.ts @@ -70,13 +70,16 @@ import type ListItemBase from "@ui5/webcomponents/dist/ListItemBase.js"; type ShellBarV2Breakpoint = "S" | "M" | "L" | "XL" | "XXL"; +// actions always visible in lean mode, order is important +const PREDEFINED_PLACE_ITEMS = ["feedback", "sys-help"]; + const ShellBarV2Actions = { Search: "search", Profile: "profile", Overflow: "overflow", Assistant: "assistant", - Notifications: "notifications", ProductSwitch: "products", + Notifications: "notifications", }; const ShellBarV2ActionsSelectors = { @@ -84,8 +87,8 @@ const ShellBarV2ActionsSelectors = { Profile: ".ui5-shellbar-image-button", Overflow: ".ui5-shellbar-overflow-button", Assistant: ".ui5-shellbar-assistant-button", - Notifications: ".ui5-shellbar-bell-button", ProductSwitch: ".ui5-shellbar-button-product-switch", + Notifications: ".ui5-shellbar-bell-button", }; type ShellBarV2ActionId = typeof ShellBarV2Actions[keyof typeof ShellBarV2Actions]; @@ -376,7 +379,7 @@ class ShellBarV2 extends UI5Element { * You can use the ``. * @public */ - @slot({ type: HTMLElement, "default": true }) + @slot({ type: HTMLElement, "default": true, individualSlots: true }) items!: Array; /** @@ -731,8 +734,8 @@ class ShellBarV2 extends UI5Element { [ShellBarV2Actions.Profile]: this.texts.profile, [ShellBarV2Actions.Overflow]: this.texts.overflow, [ShellBarV2Actions.Assistant]: this.texts.assistant, - [ShellBarV2Actions.Notifications]: this.texts.notificationsNoCount, [ShellBarV2Actions.ProductSwitch]: this.texts.products, + [ShellBarV2Actions.Notifications]: this.texts.notificationsNoCount, }; return texts[actionId] || actionId; } @@ -763,7 +766,7 @@ class ShellBarV2 extends UI5Element { const result = this.overflow.updateOverflow({ actions: this.actions, content: this.sortContent(this.content), - customItems: this.items, + customItems: this.sortItems(this.items), hiddenItemsIds: this.hiddenItemsIds, showSearchField: this.enabledFeatures.search && this.showSearchField, overflowOuter: this.overflowOuter!, @@ -857,7 +860,7 @@ class ShellBarV2 extends UI5Element { get overflowItems() { return this.overflow.getOverflowItems({ actions: this.actions, - customItems: this.items, + customItems: this.sortItems(this.items), hiddenItemsIds: this.hiddenItemsIds, }); } @@ -1020,6 +1023,21 @@ class ShellBarV2 extends UI5Element { }; } + sortContent(content: readonly HTMLElement[]) { + // reverse so items on the right are hidden first + // then sort by hide order to apply custom preferences + return content.toReversed().toSorted((a, b) => { + const aOrder = parseInt(a.getAttribute("data-hide-order") || "0"); + const bOrder = parseInt(b.getAttribute("data-hide-order") || "0"); + return aOrder - bOrder; + }); + } + + /* + * Determines whether a separator should be packed with an item. + * Separators are packed with the last item that is hidden to account for + * the space they occupy when next overflow calculation occurs. + */ getPackedSeparatorInfo(item: HTMLElement, isStartGroup: boolean) { const group = isStartGroup ? this.startContent : this.endContent; const sorted = this.sortContent(group); @@ -1029,13 +1047,13 @@ class ShellBarV2 extends UI5Element { return { shouldPack: isHidden && isLastItem }; } - sortContent(content: HTMLElement[]) { - // reverse so items on the right are hidden first - // then sort by hide order to apply custom preferences - return content.toReversed().toSorted((a, b) => { - const aOrder = parseInt(a.getAttribute("data-hide-order") || "0"); - const bOrder = parseInt(b.getAttribute("data-hide-order") || "0"); - return aOrder - bOrder; + /* =================== Items Management =================== */ + + sortItems(items: readonly ShellBarV2Item[]) { + return items.toSorted((a, b) => { + const aIndex = PREDEFINED_PLACE_ITEMS.indexOf(a.icon || ""); + const bIndex = PREDEFINED_PLACE_ITEMS.indexOf(b.icon || ""); + return aIndex - bIndex; }); } diff --git a/packages/fiori/src/ShellBarV2Template.tsx b/packages/fiori/src/ShellBarV2Template.tsx index 8d8422a35800..f841397870c4 100644 --- a/packages/fiori/src/ShellBarV2Template.tsx +++ b/packages/fiori/src/ShellBarV2Template.tsx @@ -139,7 +139,7 @@ export default function ShellBarV2Template(this: ShellBarV2) { )} {/* Custom Items */} - {this.items.map(item => ( + {this.sortItems(this.items).map(item => (