diff --git a/packages/fiori/cypress/specs/Timeline.cy.tsx b/packages/fiori/cypress/specs/Timeline.cy.tsx
index 96b57949b5cd..feb3cc5758e2 100644
--- a/packages/fiori/cypress/specs/Timeline.cy.tsx
+++ b/packages/fiori/cypress/specs/Timeline.cy.tsx
@@ -1,12 +1,14 @@
import Timeline from "../../src/Timeline.js";
+import type { TimelineSearchEventDetail, TimelineSortEventDetail } from "../../src/Timeline.js";
import TimelineItem from "../../src/TimelineItem.js";
import TimelineGroupItem from "../../src/TimelineGroupItem.js";
+import TimelineHeaderBar from "../../src/TimelineHeaderBar.js";
+import TimelineFilterOption from "../../src/TimelineFilterOption.js";
import accept from "@ui5/webcomponents-icons/dist/accept.js";
import calendar from "@ui5/webcomponents-icons/dist/calendar.js";
import messageInformation from "@ui5/webcomponents-icons/dist/message-information.js";
import Label from "@ui5/webcomponents/dist/Label.js";
import Avatar from "@ui5/webcomponents/dist/Avatar.js";
-import UI5Element from "@ui5/webcomponents-base";
import Button from "@ui5/webcomponents/dist/Button.js";
import Input from "@ui5/webcomponents/dist/Input.js";
@@ -478,11 +480,12 @@ describe("Timeline - getFocusDomRef", () => {
);
- cy.get("[ui5-timeline], #firstItem")
- .then(($el) => {
- const timeline = $el[0],
- firstItem = $el[1];
- expect(timeline.getFocusDomRef()).to.equal(firstItem.getFocusDomRef());
+ cy.get("[ui5-timeline]")
+ .then(($timeline) => {
+ cy.get("#firstItem")
+ .then(($firstItem) => {
+ expect($timeline[0].getFocusDomRef()).to.equal($firstItem[0].getFocusDomRef());
+ });
});
});
@@ -495,21 +498,313 @@ describe("Timeline - getFocusDomRef", () => {
);
- cy.get("[ui5-timeline]")
- .as("timeline");
-
cy.get("[ui5-timeline]")
.find("#lastItem")
.realClick();
- cy.get("[ui5-timeline], #lastItem")
- .then(($el) => {
- const timeline = $el[0],
- lastItem = $el[1];
- expect(timeline.getFocusDomRef()).to.equal(lastItem.getFocusDomRef());
+ cy.get("[ui5-timeline]")
+ .then(($timeline) => {
+ cy.get("#lastItem")
+ .then(($lastItem) => {
+ expect($timeline[0].getFocusDomRef()).to.equal($lastItem[0].getFocusDomRef());
+ });
});
});
});
+describe("Timeline Header Bar", () => {
+ describe("Search functionality", () => {
+ it("should show header bar when slotted", () => {
+ cy.mount(
+
+
+
+
+
+ );
+
+ cy.get("[ui5-timeline]")
+ .shadow()
+ .find(".ui5-timeline-header-bar-wrapper")
+ .should("exist");
+ });
+
+ it("should fire search event when user types in search input", () => {
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get("[ui5-timeline]").then($timeline => {
+ $timeline.get(0).addEventListener("search", cy.stub().as("searchEvent"));
+ });
+
+ cy.get("[ui5-timeline-header-bar]")
+ .shadow()
+ .find("[ui5-input]")
+ .realClick();
+
+ cy.realType("Meeting");
+
+ cy.get("@searchEvent").should("have.been.called");
+ });
+
+ it("should include search value in event detail", () => {
+ cy.mount(
+
+
+
+
+ );
+
+ let searchValue = "";
+ cy.get("[ui5-timeline]").then($timeline => {
+ $timeline.get(0).addEventListener("search", (e: CustomEvent) => {
+ searchValue = e.detail.value;
+ });
+ });
+
+ cy.get("[ui5-timeline-header-bar]")
+ .shadow()
+ .find("[ui5-input]")
+ .realClick();
+
+ cy.realType("Test");
+
+ cy.wrap(null).then(() => {
+ expect(searchValue).to.equal("Test");
+ });
+ });
+ });
+
+ describe("Filter functionality", () => {
+ it("should show filter dropdown when showFilter is true", () => {
+ cy.mount(
+
+
+
+
+
+
+
+ );
+
+ cy.get("[ui5-timeline-header-bar]")
+ .shadow()
+ .find("[ui5-select]")
+ .should("exist");
+ });
+
+ it("should fire filter event when filter selection changes", () => {
+ cy.mount(
+
+
+
+
+
+
+
+ );
+
+ cy.get("[ui5-timeline]").then($timeline => {
+ $timeline.get(0).addEventListener("filter", cy.stub().as("filterEvent"));
+ });
+
+ // Click on the select to open it
+ cy.get("[ui5-timeline-header-bar]")
+ .shadow()
+ .find("[ui5-select]")
+ .realClick();
+
+ // Use keyboard to select next option
+ cy.realPress("ArrowDown");
+ cy.realPress("Enter");
+
+ cy.get("@filterEvent").should("have.been.called");
+ });
+ });
+
+ describe("Sort functionality", () => {
+ it("should show sort button when showSort is true", () => {
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get("[ui5-timeline-header-bar]")
+ .shadow()
+ .find("[ui5-button]")
+ .should("exist");
+ });
+
+ it("should fire sort event when user clicks sort button", () => {
+ cy.mount(
+
+
+
+
+ );
+ cy.get("[ui5-timeline]").then($timeline => {
+ $timeline.get(0).addEventListener("sort", cy.stub().as("sortEvent"));
+ });
+
+ cy.get("[ui5-timeline-header-bar]")
+ .shadow()
+ .find("[ui5-button]")
+ .realClick();
+
+ cy.get("@sortEvent").should("have.been.called");
+ });
+
+ it("should toggle sort order on consecutive clicks", () => {
+ cy.mount(
+
+
+
+
+ );
+
+ const sortOrders: string[] = [];
+ cy.get("[ui5-timeline]").then($timeline => {
+ $timeline.get(0).addEventListener("sort", (e: CustomEvent) => {
+ sortOrders.push(e.detail.sortOrder);
+ });
+ });
+
+ cy.get("[ui5-timeline-header-bar]")
+ .shadow()
+ .find("[ui5-button]")
+ .as("sortButton");
+
+ cy.get("@sortButton").realClick();
+ cy.get("@sortButton").realClick();
+ cy.get("@sortButton").realClick();
+
+ cy.wrap(null).then(() => {
+ expect(sortOrders).to.deep.equal(["Ascending", "Descending", "Ascending"]);
+ });
+ });
+ });
+
+ describe("Accessibility", () => {
+ it("should have correct ARIA role on header bar", () => {
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get("[ui5-timeline-header-bar]")
+ .shadow()
+ .find(".ui5-timeline-header-bar")
+ .should("have.attr", "role", "toolbar");
+ });
+ it("should have accessible name on search input", () => {
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get("[ui5-timeline-header-bar]")
+ .shadow()
+ .find("[ui5-input]")
+ .should("have.attr", "accessible-name");
+ });
+ });
+
+ describe("Combined features", () => {
+ it("should support all features together", () => {
+ cy.mount(
+
+
+
+
+
+
+
+
+
+ );
+
+ // Search input should exist
+ cy.get("[ui5-timeline-header-bar]")
+ .shadow()
+ .find("[ui5-input]")
+ .should("exist");
+
+ // Filter select should exist
+ cy.get("[ui5-timeline-header-bar]")
+ .shadow()
+ .find("[ui5-select]")
+ .should("exist");
+
+ // Sort button should exist
+ cy.get("[ui5-timeline-header-bar]")
+ .shadow()
+ .find("[ui5-button]")
+ .should("exist");
+ });
+ });
+
+ describe("Application-side filtering", () => {
+ it("application can remove items from DOM based on search event", () => {
+ cy.mount(
+
+
+
+
+
+ );
+
+ // Application handles filtering by removing non-matching items from DOM
+ cy.get("[ui5-timeline]").then($timeline => {
+ const timeline = $timeline.get(0) as Timeline;
+ const allItems = Array.from(timeline.querySelectorAll("[ui5-timeline-item]")) as TimelineItem[];
+
+ timeline.addEventListener("search", (e: CustomEvent) => {
+ const searchValue = e.detail.value.toLowerCase();
+
+ if (searchValue === "") {
+ // Restore all items when search is cleared
+ allItems.forEach(item => {
+ if (!item.parentElement) {
+ timeline.appendChild(item);
+ }
+ });
+ } else {
+ // Remove non-matching items from DOM
+ allItems.forEach(item => {
+ const titleText = item.titleText?.toLowerCase() || "";
+ if (!titleText.includes(searchValue)) {
+ item.remove();
+ } else if (!item.parentElement) {
+ timeline.appendChild(item);
+ }
+ });
+ }
+ });
+ });
+
+ // Type in search
+ cy.get("[ui5-timeline-header-bar]")
+ .shadow()
+ .find("[ui5-input]")
+ .realClick();
+
+ cy.realType("Meeting");
+
+ // Application filtered - only matching item remains in DOM
+ cy.get("[ui5-timeline-item]").should("have.length", 1);
+ cy.get("[ui5-timeline-item]").eq(0).should("have.attr", "title-text", "Meeting with John");
+ });
+ });
+});
\ No newline at end of file
diff --git a/packages/fiori/src/Timeline.ts b/packages/fiori/src/Timeline.ts
index 77aed3ff5e68..3ae6e1f87f8e 100644
--- a/packages/fiori/src/Timeline.ts
+++ b/packages/fiori/src/Timeline.ts
@@ -1,5 +1,5 @@
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
-import type { DefaultSlot } from "@ui5/webcomponents-base/dist/UI5Element.js";
+import type { DefaultSlot, Slot } from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";
@@ -21,7 +21,10 @@ import type ToggleButton from "@ui5/webcomponents/dist/ToggleButton.js";
import "./TimelineItem.js";
import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
import NavigationMode from "@ui5/webcomponents-base/dist/types/NavigationMode.js";
-import { TIMELINE_ARIA_LABEL, TIMELINE_LOAD_MORE_BUTTON_TEXT } from "./generated/i18n/i18n-defaults.js";
+import {
+ TIMELINE_ARIA_LABEL,
+ TIMELINE_LOAD_MORE_BUTTON_TEXT,
+} from "./generated/i18n/i18n-defaults.js";
import TimelineTemplate from "./TimelineTemplate.js";
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
import debounce from "@ui5/webcomponents-base/dist/util/debounce.js";
@@ -35,6 +38,8 @@ import TimelineLayout from "./types/TimelineLayout.js";
import TimelineGrowingMode from "./types/TimelineGrowingMode.js";
import { getFirstFocusableElement } from "@ui5/webcomponents-base/dist/util/FocusableElements.js";
import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js";
+import type TimelineHeaderBar from "./TimelineHeaderBar.js";
+import type { TimelineHeaderBarSearchEventDetail, TimelineHeaderBarFilterEventDetail, TimelineHeaderBarSortEventDetail } from "./TimelineHeaderBar.js";
/**
* Interface for components that may be slotted inside `ui5-timeline` as items
@@ -54,8 +59,17 @@ interface ITimelineItem extends UI5Element, ITabbable {
isNextItemGroup?: boolean;
firstItemInTimeline?: boolean;
effectiveRole?: string;
+ titleText?: string;
+ name?: string;
+ subtitleText?: string;
}
+type TimelineSearchEventDetail = TimelineHeaderBarSearchEventDetail;
+
+type TimelineFilterEventDetail = TimelineHeaderBarFilterEventDetail;
+
+type TimelineSortEventDetail = TimelineHeaderBarSortEventDetail;
+
const SHORT_LINE_WIDTH = "ShortLineWidth";
const LARGE_LINE_WIDTH = "LargeLineWidth";
const GROWING_WITH_SCROLL_DEBOUNCE_RATE = 250; // ms
@@ -70,6 +84,15 @@ const GROWING_WITH_SCROLL_DEBOUNCE_RATE = 250; // ms
* These entries can be generated by the system (for example, value XY changed from A to B), or added manually.
* There are two distinct variants of the timeline: basic and social. The basic timeline is read-only,
* while the social timeline offers a high level of interaction and collaboration, and is integrated within SAP Jam.
+ *
+ * ### Header Bar
+ *
+ * The Timeline supports a `header-bar` slot for search, filter, and sort functionality.
+ * Use the `ui5-timeline-header-bar` component in this slot.
+ * The Timeline fires `search`, `filter`, and `sort` events that the application should handle
+ * by adding, removing, or reordering items in the DOM. The Timeline itself does not perform
+ * filtering or sorting — it renders whatever items are provided in the default slot.
+ *
* @constructor
* @extends UI5Element
* @public
@@ -94,9 +117,55 @@ const GROWING_WITH_SCROLL_DEBOUNCE_RATE = 250; // ms
bubbles: true,
})
+/**
+ * Fired when the user performs a search in the header bar.
+ *
+ * **Note:** The Timeline does not perform filtering. The application should handle
+ * this event and add/remove items from the DOM to reflect the search results.
+ *
+ * @param {string} value The search value entered by the user.
+ * @public
+ * @since 2.20.0
+ */
+@event("search", {
+ bubbles: true,
+})
+
+/**
+ * Fired when the user changes filter selection in the header bar.
+ *
+ * **Note:** The Timeline does not perform filtering. The application should handle
+ * this event and add/remove items from the DOM to reflect the filter selection.
+ *
+ * @param {string} filterBy The filter category.
+ * @param {string[]} selectedOptions The selected filter option texts.
+ * @public
+ * @since 2.20.0
+ */
+@event("filter", {
+ bubbles: true,
+})
+
+/**
+ * Fired when the user changes sort order in the header bar.
+ *
+ * **Note:** The Timeline does not perform sorting. The application should handle
+ * this event and reorder the items in the DOM accordingly.
+ *
+ * @param {string} sortOrder The sort order ("Ascending" or "Descending").
+ * @public
+ * @since 2.20.0
+ */
+@event("sort", {
+ bubbles: true,
+})
+
class Timeline extends UI5Element {
eventDetails!: {
"load-more": void,
+ "search": TimelineSearchEventDetail,
+ "filter": TimelineFilterEventDetail,
+ "sort": TimelineSortEventDetail,
}
/**
* Defines the items orientation.
@@ -167,6 +236,19 @@ class Timeline extends UI5Element {
@slot({ type: HTMLElement, individualSlots: true, "default": true })
items!: DefaultSlot;
+ /**
+ * Defines the header bar of the timeline.
+ * Use `ui5-timeline-header-bar` for filtering, sorting, and search functionality.
+ *
+ * **Note:** The Timeline fires `search`, `filter`, and `sort` events when the user interacts
+ * with the header bar. The application should handle these events to filter/sort the items.
+ *
+ * @public
+ * @since 2.20.0
+ */
+ @slot()
+ headerBar!: Slot;
+
@query(".ui5-timeline-end-marker")
timelineEndMarker!: HTMLElement;
@@ -215,6 +297,14 @@ class Timeline extends UI5Element {
return this.growing === TimelineGrowingMode.Button;
}
+ get _hasHeaderBar(): boolean {
+ return this.headerBar.length > 0;
+ }
+
+ onExitDOM() {
+ this.unobserveTimelineEnd();
+ }
+
onAfterRendering() {
if (this.growsOnScroll) {
this.observeTimelineEnd();
@@ -225,10 +315,6 @@ class Timeline extends UI5Element {
this.growingIntersectionObserver = this.getIntersectionObserver();
}
- onExitDOM() {
- this.unobserveTimelineEnd();
- }
-
async observeTimelineEnd() {
if (!this.timeLineEndObserved) {
await renderFinished();
@@ -473,4 +559,7 @@ Timeline.define();
export default Timeline;
export type {
ITimelineItem,
+ TimelineSearchEventDetail,
+ TimelineFilterEventDetail,
+ TimelineSortEventDetail,
};
diff --git a/packages/fiori/src/TimelineFilterOption.ts b/packages/fiori/src/TimelineFilterOption.ts
new file mode 100644
index 000000000000..f5b68f9a02c1
--- /dev/null
+++ b/packages/fiori/src/TimelineFilterOption.ts
@@ -0,0 +1,47 @@
+import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
+import property from "@ui5/webcomponents-base/dist/decorators/property.js";
+import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
+
+/**
+ * @class
+ *
+ * ### Overview
+ *
+ * The `ui5-timeline-filter-option` component defines individual filter values within a `ui5-timeline-header-bar`.
+ * It represents a single selectable option that users can choose to filter timeline items.
+ *
+ * ### Usage
+ *
+ * The `ui5-timeline-filter-option` is used as a child component within `ui5-timeline-header-bar`.
+ * Each option represents a specific value that can be used for filtering.
+ *
+ * ### ES6 Module Import
+ *
+ * `import "@ui5/webcomponents-fiori/dist/TimelineFilterOption.js";`
+ * @constructor
+ * @extends UI5Element
+ * @since 2.20.0
+ * @public
+ */
+@customElement("ui5-timeline-filter-option")
+class TimelineFilterOption extends UI5Element {
+ /**
+ * Defines the text of the filter option.
+ * @default ""
+ * @public
+ */
+ @property()
+ text = "";
+
+ /**
+ * Defines if the filter option is selected.
+ * @default false
+ * @public
+ */
+ @property({ type: Boolean })
+ selected = false;
+}
+
+TimelineFilterOption.define();
+
+export default TimelineFilterOption;
diff --git a/packages/fiori/src/TimelineHeaderBar.ts b/packages/fiori/src/TimelineHeaderBar.ts
new file mode 100644
index 000000000000..eb7881c88c64
--- /dev/null
+++ b/packages/fiori/src/TimelineHeaderBar.ts
@@ -0,0 +1,266 @@
+import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
+import type { Slot } from "@ui5/webcomponents-base/dist/UI5Element.js";
+import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
+import property from "@ui5/webcomponents-base/dist/decorators/property.js";
+import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.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 type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
+import type Input from "@ui5/webcomponents/dist/Input.js";
+import type Select from "@ui5/webcomponents/dist/Select.js";
+import type TimelineSortOrder from "./types/TimelineSortOrder.js";
+import type TimelineFilterOption from "./TimelineFilterOption.js";
+
+// Import icons to register them
+import "@ui5/webcomponents-icons/dist/sort.js";
+import "@ui5/webcomponents-icons/dist/sort-ascending.js";
+import "@ui5/webcomponents-icons/dist/sort-descending.js";
+
+import TimelineHeaderBarTemplate from "./TimelineHeaderBarTemplate.js";
+import TimelineHeaderBarCss from "./generated/themes/TimelineHeaderBar.css.js";
+
+import {
+ TIMELINE_HEADER_BAR_ACCESSIBLE_NAME,
+ TIMELINE_SEARCH_PLACEHOLDER,
+ TIMELINE_SEARCH_ACCESSIBLE_NAME,
+ TIMELINE_FILTER_ACCESSIBLE_NAME,
+ TIMELINE_SORT_ASCENDING_TOOLTIP,
+ TIMELINE_SORT_DESCENDING_TOOLTIP,
+ TIMELINE_SORT_ACCESSIBLE_NAME,
+} from "./generated/i18n/i18n-defaults.js";
+
+type TimelineHeaderBarSearchEventDetail = {
+ value: string;
+};
+
+type TimelineHeaderBarFilterEventDetail = {
+ filterBy: string;
+ selectedOptions: string[];
+};
+
+type TimelineHeaderBarSortEventDetail = {
+ sortOrder: string;
+};
+
+/**
+ * @class
+ *
+ * ### Overview
+ *
+ * The `ui5-timeline-header-bar` component provides search, filter, and sort functionality
+ * for the `ui5-timeline` component. It is designed to be slotted into the `header-bar` slot
+ * of the Timeline.
+ *
+ * ### Usage
+ *
+ * The component fires events (`search`, `filter`, `sort`) that the application should handle
+ * to filter/sort the timeline items. The Timeline component itself does not perform any
+ * filtering or sorting - this is the responsibility of the application.
+ *
+ * ### ES6 Module Import
+ *
+ * `import "@ui5/webcomponents-fiori/dist/TimelineHeaderBar.js";`
+ *
+ * @constructor
+ * @extends UI5Element
+ * @public
+ * @since 2.20.0
+ */
+@customElement({
+ tag: "ui5-timeline-header-bar",
+ languageAware: true,
+ renderer: jsxRenderer,
+ template: TimelineHeaderBarTemplate,
+ styles: TimelineHeaderBarCss,
+})
+
+/**
+ * Fired when the user performs a search.
+ *
+ * @param {string} value The search value entered by the user.
+ * @public
+ */
+@event("search", {
+ bubbles: true,
+})
+
+/**
+ * Fired when the user changes filter selection.
+ *
+ * @param {string} filterBy The filter category.
+ * @param {string[]} selectedOptions The selected filter option texts.
+ * @public
+ */
+@event("filter", {
+ bubbles: true,
+})
+
+/**
+ * Fired when the user changes sort order.
+ *
+ * @param {string} sortOrder The sort order ("Ascending" or "Descending").
+ * @public
+ */
+@event("sort", {
+ bubbles: true,
+})
+
+class TimelineHeaderBar extends UI5Element {
+ eventDetails!: {
+ "search": TimelineHeaderBarSearchEventDetail,
+ "filter": TimelineHeaderBarFilterEventDetail,
+ "sort": TimelineHeaderBarSortEventDetail,
+ };
+
+ /**
+ * Shows the search input field.
+ * @default false
+ * @public
+ */
+ @property({ type: Boolean })
+ showSearch = false;
+
+ /**
+ * Shows the filter dropdown.
+ * @default false
+ * @public
+ */
+ @property({ type: Boolean })
+ showFilter = false;
+
+ /**
+ * Shows the sort button.
+ * @default false
+ * @public
+ */
+ @property({ type: Boolean })
+ showSort = false;
+
+ /**
+ * Shows the filter by date option.
+ * @default false
+ * @public
+ */
+ @property({ type: Boolean })
+ showFilterByDate = false;
+
+ /**
+ * The current filter category label.
+ * @default ""
+ * @public
+ */
+ @property()
+ filterBy = "";
+
+ /**
+ * The current search value.
+ * @default ""
+ * @public
+ */
+ @property()
+ searchValue = "";
+
+ /**
+ * The current sort order.
+ * @default "None"
+ * @public
+ */
+ @property()
+ sortOrder: `${TimelineSortOrder}` = "None";
+
+ /**
+ * Filter options to display in the filter dropdown.
+ * @public
+ */
+ @slot({ type: HTMLElement, "default": true, invalidateOnChildChange: true })
+ filterOptions!: Slot;
+
+ @i18n("@ui5/webcomponents-fiori")
+ static i18nBundle: I18nBundle;
+
+ get _headerBarAccessibleName() {
+ return TimelineHeaderBar.i18nBundle.getText(TIMELINE_HEADER_BAR_ACCESSIBLE_NAME);
+ }
+
+ get _searchPlaceholder() {
+ return TimelineHeaderBar.i18nBundle.getText(TIMELINE_SEARCH_PLACEHOLDER);
+ }
+
+ get _searchAccessibleName() {
+ return TimelineHeaderBar.i18nBundle.getText(TIMELINE_SEARCH_ACCESSIBLE_NAME);
+ }
+
+ get _filterAccessibleName() {
+ return TimelineHeaderBar.i18nBundle.getText(TIMELINE_FILTER_ACCESSIBLE_NAME);
+ }
+
+ get _sortAccessibleName() {
+ return TimelineHeaderBar.i18nBundle.getText(TIMELINE_SORT_ACCESSIBLE_NAME);
+ }
+
+ get _sortTooltip() {
+ if (this.sortOrder === "Ascending") {
+ return TimelineHeaderBar.i18nBundle.getText(TIMELINE_SORT_DESCENDING_TOOLTIP);
+ }
+ return TimelineHeaderBar.i18nBundle.getText(TIMELINE_SORT_ASCENDING_TOOLTIP);
+ }
+
+ get _sortIcon() {
+ if (this.sortOrder === "Ascending") {
+ return "sort-ascending";
+ }
+ if (this.sortOrder === "Descending") {
+ return "sort-descending";
+ }
+ return "sort";
+ }
+
+ _onSearchInput(e: CustomEvent) {
+ const value = (e.target as Input).value;
+ this.searchValue = value;
+ this.fireDecoratorEvent("search", { value });
+ }
+
+ _onFilterChange(e: CustomEvent) {
+ const select = e.target as Select;
+ const selectedOption = select.selectedOption;
+ const selectedText = selectedOption?.textContent?.trim() || "";
+
+ // Update the selected state of filter options
+ this.filterOptions.forEach(option => {
+ option.selected = option.text === selectedText;
+ });
+
+ const selectedOptions = this.filterOptions
+ .filter(option => option.selected)
+ .map(option => option.text);
+
+ this.fireDecoratorEvent("filter", {
+ filterBy: this.filterBy,
+ selectedOptions,
+ });
+ }
+
+ _onSortClick() {
+ // Toggle sort order: None -> Ascending -> Descending -> Ascending
+ if (this.sortOrder === "None" || this.sortOrder === "Descending") {
+ this.sortOrder = "Ascending";
+ } else {
+ this.sortOrder = "Descending";
+ }
+
+ this.fireDecoratorEvent("sort", {
+ sortOrder: this.sortOrder,
+ });
+ }
+}
+
+TimelineHeaderBar.define();
+
+export default TimelineHeaderBar;
+export type {
+ TimelineHeaderBarSearchEventDetail,
+ TimelineHeaderBarFilterEventDetail,
+ TimelineHeaderBarSortEventDetail,
+};
diff --git a/packages/fiori/src/TimelineHeaderBarTemplate.tsx b/packages/fiori/src/TimelineHeaderBarTemplate.tsx
new file mode 100644
index 000000000000..b443b6249b53
--- /dev/null
+++ b/packages/fiori/src/TimelineHeaderBarTemplate.tsx
@@ -0,0 +1,66 @@
+import Input from "@ui5/webcomponents/dist/Input.js";
+import Button from "@ui5/webcomponents/dist/Button.js";
+import Select from "@ui5/webcomponents/dist/Select.js";
+import Option from "@ui5/webcomponents/dist/Option.js";
+import Icon from "@ui5/webcomponents/dist/Icon.js";
+import type TimelineHeaderBar from "./TimelineHeaderBar.js";
+import search from "@ui5/webcomponents-icons/dist/search.js";
+
+export default function TimelineHeaderBarTemplate(this: TimelineHeaderBar) {
+ return (
+
+ );
+}
diff --git a/packages/fiori/src/TimelineItem.ts b/packages/fiori/src/TimelineItem.ts
index 4b0a26dce81f..77931dcdffb7 100644
--- a/packages/fiori/src/TimelineItem.ts
+++ b/packages/fiori/src/TimelineItem.ts
@@ -161,6 +161,8 @@ class TimelineItem extends UI5Element implements ITimelineItem {
lastItem = false;
/**
+ * Used internally by TimelineGroupItem for collapse/expand mechanics.
+ * Applications should not use this for filtering — instead, add/remove items from the DOM.
* @private
*/
@property({ type: Boolean })
diff --git a/packages/fiori/src/TimelineTemplate.tsx b/packages/fiori/src/TimelineTemplate.tsx
index 7818da88a07a..be0d318d4293 100644
--- a/packages/fiori/src/TimelineTemplate.tsx
+++ b/packages/fiori/src/TimelineTemplate.tsx
@@ -14,6 +14,13 @@ export default function TimelineTemplate(this: Timeline) {
onFocusIn={this._onfocusin}
onKeyDown={this._onkeydown}
>
+ {/* Header Bar Slot */}
+ {this._hasHeaderBar && (
+
+ )}
+
Timeline with Various Timeline Item States
+
+
+ Timeline with Header Bar - Full Featured Demo
+ Complete example with Search, Filter, and Sort - all features working together.
+
+ Event Log: Interact with the header bar...
+
+
+ Visible Items: 6 of 6
+
+
+
+
+
+
+
+
+
+
+ Quarterly planning session with the development team
+
+
+ Review pull request for authentication module
+
+
+ Discussion about project requirements
+
+
+ Update API documentation for new endpoints
+
+
+ Deploy hotfix v2.5.1 to production environment
+
+
+ Daily standup meeting - sprint progress review
+
+
+
+
+
+
+ Timeline with Search Only
+ Search filters items by title, name, and subtitle text in real-time.
+
+
+
+
+ Plan next sprint tasks and assign story points
+
+
+ New user dashboard feature with analytics
+
+
+ Demo new features to client stakeholders
+
+
+ Deploy v2.5.0 to production environment
+
+
+
+
+
+
+ Timeline with Sort Only
+ Click the sort button to toggle between ascending (oldest first) and descending (newest first) order.
+
+ Current Order: None
+
+
+
+
+
+ Daily standup - sprint day 3
+
+
+ Review new component designs
+
+
+ Discuss microservices migration
+
+
+ End of sprint retrospective
+
+
+
+
+
+
+ Timeline with Filter Only
+ Filter items by priority level. Groups are hidden when they have no visible items.
+
+
+
+
+
+
+
+
+
+
+ Fix authentication bypass vulnerability
+
+
+ Review new search functionality
+
+
+ Update user guide for v2.5
+
+
+
+
+ Deploy v2.5.0 to production
+
+
+ Weekly team synchronization
+
+
+
+
+
+
+
+ Timeline with Combined Search and Filter
+ Both search and filter work together - items must match both criteria to be visible.
+
+
+
+
+
+
+
+
+
+ Integrate payment gateway API
+
+
+ Live product demonstration for customers
+
+
+ Scheduled downtime for database upgrade
+
+
+ Quarterly security assessment
+
+
+ Outdoor team building activity
+
+
+ Software license expiring soon
+
+
+
+
+
+
+
+
+
+
+
+ Full Demo
+ Project Activity Feed
+
+ A real-world project tracker. The application stores all items in memory and
+ re-renders only the matching subset into the DOM on every search, filter, or sort interaction.
+
+
+
+ Last action
+ —
+
+
+
+
+
+
+ Showing 8 of 8 items
+
+
+
+ —
+
+
+
+
+ Try searching deploy, filtering by Development, or sorting by date.
+ Items are added and removed from the DOM — no hidden attribute is used.
+
+
+
+
+
+
+
+
+
+
+
+
+ Kick-off for Sprint 24. Estimated velocity: 42 story points.
+
+
+
+ Replaced legacy session handling with JWT tokens. All 127 tests green.
+
+
+
+ Discussed Q2 roadmap priorities. Action items documented in Confluence.
+
+
+
+ Added OpenAPI specs for /payments and /webhooks endpoints.
+
+
+
+ Staging deployment successful. Smoke tests passed in 3m 22s.
+
+
+
+ Fixed N+1 query in dashboard endpoint. Latency p99 dropped from 1.2s to 180ms.
+
+
+
+ Production deploy complete. Zero-downtime rolling update across 12 pods.
+
+
+
+ Retro on v2.5.1 cycle. Key takeaway: improve staging parity with prod.
+
+
+
+
+
+
+
+
+ Search
+ Support Ticket History
+
+ Search removes non-matching items from the DOM and restores them when cleared.
+ Try typing auth or payment.
+
+
+
+
+
+
+
+
+ Users report 403 errors when authenticating via Safari 17.2 with SSO enabled.
+
+
+
+ Stripe webhook responses exceeding 30s timeout under peak load.
+
+
+
+ Export report omits custom fields added after v2.4 migration.
+
+
+
+ JWT refresh endpoint returns 401, causing infinite retry in the client SDK.
+
+
+
+ Bar chart overlaps axis labels when dataset exceeds 50 entries.
+
+
+
+
+
+
+
+
+ Sort
+ Build Pipeline Log
+
+ Sort reorders items in the DOM via appendChild. The connector lines
+ remain intact because the Timeline recalculates layout on every render.
+
+
+
+ Sort order
+ Default
+
+
+
+
+
+
+
+
+ TypeScript compilation succeeded in 47s. Zero errors.
+
+
+
+ ESLint passed with 3 warnings (no-unused-vars).
+
+
+
+ 1,247 tests passed. Coverage: 94.2% (+0.3%).
+
+
+
+ 2 failures in /api/webhooks suite. Retry scheduled.
+
+
+
+ Retry succeeded. Flaky test isolated and quarantined.
+
+
+
+ Deployed to staging cluster. Health checks green.
+
+
+
+
+
+
+
+
+ Filter
+ Incident Response Log
+
+ Filter by severity. When all items in a group are filtered out, the entire
+ group is removed from the DOM — not hidden.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Primary DB node unresponsive. Automatic failover to replica completed in 8s.
+
+
+
+ p99 latency exceeded 2s threshold for /search endpoint (3.1s observed).
+
+
+
+ Scaled from 8 to 12 pods in us-east-1 due to traffic surge.
+
+
+
+
+
+ Wildcard cert for *.api.example.com expires in 72 hours. Auto-renewal failed.
+
+
+
+ /var/log partition at 87% capacity on worker-node-03.
+
+
+
+ Updated lodash from 4.17.20 to 4.17.21 (security patch).
+
+
+
+
+
+
+
+
+
+ Combined
+ Team Activity Stream
+
+ Both criteria apply simultaneously. An item must match the search text and
+ the selected filter to remain in the DOM.
+
+
+
+ Active filters
+ Category: All
+
+
+
+
+
+
+
+
+
+
+
+
+
+ New REST endpoints for user preference storage. Includes rate limiting.
+
+
+
+ Approved with minor suggestions on error handling.
+
+
+
+ Event listeners were not being cleaned up on disconnect.
+
+
+
+ Canary at 5% traffic. Error rate: 0.02% (baseline: 0.03%).
+
+
+
+ Requested changes: accessibility contrast ratios need adjustment.
+
+
+
+ v2.6.0 fully rolled out. All health checks passing.
+
+
+
+ Moved auth logic to shared middleware. Reduced duplication across 14 routes.
+
+
+
+
+
+
+
+
+ Edge Case
+ Horizontal Timeline — Search + Sort
+
+ Validates that connector lines render correctly in horizontal layout after
+ items are removed and re-added. Try searching deploy then clearing.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Edge Case
+ Mixed Layout — Flat Items + Groups Interleaved
+
+ Tests ordering preservation when flat items and groups are interleaved.
+ Filter by category, then select "All" again — items must return to their original positions.
+
+
+
+ Status
+ All items visible
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This flat item should always appear first.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This flat item should stay between the two groups.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This flat item should always appear last.
+
+
+
+
+
+
+
+
+ Edge Case
+ Empty State — No Matching Results
+
+ Search for xyznonexistent to remove all items. The timeline
+ should be empty without errors. Clear search to restore all items in order.
+
+
+
+
+
+
+
+
+ Content A
+
+
+
+ Content B
+
+
+
+ Content C
+
+
+
+
+
+
+
+
+
diff --git a/packages/fiori/test/pages/TimelineHeaderBar.html b/packages/fiori/test/pages/TimelineHeaderBar.html
new file mode 100644
index 000000000000..1ea6e41fb0ec
--- /dev/null
+++ b/packages/fiori/test/pages/TimelineHeaderBar.html
@@ -0,0 +1,817 @@
+
+
+
+