diff --git a/packages/fiori/cypress/specs/Timeline.cy.ts b/packages/fiori/cypress/specs/Timeline.cy.ts
index b2c84ed3efa8..6b2e717c776d 100644
--- a/packages/fiori/cypress/specs/Timeline.cy.ts
+++ b/packages/fiori/cypress/specs/Timeline.cy.ts
@@ -1,10 +1,250 @@
import { html } from "lit";
import "../../src/Timeline.js";
import "../../src/TimelineItem.js";
-import "@ui5/webcomponents-icons/dist/accept.js";
-import "@ui5/webcomponents-icons/dist/message-information.js";
-import "@ui5/webcomponents-icons/dist/decline.js";
-import "@ui5/webcomponents-icons/dist/message-warning.js";
+import "../../src/TimelineGroupItem.js";
+
+const sample = html`
+
+
+
+
+ MR SOF02 2.43
+
+
+ Online meeting
+
+
+
+`;
+
+const sampleWithSingleItem = html`
+
+
+
+`;
+
+const groupSample = html`
+
+
+ Morning event
+
+
+ Good morning
+
+
+ 20.02.2017 11:30
+
+
+
+ 20.02.2017 11:30
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+describe("Timeline general interaction", () => {
+ it("should fire name-click event on a normal item name", () => {
+ cy.mount(sample);
+
+ cy.get("[ui5-timeline]")
+ .as("timeline")
+ .then($item => {
+ $item.get(0).addEventListener("name-click", cy.stub().as("clicked"));
+ });
+
+ cy.get("ui5-timeline-item")
+ .shadow()
+ .find("ui5-link")
+ .click();
+
+ cy.get("@clicked").should("have.been.calledOnce");
+ });
+
+ it("setting accessible-name applied on the host element is reflected on the ul tag", () => {
+ cy.mount(sample);
+ cy.get("[ui5-timeline]")
+ .shadow()
+ .find("ul")
+ .should("have.attr", "aria-label", "Timeline vertical");
+ });
+
+ it("Item within Timeline Item is rendered", () => {
+ cy.mount(sampleWithSingleItem);
+ cy.get("[ui5-timeline]")
+ .find("#testTimelineItem")
+ .shadow()
+ .find(".ui5-tli-bubble")
+ .find(".ui5-tli-desc")
+ .should("exist");
+ });
+});
+
+describe("Timeline with group items interactions", () => {
+ it("Group items are rendered", () => {
+ cy.mount(groupSample);
+
+ cy.get("[ui5-timeline]")
+ .find("[ui5-timeline-group-item][group-name='Events']")
+ .as("groupItem");
+
+ cy.get("@groupItem")
+ .eq(0)
+ .find("ui5-timeline-item")
+ .should("have.length", 4);
+ });
+
+ it("Group items are collapsed on button click", () => {
+ cy.mount(groupSample);
+
+ cy.get("[ui5-timeline]")
+ .find("[ui5-timeline-group-item][group-name='Events']")
+ .as("currentGroup");
+
+ cy.get("@currentGroup")
+ .eq(0)
+ .shadow()
+ .find("[ui5-toggle-button]")
+ .as("currentGroupButton");
+
+ cy.get("@currentGroupButton")
+ .realClick();
+
+ cy.realPress("Tab");
+
+ cy.get("[ui5-timeline]")
+ .find("[ui5-timeline-group-item][group-name='Meetings']")
+ .as("nextGroup");
+
+ cy.get("@nextGroup")
+ .eq(0)
+ .shadow()
+ .find("[ui5-toggle-button]")
+ .should("be.focused");
+ });
+
+ it("Group items are navigable", () => {
+ cy.mount(groupSample);
+
+ cy.get("[ui5-timeline]")
+ .find("[ui5-timeline-group-item][group-name='Events']")
+ .eq(0)
+ .as("currentGroup");
+
+ cy.realPress("Tab");
+ cy.realPress("ArrowDown");
+ cy.realPress("ArrowDown");
+ cy.realPress("ArrowDown");
+ cy.realPress("ArrowUp");
+
+ cy.get("@currentGroup")
+ .find("ui5-timeline-item")
+ .eq(1)
+ .should("be.focused");
+ });
+
+ it("Group can be collapsed/expanded using keyboard", () => {
+ cy.mount(groupSample);
+
+ cy.get("[ui5-timeline]")
+ .find("[ui5-timeline-group-item][group-name='Events']")
+ .as("currentGroup");
+
+ cy.get("@currentGroup")
+ .eq(0)
+ .shadow()
+ .find("[ui5-toggle-button]")
+ .as("currentGroupButton");
+
+ cy.get("@currentGroupButton")
+ .realClick();
+
+ cy.realPress("Enter");
+
+ cy.get("@currentGroup")
+ .eq(0)
+ .should("not.have.attr", "collapsed");
+
+ cy.realPress("Space");
+
+ cy.get("@currentGroup")
+ .eq(0)
+ .should("have.attr", "collapsed");
+ });
+});
+
+describe("Timeline with growing mode", () => {
+ it("tests 'loadMore' event fired upon infinite scroll", () => { // 8
+ cy.mount(html`
+
+
+
+
+
+
+
+
+
`);
+
+ cy.get("[ui5-timeline]")
+ .as("timeline");
+
+ cy.get("@timeline")
+ .then(timeline => {
+ timeline.get(0).addEventListener("ui5-load-more", cy.stub().as("loadMore"));
+ });
+
+ cy.get("#scroll-container")
+ .scrollTo("bottom", { duration: 100 });
+
+ cy.get("@loadMore")
+ .should("have.been.calledOnce");
+ });
+
+ it("Arrow down and up navigation between last item and growing button", () => {
+ cy.mount(html`
+
+
+
+
+
+ `);
+
+ cy.get("[ui5-timeline]")
+ .as("timeline");
+
+ cy.get("@timeline")
+ .find("ui5-timeline-item")
+ .last()
+ .click();
+
+ cy.get("@timeline")
+ .find("ui5-timeline-item")
+ .last()
+ .should("be.focused");
+
+ cy.realPress("Tab");
+
+ cy.get("@timeline")
+ .shadow()
+ .find("[id$='growing-btn']")
+ .should("be.focused");
+
+ cy.realPress("Tab");
+
+ cy.get("@timeline")
+ .find("ui5-timeline-item")
+ .first()
+ .should("be.focused");
+ });
+});
describe("Accessibility", () => {
beforeEach(() => {
diff --git a/packages/fiori/src/Timeline.ts b/packages/fiori/src/Timeline.ts
index 28d9a324048e..1c91071baf82 100644
--- a/packages/fiori/src/Timeline.ts
+++ b/packages/fiori/src/Timeline.ts
@@ -5,22 +5,30 @@ 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 I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
+import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
import {
isTabNext,
isTabPrevious,
+ isSpace,
+ isEnter,
} from "@ui5/webcomponents-base/dist/Keys.js";
import type { ITabbable } from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
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 } from "./generated/i18n/i18n-defaults.js";
import TimelineTemplate from "./TimelineTemplate.js";
-import "./TimelineItem.js";
-import "./TimelineGroupItem.js";
-
+import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
+import debounce from "@ui5/webcomponents-base/dist/util/debounce.js";
+import query from "@ui5/webcomponents-base/dist/decorators/query.js";
+import process from "@ui5/webcomponents-icons/dist/process.js";
+import drillDown from "@ui5/webcomponents-icons/dist/drill-down.js";
// Styles
import TimelineCss from "./generated/themes/Timeline.css.js";
import TimelineLayout from "./types/TimelineLayout.js";
+// Mode
+import TimelineGrowingMode from "./types/TimelineGrowingMode.js";
/**
* Interface for components that may be slotted inside `ui5-timeline` as items
@@ -43,6 +51,7 @@ interface ITimelineItem extends UI5Element, ITabbable {
const SHORT_LINE_WIDTH = "ShortLineWidth";
const LARGE_LINE_WIDTH = "LargeLineWidth";
+const GROWING_WITH_SCROLL_DEBOUNCE_RATE = 250; // ms
/**
* @class
@@ -66,7 +75,22 @@ const LARGE_LINE_WIDTH = "LargeLineWidth";
styles: TimelineCss,
template: TimelineTemplate,
})
+
+/**
+ * Fired when the user presses the `More` button or scrolls to the Timeline's end.
+ *
+ * **Note:** The event will be fired if `growing` is set to `Button` or `Scroll`.
+ * @public
+ * @since 2.7.0
+ */
+@event("load-more", {
+ bubbles: true,
+})
+
class Timeline extends UI5Element {
+ eventDetails!: {
+ "load-more": void,
+ }
/**
* Defines the items orientation.
* @default "Vertical"
@@ -85,6 +109,50 @@ class Timeline extends UI5Element {
@property()
accessibleName?: string;
+ /**
+ * Defines if the component should display a loading indicator over the Timeline.
+ *
+ * @default false
+ * @since 2.7.0
+ * @public
+ */
+ @property({ type: Boolean })
+ loading = false;
+
+ /**
+ * Defines the delay in milliseconds, after which the loading indicator will show up for this component.
+ * @default 1000
+ * @public
+ */
+ @property({ type: Number })
+ loadingDelay = 1000;
+
+ /**
+ * Defines whether the Timeline will have growing capability either by pressing a "More" button,
+ * or via user scroll. In both cases a `load-more` event is fired.
+ *
+ * Available options:
+ *
+ * `Button` - Displays a button at the end of the Timeline, which when pressed triggers the `load-more` event.
+ *
+ * `Scroll` -Triggers the `load-more` event when the user scrolls to the end of the Timeline.
+ *
+ * `None` (default) - The growing functionality is off.
+ *
+ * @default "None"
+ * @since 2.7.0
+ * @public
+ */
+ @property()
+ growing: `${TimelineGrowingMode}` = "None";
+
+ /**
+ * Defines the active state of the `More` button.
+ * @private
+ */
+ @property({ type: Boolean })
+ _loadMoreActive = false;
+
/**
* Determines the content of the `ui5-timeline`.
* @public
@@ -92,16 +160,22 @@ class Timeline extends UI5Element {
@slot({ type: HTMLElement, individualSlots: true, "default": true })
items!: Array;
+ @query(".ui5-timeline-end-marker")
+ timelineEndMarker!: HTMLElement;
+
@i18n("@ui5/webcomponents-fiori")
static i18nBundle: I18nBundle;
_itemNavigation: ItemNavigation;
+ growingIntersectionObserver?: IntersectionObserver | null;
+ timeLineEndObserved = false;
+ initialIntersection = true;
constructor() {
super();
this._itemNavigation = new ItemNavigation(this, {
- getItemsCallback: () => this._navigatableItems,
+ getItemsCallback: () => this._navigableItems,
});
}
@@ -111,6 +185,101 @@ class Timeline extends UI5Element {
: Timeline.i18nBundle.getText(TIMELINE_ARIA_LABEL);
}
+ get showBusyIndicatorOverlay() {
+ return !this.growsWithButton && this.loading;
+ }
+
+ get growsOnScroll(): boolean {
+ return this.growing === TimelineGrowingMode.Scroll;
+ }
+
+ get growingButtonIcon() {
+ return this.layout === TimelineLayout.Horizontal ? process : drillDown;
+ }
+
+ get growsWithButton(): boolean {
+ return this.growing === TimelineGrowingMode.Button;
+ }
+
+ onAfterRendering() {
+ if (this.growsOnScroll) {
+ this.observeTimelineEnd();
+ } else if (this.timeLineEndObserved) {
+ this.unobserveTimelineEnd();
+ }
+
+ this.growingIntersectionObserver = this.getIntersectionObserver();
+ }
+
+ onExitDOM() {
+ this.unobserveTimelineEnd();
+ }
+
+ async observeTimelineEnd() {
+ if (!this.timeLineEndObserved) {
+ await renderFinished();
+ this.getIntersectionObserver().observe(this.timelineEndMarker);
+ this.timeLineEndObserved = true;
+ }
+ }
+
+ unobserveTimelineEnd() {
+ if (this.growingIntersectionObserver) {
+ this.growingIntersectionObserver.disconnect();
+ this.growingIntersectionObserver = null;
+ this.timeLineEndObserved = false;
+ }
+ }
+
+ getIntersectionObserver(): IntersectionObserver {
+ if (!this.growingIntersectionObserver) {
+ this.growingIntersectionObserver = new IntersectionObserver(this.onIntersection.bind(this), {
+ root: null,
+ threshold: 1.0,
+ });
+ }
+
+ return this.growingIntersectionObserver;
+ }
+
+ onIntersection(entries: Array) {
+ if (this.initialIntersection) {
+ this.initialIntersection = false;
+ return;
+ }
+
+ if (entries.some(entry => entry.isIntersecting)) {
+ debounce(this.loadMore.bind(this), GROWING_WITH_SCROLL_DEBOUNCE_RATE);
+ }
+ }
+
+ loadMore() {
+ this.fireDecoratorEvent("load-more");
+ }
+
+ _onLoadMoreKeydown(e: KeyboardEvent) {
+ if (isSpace(e)) {
+ e.preventDefault();
+ this._loadMoreActive = true;
+ }
+
+ if (isEnter(e)) {
+ this._onLoadMoreClick();
+ this._loadMoreActive = true;
+ }
+ }
+
+ _onLoadMoreKeyup(e: KeyboardEvent) {
+ if (isSpace(e)) {
+ this._onLoadMoreClick();
+ }
+ this._loadMoreActive = false;
+ }
+
+ _onLoadMoreClick() {
+ this.fireDecoratorEvent("load-more");
+ }
+
_onfocusin(e: FocusEvent) {
let target = e.target as ITimelineItem | ToggleButton;
@@ -188,8 +357,8 @@ class Timeline extends UI5Element {
updatedTarget = target.shadowRoot!.querySelector("[ui5-toggle-button]")!;
}
- const nextTargetIndex = isNext ? this._navigatableItems.indexOf(updatedTarget) + 1 : this._navigatableItems.indexOf(updatedTarget) - 1;
- const nextTarget = this._navigatableItems[nextTargetIndex];
+ const nextTargetIndex = isNext ? this._navigableItems.indexOf(updatedTarget) + 1 : this._navigableItems.indexOf(updatedTarget) - 1;
+ const nextTarget = this._navigableItems[nextTargetIndex];
if (!nextTarget) {
return;
@@ -202,7 +371,7 @@ class Timeline extends UI5Element {
}
}
- get _navigatableItems() {
+ get _navigableItems() {
const navigatableItems: Array = [];
if (!this.items.length) {
diff --git a/packages/fiori/src/TimelineItem.ts b/packages/fiori/src/TimelineItem.ts
index df4f8162ed24..89596151e12b 100644
--- a/packages/fiori/src/TimelineItem.ts
+++ b/packages/fiori/src/TimelineItem.ts
@@ -11,6 +11,7 @@ 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";
+// Styles
import type TimelineLayout from "./types/TimelineLayout.js";
import {
@@ -20,7 +21,6 @@ import {
TIMELINE_ITEM_CRITICAL_STATE_TEXT,
} from "./generated/i18n/i18n-defaults.js";
-// Styles
import TimelineItemCss from "./generated/themes/TimelineItem.css.js";
/**
diff --git a/packages/fiori/src/TimelineTemplate.tsx b/packages/fiori/src/TimelineTemplate.tsx
index 6550273dc5c8..d2b6685432e0 100644
--- a/packages/fiori/src/TimelineTemplate.tsx
+++ b/packages/fiori/src/TimelineTemplate.tsx
@@ -1,20 +1,67 @@
+import Button from "@ui5/webcomponents/dist/Button.js";
import type Timeline from "./Timeline.js";
+import BusyIndicator from "@ui5/webcomponents/dist/BusyIndicator.js";
export default function TimelineTemplate(this: Timeline) {
return (
+ onKeyDown={this._onkeydown}
+ >
+
+
+ );
+}
+
+function moreRow(this: Timeline) {
+ return (
+
+
+
+ {this.loading &&
+
+
+ }
+
+ );
+}
+
+function endRow(this: Timeline) {
+ return (
+
+
);
}
diff --git a/packages/fiori/src/themes/Timeline.css b/packages/fiori/src/themes/Timeline.css
index 757a68c70f71..0e12bb56bec1 100644
--- a/packages/fiori/src/themes/Timeline.css
+++ b/packages/fiori/src/themes/Timeline.css
@@ -14,6 +14,25 @@
padding: 0;
}
+:host([layout="Horizontal"]) .ui5-timeline-list-item.ui5-timeline-list-growing {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin-inline-start: 0.625rem;
+ align-items: center;
+ min-width: 3.375rem;
+}
+:host([layout="Vertical"]) .ui5-timeline-list-item.ui5-timeline-list-growing {
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: center;
+ align-items: center;
+}
+
+:host([layout="Horizontal"]) .ui5-timeline-end-marker {
+ margin: -0.625rem;
+}
+
:host([layout="Vertical"]) .ui5-timeline-list {
display: flex;
flex-direction: column;
@@ -44,4 +63,28 @@
overflow: auto;
/* The padding values of the parent container are added to the size of scroll container */
width: calc(100% + var(--_ui5_timeline_scroll_container_offset));
+}
+
+:host([loading]) .ui5-timeline-growing-button-busy-indicator:not([_is-busy]) {
+ display: none;
+}
+
+.ui5-timeline-root,
+.ui5-timeline-busy-indicator {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ box-sizing: border-box;
+ padding-inline-end: 0;
+}
+
+:host([layout="Vertical"]) .ui5-timeline-scroll-container {
+ overflow: auto;
+ height: 100%;
+ width: 100%;
+}
+
+:host([growing="Scroll"]) .ui5-timeline-end-marker {
+ /* Ensure the list-end-marker has a block property to always be stretched and "visible" on the screen */
+ display: inline-block;
}
\ No newline at end of file
diff --git a/packages/fiori/src/types/TimelineGrowingMode.ts b/packages/fiori/src/types/TimelineGrowingMode.ts
new file mode 100644
index 000000000000..ff9d8358bad5
--- /dev/null
+++ b/packages/fiori/src/types/TimelineGrowingMode.ts
@@ -0,0 +1,22 @@
+enum TimelineGrowingMode {
+ /**
+ * Event `load-more` is fired
+ * upon pressing a "More" button at the end.
+ * @public
+ */
+ Button = "Button",
+
+ /**
+ * Event `load-more` is fired upon scroll.
+ * @public
+ */
+ Scroll = "Scroll",
+
+ /**
+ * The growing feature is not enabled.
+ * @public
+ */
+ None = "None",
+}
+
+export default TimelineGrowingMode;
diff --git a/packages/fiori/test/pages/Timeline.html b/packages/fiori/test/pages/Timeline.html
index 527b9a4bea50..6b84273bec3a 100644
--- a/packages/fiori/test/pages/Timeline.html
+++ b/packages/fiori/test/pages/Timeline.html
@@ -14,278 +14,353 @@
-
+
-
-
-
Timeline within Card Vertical
-
-
-
-
-
-
-
-
- MR SOF02 2.43
-
-
- Online meeting
-
-
-
-
-
Timeline within Card Horizontal
-
-
-
-
-
-
-
-
- MR SOF02 2.43
-
-
- Online meeting
-
-
-
+
+
+
Timeline within Card Vertical
+
+
+
+
+
+
+
+
+ MR SOF02 2.43
+
+
+ Online meeting
+
+
+
+
+
Timeline within Card Horizontal
+
+
+
+
+
+
+
+
+ MR SOF02 2.43
+
+
+ Online meeting
+
+
+
+
-
-
-
Result
-
-
-
+
-
- Basic Timeline - Vertical
+
+ Basic Timeline - Vertical
-
-
+
+
MR SOF02 2.43
-
+
Online meeting
-
+
Online meeting
-
+
Online meeting
-
+
Online meeting
-
+
Online meeting
-
+
Online meeting
-
+
Online meeting
-
+
Online meeting
-
+
-
+
-
- Basic Timeline - Horizontal
+
+ Basic Timeline - Horizontal
-
-
+
+
MR SOF02 2.43
-
+
Online meeting
-
+
Online meeting
-
+
Online meeting
-
+
Online meeting
-
+
Online meeting
-
+
Online meeting
-
+
+
+
-
+
+ Basic Timeline - Vertical With groups
+
+
+
+
+ Morning event
+
+
+ Good morning
+
+
+ 20.02.2017 11:30
+
-
- Basic Timeline - Vertical With groups
-
-
+
+
+ 20.02.2017 11:30
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- Morning event
-
+
+ Basic Timeline - Vertical
+
+
+
Good morning
-
- 20.02.2017 11:30
-
-
-
-
-
-
- 20.02.2017 11:30
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Basic Timeline - Vertical
-
-
-
-
- Good morning
-
-
- 20.02.2017 11:30
-
- 20.02.2017 11:30
-
-
-
-
-
-
-
-
+
+ 20.02.2017 11:30
+
+ 20.02.2017 11:30
+
+
+
+
+
+
+
+
-
- Basic Timeline - Horizontal With Groups
-
-
-
-
-
-
-
-
- 20.02.2017 11:30
-
-
-
-
-
-
-
-
-
-
+
+ Basic Timeline - Horizontal With Groups
+
+
+
+
+
+
+
+
+ 20.02.2017 11:30
+
+
+
+
+
+
+
+
+
+
-
- Basic Timeline - Horizontal
-
-
- Test item
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad sequi magnam nam sed numquam.
-
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad sequi magnam nam sed numquam.
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad sequi magnam nam sed numquam.
-
- 20.02.2017 11:30
-
-
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad sequi magnam nam sed numquam.
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad sequi magnam nam sed numquam.
-
-
-
-
+
+ Basic Timeline - Horizontal
+
+
+ Test item
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad
+ sequi magnam nam sed numquam.
+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad
+ sequi magnam nam sed numquam.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad
+ sequi magnam nam sed numquam.
+
+ 20.02.2017 11:30
+
+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad
+ sequi magnam nam sed numquam.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad
+ sequi magnam nam sed numquam.
+
+
+
+
-
- Advanced Timeline - Horizontal With Groups and Diverse Components
-
-
-
-
- Discussion about upcoming projects
- Meeting Link
-
-
- Important
- Topics: UI5 Basics, Advanced Concepts
- Workshop Details
-
-
-
- Speaker: Sarah Kerrigan
- Join Webinar
-
-
-
-
-
-
-
-
-
- Quarterly planning session
-
-
-
- Reviewing the budget for Q1
-
-
-
- Manager: John Smith
-
-
-
-
-
- High Priority
- Task progress update
-
-
- Deadline
- Upload final project documents
- Submit
-
-
-
-
-
-
-
- Review project deliverables
-
-
-
-
-
+
+ Advanced Timeline - Horizontal With Groups and Diverse Components
+
+
+
+
+ Discussion about upcoming projects
+ Meeting Link
+
+
+ Important
+ Topics: UI5 Basics, Advanced Concepts
+ Workshop Details
+
+
+
+ Speaker: Sarah Kerrigan
+ Join Webinar
+
+
+
+
+
+
+
+
+
+ Quarterly planning session
+
+
+
+ Reviewing the budget for Q1
+
+
+
+ Manager: John Smith
+
+
+
+
+
+ High Priority
+ Task progress update
+
+
+ Deadline
+ Upload final project documents
+ Submit
+
+
+
+
+
+
+
+ Review project deliverables
+
+
+
+
+
Timeline with Various Timeline Item States
@@ -313,14 +388,7 @@ Timeline with Various Timeline Item States
-
-
-
+
+