+ {{#each startContent}}
+
+
+
+ {{/each}}
+
+ {{#each endContent}}
+
+
+
+ {{/each}}
+
+ {{#if hasSearchField}}
+ {{#if _showFullWidthSearch}}
+
+
+
+
+
+ {{_cancelBtnText}}
+
+
+ {{/if}}
-
-
-
- {{#unless hasMidContent }}
- {{#unless _isXXLBreakpoint }}
- {{#if hasSearchField}}
- {{#if _fullWidthSearch}}
-
-
+
+ {{#unless _showFullWidthSearch}}
+ {{/unless}}
-
- {{_cancelBtnText}}
-
-
- {{/if}}
-
- {{#unless _fullWidthSearch}}
-
- {{/unless}}
-
+
+ {{/if}}
+ {{#if hasAssistant}}
+
+
+
+ {{/if}}
-
- {{/if}}
- {{/unless}}
- {{/unless}}
+ {{#if showNotifications}}
+
+ {{/if}}
- {{#if hasAssistant}}
-
- {{/if}}
+ {{#each customItemsInfo}}
+
+ {{/each}}
- {{#each customItemsInfo}}
- {{/each}}
-
- {{#if showNotifications}}
-
- {{/if}}
-
-
- {{#if hasProfile}}
- {{> profileButton}}
- {{/if}}
+ {{#if hasProfile}}
+ {{> profileButton}}
+ {{/if}}
- {{#if showProductSwitch}}
-
- {{/if}}
+ {{#if showProductSwitch}}
+
+ {{/if}}
+
-
{{#*inline "profileButton"}}
@@ -216,9 +211,10 @@
profile-btn
id="{{this._id}}-item-3"
@click={{_handleProfilePress}}
- style="{{styles.items.profile}}"
tooltip="{{_profileText}}"
- class="ui5-shellbar-button ui5-shellbar-image-button"
+ class="ui5-shellbar-button ui5-shellbar-image-button ui5-shellbar-no-overflow-button ui5-shellbar-items-for-arrow-nav"
+ aria-label="{{imageBtnText}}"
+ aria-haspopup="dialog"
.accessibilityAttributes={{accInfo.profile.accessibilityAttributes}}
data-ui5-stable="profile"
>
@@ -226,4 +222,42 @@
{{/inline}}
+
+{{#*inline "singleLogo"}}
+
+
+
+{{/inline}}
+
+{{#*inline "combinedLogo"}}
+
+ {{#if hasLogo}}
+
+
+
+ {{/if}}
+
+ {{#if primaryTitle}}
+
+ {{primaryTitle}}
+
+ {{/if}}
+
+
+{{/inline}}
+
{{>include "./ShellBarPopover.hbs"}}
\ No newline at end of file
diff --git a/packages/fiori/src/ShellBar.ts b/packages/fiori/src/ShellBar.ts
index 87810bfaea29..f3bb40a281ea 100644
--- a/packages/fiori/src/ShellBar.ts
+++ b/packages/fiori/src/ShellBar.ts
@@ -7,13 +7,20 @@ import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
-import { isSpace, isEnter } from "@ui5/webcomponents-base/dist/Keys.js";
+import {
+ isSpace,
+ isEnter,
+ isLeft,
+ isRight,
+} from "@ui5/webcomponents-base/dist/Keys.js";
+import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js";
import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js";
import List from "@ui5/webcomponents/dist/List.js";
import type { ListSelectionChangeEventDetail } 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 Menu from "@ui5/webcomponents/dist/Menu.js";
import Icon from "@ui5/webcomponents/dist/Icon.js";
import type Input from "@ui5/webcomponents/dist/Input.js";
import type { IButton } from "@ui5/webcomponents/dist/Button.js";
@@ -25,13 +32,16 @@ import "@ui5/webcomponents-icons/dist/overflow.js";
import "@ui5/webcomponents-icons/dist/grid.js";
import "@ui5/webcomponents-icons/dist/slim-arrow-down.js";
import type {
- Timeout,
ClassMap,
AccessibilityAttributes,
AriaRole,
} 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";
// Templates
@@ -45,11 +55,18 @@ 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 = {
@@ -57,7 +74,7 @@ type ShellBarLogoAccessibilityAttributes = {
name?: string,
}
type ShellBarProfileAccessibilityAttributes = Pick
;
-type ShellBarAreaAccessibilityAttributes = Pick;
+type ShellBarAreaAccessibilityAttributes = Pick;
type ShellBarAccessibilityAttributes = {
logo?: ShellBarLogoAccessibilityAttributes
notifications?: ShellBarAreaAccessibilityAttributes
@@ -87,6 +104,10 @@ type ShellBarMenuItemClickEventDetail = {
item: HTMLElement;
};
+type ShellBarContentItemVisibilityChangeEventDetail = {
+ items: Array
+};
+
type ShellBarSearchButtonEventDetail = {
targetRef: HTMLElement;
searchFieldVisible: boolean;
@@ -96,7 +117,6 @@ interface IShelBarItemInfo {
id: string,
icon?: string,
text?: string,
- priority: number,
show: boolean,
count?: string,
custom?: boolean,
@@ -104,14 +124,17 @@ interface IShelBarItemInfo {
stableDomRef?: string,
refItemid?: string,
press: (e: MouseEvent) => void,
- styles: object,
domOrder: number,
classes: string,
order?: number,
profile?: boolean,
+ tooltip?: string,
}
-const HANDLE_RESIZE_DEBOUNCE_RATE = 200; // ms
+const RESIZE_THROTTLE_RATE = 40; // ms
+
+// actions always visible in lean mode, order is important
+const PREDEFINED_PLACE_ACTIONS = ["feedback", "sys-help"];
/**
* @class
@@ -159,6 +182,7 @@ const HANDLE_RESIZE_DEBOUNCE_RATE = 200; // ms
List,
Popover,
ListItemStandard,
+ Menu,
],
})
/**
@@ -230,6 +254,18 @@ const HANDLE_RESIZE_DEBOUNCE_RATE = 200; // ms
bubbles: true,
})
+/**
+ * Fired, when an item from the startContent or endContent slots 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,
@@ -238,6 +274,7 @@ class ShellBar extends UI5Element {
"logo-click": ShellBarLogoClickEventDetail,
"menu-item-click": ShellBarMenuItemClickEventDetail,
"search-button-click": ShellBarSearchButtonEventDetail,
+ "content-item-visibility-change": ShellBarContentItemVisibilityChangeEventDetail
}
/**
* Defines the `primaryTitle`.
@@ -327,14 +364,14 @@ class ShellBar extends UI5Element {
* @public
* @since 1.10.0
*/
- @property({ type: Object })
- accessibilityAttributes: ShellBarAccessibilityAttributes = {};
+ @property({ type: Object })
+ accessibilityAttributes: ShellBarAccessibilityAttributes = {};
/**
* @private
*/
@property()
- breakpointSize?: string;
+ breakpointSize = "S";
/**
* @private
@@ -355,10 +392,12 @@ class ShellBar extends UI5Element {
_overflowPopoverExpanded = false;
@property({ type: Boolean, noAttribute: true })
- _fullWidthSearch = false;
+ hasVisibleStartContent = false;
@property({ type: Boolean, noAttribute: true })
- _isXXLBreakpoint = false;
+ hasVisibleEndContent = false;
+
+ _cachedHiddenContent: Array = [];
/**
* Defines the assistant slot.
@@ -401,7 +440,7 @@ class ShellBar extends UI5Element {
logo!: Array;
/**
- * Defines the items displayed in menu after a click on the primary title.
+ * Defines the items displayed in menu after a click on a start button.
*
* **Note:** You can use the `` and its ancestors.
* @since 0.10
@@ -419,7 +458,7 @@ class ShellBar extends UI5Element {
/**
* Defines a `ui5-button` in the bar that will be placed in the beginning.
- * We encourage this slot to be used for a back or home button.
+ * We encourage this slot to be used for a menu button.
* It gets overstyled to match ShellBar's styling.
* @public
*/
@@ -435,6 +474,26 @@ class ShellBar extends UI5Element {
@slot()
midContent!: Array;
+ /**
+ * Define the items displayed in the start of the additional content area.
+ * **Note:** The `startContent` slot is in an experimental state and is a subject to change.
+ *
+ * @public
+ * @since 2.7.0
+ */
+ @slot({ type: HTMLElement, individualSlots: true })
+ startContent!: Array;
+
+ /**
+ * Define the items displayed in the end of the additional content area.
+ * **Note:** The `endContent` slot is in an experimental state and is a subject to change.
+ *
+ * @public
+ * @since 2.7.0
+ */
+ @slot({ type: HTMLElement, individualSlots: true })
+ endContent!: Array;
+
@i18n("@ui5/webcomponents-fiori")
static i18nBundle: I18nBundle;
overflowPopover?: Popover | null;
@@ -442,9 +501,16 @@ class ShellBar extends UI5Element {
_isInitialRendering: boolean;
_defaultItemPressPrevented: boolean;
menuItemsObserver: MutationObserver;
- _debounceInterval?: Timeout | null;
+ additionalContextObserver: MutationObserver;
_hiddenIcons: Array;
_handleResize: ResizeObserverCallback;
+ _overflowNotifications: string | null;
+ _lastOffsetWidth = 0;
+ _observableContent: Array = [];
+ _searchBarAutoOpen: boolean = false;
+ _searchBarAutoClosed: boolean = false;
+ _searchIconPressed: boolean = false;
+
_headerPress: () => void;
static get FIORI_3_BREAKPOINTS() {
@@ -474,6 +540,7 @@ class ShellBar extends UI5Element {
this._hiddenIcons = [];
this._itemsInfo = [];
this._isInitialRendering = true;
+ this._overflowNotifications = null;
// marks if preventDefault() is called in item's press handler
this._defaultItemPressPrevented = false;
@@ -482,6 +549,10 @@ class ShellBar extends UI5Element {
this._updateClonedMenuItems();
});
+ this.additionalContextObserver = new MutationObserver(() => {
+ this._updateAdditionalContextItems();
+ });
+
this._headerPress = () => {
this._updateClonedMenuItems();
@@ -492,22 +563,113 @@ class ShellBar extends UI5Element {
}
};
- this._handleResize = () => {
- this._debounce(() => {
- this.menuPopover = this._getMenuPopover();
- this.overflowPopover = this._getOverflowPopover();
- this.overflowPopover.open = false;
+ this._handleResize = throttle(() => {
+ this.menuPopover = this._getMenuPopover();
+ this.overflowPopover = this._getOverflowPopover();
+ this.overflowPopover.open = false;
+ if (this._lastOffsetWidth !== this.offsetWidth) {
this._overflowActions();
- }, HANDLE_RESIZE_DEBOUNCE_RATE);
- };
+ if (this._searchBarAutoOpen) {
+ this._searchBarInitialState();
+ }
+ }
+ }, RESIZE_THROTTLE_RATE);
+ }
+
+ _searchBarInitialState() {
+ const spacerWidth = this.shadowRoot!.querySelector(".ui5-shellbar-spacer") ? this.shadowRoot!.querySelector(".ui5-shellbar-spacer")!.getBoundingClientRect().width : 0;
+ const searchFieldWidth = this.domCalculatedValues("--_ui5_shellbar_search_field_width");
+ if (this._searchIconPressed || document.activeElement === this.searchField[0]) {
+ return;
+ }
+ if (this._showFullWidthSearch) {
+ this.showSearchField = false;
+ this._searchBarAutoClosed = true;
+ return;
+ }
+ if ((spacerWidth <= 0 || this.additionalContextHidden.length !== 0) && this.showSearchField === true) {
+ this.showSearchField = false;
+ this._searchBarAutoClosed = true;
+ }
+ if (spacerWidth > searchFieldWidth && this.additionalContextHidden.length === 0 && this.showSearchField === false) {
+ this.showSearchField = true;
+ this._searchBarAutoClosed = false;
+ }
+ }
+
+ _onKeyDown(e: KeyboardEvent) {
+ const items = this._getVisibleAndInteractiveItems();
+ const activeElement = getActiveElement();
+ const currentIndex = items.findIndex(el => el === activeElement);
+
+ if (isLeft(e) || isRight(e)) {
+ e.preventDefault();// Prevent the default behavior to avoid any further automatic focus movemen
+
+ // Focus navigation based on the key pressed
+ if (isLeft(e)) {
+ this._focusPreviousItem(items, currentIndex);
+ } else if (isRight(e)) {
+ this._focusNextItem(items, currentIndex);
+ }
+ }
+ }
+
+ _focusNextItem(items: HTMLElement[], currentIndex: number) {
+ if (currentIndex < items.length - 1) {
+ (items[currentIndex + 1]).focus(); // Focus the next element
+ }
+ }
+
+ _focusPreviousItem(items: HTMLElement[], currentIndex: number) {
+ if (currentIndex > 0) {
+ (items[currentIndex - 1]).focus(); // Focus the previous element
+ }
+ }
+
+ _isVisible(element: HTMLElement): boolean {
+ const style = getComputedStyle(element);
+
+ return style.display !== "none" && style.visibility !== "hidden" && element.offsetWidth > 0 && element.offsetHeight > 0;
+ }
+
+ _isInteractive(element: HTMLElement | UI5Element): boolean {
+ const component = element as UI5Element;
+ if (component.isUI5Element) {
+ const dom = component.getFocusDomRef();
+ return dom?.tabIndex === 0;
+ }
+ return element.tabIndex === 0;
+ }
+
+ _getNavigableContent() {
+ return [
+ ...this.startButton,
+ ...this.logo,
+ ...this.shadowRoot!.querySelectorAll(".ui5-shellbar-logo"),
+ ...this.shadowRoot!.querySelectorAll(".ui5-shellbar-logo-area"),
+ ...this.shadowRoot!.querySelectorAll(".ui5-shellbar-menu-button"),
+ ...this.startContent,
+ ...this.endContent,
+ ...this._getRightChildItems(),
+ ] as HTMLElement[];
+ }
+
+ _getRightChildItems() {
+ return [
+ ...this.searchField,
+ ...this.shadowRoot!.querySelectorAll(".ui5-shellbar-search-item-for-arrow-nav"),
+ ...this.assistant,
+ ...this.shadowRoot!.querySelectorAll(".ui5-shellbar-items-for-arrow-nav"),
+ ] as HTMLElement[];
}
- _debounce(fn: () => void, delay: number) {
- clearTimeout(this._debounceInterval!);
- this._debounceInterval = setTimeout(() => {
- this._debounceInterval = null;
- fn();
- }, delay);
+ _getVisibleAndInteractiveItems() {
+ const items = this._getNavigableContent();
+ const visibleAndInteractiveItems = items.filter(item => {
+ return this._isVisible(item) && this._isInteractive(item);
+ });
+
+ return visibleAndInteractiveItems;
}
_menuItemPress(e: CustomEvent) {
@@ -564,6 +726,19 @@ class ShellBar extends UI5Element {
}
}
+ _calculateCSSREMValue(styleSet: CSSStyleDeclaration, propertyName: string): number {
+ return Number(styleSet.getPropertyValue(propertyName).replace("rem", "")) * parseInt(getComputedStyle(document.body).getPropertyValue("font-size"));
+ }
+
+ _parsePxValue(styleSet: CSSStyleDeclaration, propertyName: string): number {
+ return Number(styleSet.getPropertyValue(propertyName).replace("px", ""));
+ }
+
+ domCalculatedValues(cssVar: string): number {
+ const shellbarComputerStyle = getComputedStyle(this.getDomRef()!);
+ return this._calculateCSSREMValue(shellbarComputerStyle, getScopedVarName(cssVar)); // px
+ }
+
onBeforeRendering() {
this.withLogo = this.hasLogo;
@@ -578,12 +753,26 @@ class ShellBar extends UI5Element {
});
this._observeMenuItems();
+ this._observeAdditionalContextItems();
+ this._updateSeparatorsVisibility();
}
- onAfterRendering() {
- this._overflowActions();
+ get additionalContextSorted() {
+ return this.additionalContext.sort((a, b) => {
+ return parseInt(a.getAttribute("data-hide-order") || "0") - parseInt(b.getAttribute("data-hide-order") || "0");
+ }).map(item => this.shadowRoot!.querySelector(`#${item.slot}`)).filter(item => item !== null);
+ }
- this._fullWidthSearch = this._showFullWidthSearch;
+ get additionalContextContainer() {
+ return this.shadowRoot!.querySelector(".ui5-shellbar-overflow-container-additional-content");
+ }
+
+ onAfterRendering() {
+ requestAnimationFrame(() => {
+ this._lastOffsetWidth = this.offsetWidth;
+ this._overflowActions();
+ });
+ this._searchBarAutoOpen = this._searchBarAutoClosed || (this.showSearchField && !this._searchIconPressed);
}
/**
@@ -607,88 +796,132 @@ class ShellBar extends UI5Element {
if (this.breakpointSize !== mappedSize) {
this.breakpointSize = mappedSize;
}
-
- this._isXXLBreakpoint = this.breakpointSize === "XXL";
- return mappedSize;
}
- _handleSizeS() {
- const hasIcons = this.showNotifications || this.showProductSwitch || !!this.searchField.length || !!this.items.length;
-
- const newItems = this._getAllItems(hasIcons).map((info): IShelBarItemInfo => {
- const isOverflowIcon = info.classes.indexOf("ui5-shellbar-overflow-button") !== -1;
- const isImageIcon = info.classes.indexOf("ui5-shellbar-image-button") !== -1;
- const shouldStayOnScreen = isOverflowIcon || (isImageIcon && this.hasProfile);
+ _hideOverflowItems(hiddenItems: number, items: IShelBarItemInfo[]) {
+ for (let i = 0; hiddenItems > 0 && i < items.length; i++) {
+ // start from last item
+ const item = items[items.length - 1 - i];
+ if (item.classes.indexOf("ui5-shellbar-no-overflow-button") === -1) {
+ item.classes = `${item.classes} ui5-shellbar-hidden-button`;
+ hiddenItems--;
+ }
+ }
- return {
- ...info,
- classes: `${info.classes} ${shouldStayOnScreen ? "" : "ui5-shellbar-hidden-button"} ui5-shellbar-button`,
- styles: {
- order: shouldStayOnScreen ? 1 : -1,
- },
- };
- });
+ // assistant is a slot, still described in the itemsInfo for the purpose of the overflow
+ // so if marked as hidden, it should be hidden separately
+ this._updateAssistantIconVisibility(items);
- this._updateItemsInfo(newItems);
+ return hiddenItems;
}
- _handleActionsOverflow() {
- const rightContainerRect = this.shadowRoot!.querySelector(".ui5-shellbar-overflow-container-right")!.getBoundingClientRect();
- let overflowSelector = ".ui5-shellbar-button:not(.ui5-shellbar-overflow-button):not(.ui5-shellbar-invisible-button)";
+ _hideAdditionalContext() {
+ const container = this.additionalContextContainer;
+ const totalWidth = container?.offsetWidth || 0;
- if (this.showSearchField) {
- overflowSelector += ",.ui5-shellbar-search-field";
- }
+ const additionalContextSorted = this.additionalContextSorted.toReversed();
- const elementsToOverflow = this.shadowRoot!.querySelectorAll
);
}
+
+function getEffectiveGroupIcon(layout: `${TimelineLayout}`, collapsed: boolean): string {
+ if (layout === TimelineLayout.Vertical) {
+ return collapsed ? slimArrowleft : slimArrowDown;
+ }
+
+ return collapsed ? slimArrowup : slimArrowRight;
+}
diff --git a/packages/fiori/src/TimelineItem.ts b/packages/fiori/src/TimelineItem.ts
index 352e6a683249..df4f8162ed24 100644
--- a/packages/fiori/src/TimelineItem.ts
+++ b/packages/fiori/src/TimelineItem.ts
@@ -3,11 +3,23 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
+import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import type Link from "@ui5/webcomponents/dist/Link.js";
+import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
+import type { I18nText } from "@ui5/webcomponents-base/dist/i18nBundle.js";
import type { ITimelineItem } from "./Timeline.js";
+import type ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
import TimelineItemTemplate from "./TimelineItemTemplate.js";
import type TimelineLayout from "./types/TimelineLayout.js";
+
+import {
+ TIMELINE_ITEM_INFORMATION_STATE_TEXT,
+ TIMELINE_ITEM_POSITIVE_STATE_TEXT,
+ TIMELINE_ITEM_NEGATIVE_STATE_TEXT,
+ TIMELINE_ITEM_CRITICAL_STATE_TEXT,
+} from "./generated/i18n/i18n-defaults.js";
+
// Styles
import TimelineItemCss from "./generated/themes/TimelineItem.css.js";
@@ -86,6 +98,15 @@ class TimelineItem extends UI5Element implements ITimelineItem {
@property()
subtitleText?: string;
+ /**
+ * Defines the state of the icon displayed in the `ui5-timeline-item`.
+ * @default "None"
+ * @public
+ * @since 2.7.0
+ */
+ @property()
+ state: `${ValueState}` = "None";
+
/**
* Defines the content of the `ui5-timeline-item`.
* @public
@@ -149,6 +170,9 @@ class TimelineItem extends UI5Element implements ITimelineItem {
@property({ type: Number })
positionInGroup?: number;
+ @i18n("@ui5/webcomponents")
+ static i18nBundle: I18nBundle;
+
constructor() {
super();
}
@@ -164,6 +188,19 @@ class TimelineItem extends UI5Element implements ITimelineItem {
this.shadowRoot!.querySelector