Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions packages/fiori/cypress/specs/Timeline.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -813,4 +813,90 @@ describe("Timeline Header Bar", () => {
cy.get("[ui5-timeline-item]").eq(0).should("have.attr", "title-text", "Meeting with John");
});
});

describe("Filter Info Bar slot", () => {
it("should render filter info bar when slot content is provided", () => {
cy.mount(
<Timeline>
<TimelineHeaderBar slot="headerBar" showFilter filterBy="Status">
<div slot="filterInfoBar" id="my-filter-bar">Filtered by: Open</div>
</TimelineHeaderBar>
<TimelineItem titleText="Meeting" />
</Timeline>
);

cy.get("[ui5-timeline-header-bar]")
.shadow()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could keep TimelineHeaderBarin in variable

.find(".ui5-timeline-filter-info-bar")
.should("exist");

cy.get("[ui5-timeline-header-bar]")
.find("#my-filter-bar")
.should("exist");
});

it("should not render filter info bar wrapper when slot is empty", () => {
cy.mount(
<Timeline>
<TimelineHeaderBar slot="headerBar" showFilter filterBy="Status">
<TimelineFilterOption text="Open" />
</TimelineHeaderBar>
<TimelineItem titleText="Meeting" />
</Timeline>
);

cy.get("[ui5-timeline-header-bar]")
.shadow()
.find(".ui5-timeline-filter-info-bar")
.should("not.exist");
});

it("should show filter info bar when content is dynamically added", () => {
cy.mount(
<Timeline>
<TimelineHeaderBar slot="headerBar" showFilter filterBy="Status">
<TimelineFilterOption text="Open" />
</TimelineHeaderBar>
<TimelineItem titleText="Meeting" />
</Timeline>
);

cy.get("[ui5-timeline-header-bar]")
.shadow()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could keep TimelineHeaderBarin in variable

.find(".ui5-timeline-filter-info-bar")
.should("not.exist");

cy.get("[ui5-timeline-header-bar]").then($headerBar => {
const infoDiv = document.createElement("div");
infoDiv.slot = "filterInfoBar";
infoDiv.id = "dynamic-filter-bar";
infoDiv.textContent = "Filtered by: Open";
$headerBar[0].appendChild(infoDiv);
});

cy.get("[ui5-timeline-header-bar]")
.shadow()
.find(".ui5-timeline-filter-info-bar")
.should("exist");
});

it("should render filter info bar between toolbar and dialog", () => {
cy.mount(
<Timeline>
<TimelineHeaderBar slot="headerBar" showFilter filterBy="Status">
<TimelineFilterOption text="Open" />
<div slot="filterInfoBar">Filtered by: Open</div>
</TimelineHeaderBar>
<TimelineItem titleText="Meeting" />
</Timeline>
);

cy.get("[ui5-timeline-header-bar]")
.shadow()
.find(".ui5-timeline-filter-info-bar")
.should("exist")
.prev("[ui5-toolbar]")
.should("exist");
});
});
});
16 changes: 16 additions & 0 deletions packages/fiori/src/TimelineHeaderBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,18 @@ class TimelineHeaderBar extends UI5Element {
@slot({ type: HTMLElement, "default": true, invalidateOnChildChange: true })
filterOptions!: Slot<TimelineFilterOption>;

/**
* Defines the content displayed below the toolbar as a filter info bar.
*
* Use this slot to show active filter summary information
* along with a clear action. The application controls the content entirely.
*
* @public
* @since 2.23.0
*/
@slot({ type: HTMLElement })
filterInfoBar!: Slot<HTMLElement>;

@i18n("@ui5/webcomponents-fiori")
static i18nBundle: I18nBundle;

Expand Down Expand Up @@ -255,6 +267,10 @@ class TimelineHeaderBar extends UI5Element {
return TimelineHeaderBar.i18nBundle.getText(TIMELINE_FILTER_DIALOG_CANCEL);
}

get _hasFilterInfoBar(): boolean {
return !!this.filterInfoBar.length;
}

_onSearchInput(e: CustomEvent) {
const value = (e.target as Input).value;
this.searchValue = value;
Expand Down
6 changes: 6 additions & 0 deletions packages/fiori/src/TimelineHeaderBarTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export default function TimelineHeaderBarTemplate(this: TimelineHeaderBar) {
)}
</Toolbar>

{this._hasFilterInfoBar && (
<div class="ui5-timeline-filter-info-bar">
<slot name="filterInfoBar"></slot>
</div>
)}

{/* Filter Dialog */}
{this.showFilter && (
<Dialog
Expand Down
6 changes: 6 additions & 0 deletions packages/fiori/src/themes/TimelineHeaderBar.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@
.ui5-timeline-filter-list {
width: 100%;
}

.ui5-timeline-filter-info-bar {
border-block-start: 0.0625rem solid var(--sapToolbar_SeparatorColor);
padding-block: 0.5rem;
padding-inline: 0.5rem;
}
172 changes: 172 additions & 0 deletions packages/fiori/test/pages/TimelineFilterInfoBar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<title>Timeline — filterInfoBar Slot</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

<script src="%VITE_BUNDLE_PATH%" type="module"></script>
<style>
body {
background-color: var(--sapBackgroundColor, #f5f6f7);
font-family: "72", "72full", Arial, Helvetica, sans-serif;
padding: 2rem;
}

.filter-info-content {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-family: "72", "72full", Arial, Helvetica, sans-serif;
}

.filter-info-content .filter-info-text {
flex: 1;
}
</style>
</head>

<body>

<ui5-timeline id="timeline" accessible-name="Project Activity Feed">
<ui5-timeline-header-bar slot="headerBar" show-search show-filter show-sort filter-by="Category">
<ui5-timeline-filter-option text="Meetings"></ui5-timeline-filter-option>
<ui5-timeline-filter-option text="Development"></ui5-timeline-filter-option>
<ui5-timeline-filter-option text="Releases"></ui5-timeline-filter-option>
</ui5-timeline-header-bar>

<ui5-timeline-item data-category="Meetings" data-date="2024-03-10T09:00"
title-text="Sprint Planning" subtitle-text="10.03.2024 09:00"
icon="calendar" name="Sarah Chen">
Kick-off for Sprint 24. Estimated velocity: 42 story points.
</ui5-timeline-item>

<ui5-timeline-item data-category="Development" data-date="2024-03-10T11:30"
title-text="PR #487 — Auth Module Refactor" subtitle-text="10.03.2024 11:30"
icon="developer-settings" name="James Rivera">
Replaced legacy session handling with JWT tokens.
</ui5-timeline-item>

<ui5-timeline-item data-category="Meetings" data-date="2024-03-10T14:00"
title-text="Client Requirements Call" subtitle-text="10.03.2024 14:00"
icon="phone" name="Emily Watson">
Discussed Q2 roadmap priorities.
</ui5-timeline-item>

<ui5-timeline-item data-category="Development" data-date="2024-03-10T16:00"
title-text="API Documentation Update" subtitle-text="10.03.2024 16:00"
icon="document" name="Arun Patel">
Added OpenAPI specs for new endpoints.
</ui5-timeline-item>

<ui5-timeline-item data-category="Releases" data-date="2024-03-11T06:00"
title-text="Deploy v2.5.1 to Staging" subtitle-text="11.03.2024 06:00"
icon="upload" name="DevOps Pipeline" state="Positive">
Staging deployment successful.
</ui5-timeline-item>

<ui5-timeline-item data-category="Meetings" data-date="2024-03-12T09:00"
title-text="Post-Release Retrospective" subtitle-text="12.03.2024 09:00"
icon="group" name="Full Team">
Key takeaway: improve staging parity with prod.
</ui5-timeline-item>
</ui5-timeline>

<script>
const timeline = document.getElementById("timeline");
let headerBar;
let allItems;
let infoBarElement;
const state = { searchQuery: "", selectedCategories: [], sortOrder: "Ascending" };

function getHeaderBar() {
if (!headerBar) {
headerBar = timeline.querySelector("ui5-timeline-header-bar");
}
return headerBar;
}

function applyFilters() {
if (!allItems) {
allItems = [...timeline.querySelectorAll("[ui5-timeline-item]")];
}

allItems.forEach(item => item.remove());

let visibleItems = allItems.filter(item => {
const text = [item.titleText, item.name, item.subtitleText, item.textContent]
.join(" ").toLowerCase();
const matchesSearch = !state.searchQuery || text.includes(state.searchQuery);
const matchesCategory = state.selectedCategories.length === 0
|| state.selectedCategories.includes(item.dataset.category);
return matchesSearch && matchesCategory;
});

visibleItems.sort((itemA, itemB) => {
const dateA = new Date(itemA.dataset.date || 0);
const dateB = new Date(itemB.dataset.date || 0);
return state.sortOrder === "Ascending" ? dateA - dateB : dateB - dateA;
});

visibleItems.forEach(item => timeline.appendChild(item));
}

function updateFilterInfoBar() {
if (state.selectedCategories.length > 0) {
if (!infoBarElement) {
infoBarElement = document.createElement("div");
infoBarElement.slot = "filterInfoBar";
infoBarElement.classList.add("filter-info-content");

const textElement = document.createElement("ui5-text");
textElement.classList.add("filter-info-text");

const clearButton = document.createElement("ui5-button");
clearButton.icon = "decline";
clearButton.design = "Transparent";
clearButton.tooltip = "Clear filters";
clearButton.addEventListener("click", clearFilters);

infoBarElement.appendChild(textElement);
infoBarElement.appendChild(clearButton);
getHeaderBar().appendChild(infoBarElement);
}

infoBarElement.querySelector(".filter-info-text").textContent =
"Filtered By: " + state.selectedCategories.join(", ");
} else if (infoBarElement) {
infoBarElement.remove();
infoBarElement = null;
}
}

function clearFilters() {
getHeaderBar().querySelectorAll("ui5-timeline-filter-option")
.forEach(option => { option.selected = false; });

state.selectedCategories = [];
updateFilterInfoBar();
applyFilters();
}

timeline.addEventListener("search", event => {
state.searchQuery = event.detail.value.toLowerCase();
applyFilters();
});

timeline.addEventListener("filter", event => {
state.selectedCategories = event.detail.selectedOptions;
updateFilterInfoBar();
applyFilters();
});

timeline.addEventListener("sort", event => {
state.sortOrder = event.detail.sortOrder;
applyFilters();
});
</script>

</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ import InCard from "../../../_samples/fiori/Timeline/InCard/InCard.md";
import WithGroups from "../../../_samples/fiori/Timeline/WithGroups/WithGroups.md";
import WithState from "../../../_samples/fiori/Timeline/WithState/WithState.md";
import WithGrowing from "../../../_samples/fiori/Timeline/WithGrowing/WithGrowing.md";
import WithSearch from "../../../_samples/fiori/Timeline/WithSearch/WithSearch.md";
import WithFilter from "../../../_samples/fiori/Timeline/WithFilter/WithFilter.md";
import WithHeaderBar from "../../../_samples/fiori/Timeline/WithHeaderBar/WithHeaderBar.md";

<%COMPONENT_OVERVIEW%>

Expand Down Expand Up @@ -34,16 +31,4 @@ import WithHeaderBar from "../../../_samples/fiori/Timeline/WithHeaderBar/WithHe

### Timeline with Growing

<WithGrowing/>

### Timeline with Search

<WithSearch />

### Timeline with Filter and Sort

<WithFilter />

### Timeline with Header Bar (Combined)

<WithHeaderBar />
<WithGrowing/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
slug: ../TimelineHeaderBar
---

import WithSearch from "../../../_samples/fiori/Timeline/WithSearch/WithSearch.md";
import WithFilter from "../../../_samples/fiori/Timeline/WithFilter/WithFilter.md";
import WithHeaderBar from "../../../_samples/fiori/Timeline/WithHeaderBar/WithHeaderBar.md";

<%COMPONENT_OVERVIEW%>

## Search

<WithSearch />

## Filter and Sort

<WithFilter />

## Combined (Search + Filter + Sort + Filter Info Bar)

The `ui5-timeline-header-bar` exposes a `filterInfoBar` slot where the application can show active filter information with a clear action. The component provides the styled container; the application controls the content entirely.

<WithHeaderBar />

<%COMPONENT_METADATA%>
Loading
Loading