From 37fa20d181df4bef068df363321a88507c4581e2 Mon Sep 17 00:00:00 2001 From: Stoyan Date: Tue, 19 May 2026 10:28:18 +0300 Subject: [PATCH 1/3] feat(ui5-timeline): [PoC]introduce header and info-bar slots --- packages/fiori/cypress/specs/Timeline.cy.tsx | 152 +++++ packages/fiori/src/Timeline.ts | 95 ++- packages/fiori/src/TimelineTemplate.tsx | 11 +- packages/fiori/src/themes/Timeline.css | 57 +- packages/fiori/test/pages/Timeline.html | 178 ++++++ .../test/pages/TimelineHeaderInfoBar.html | 540 ++++++++++++++++++ packages/main/datetimepicker-investigation.md | 207 +++++++ .../fiori/Timeline/Timeline.mdx | 13 +- .../fiori/Timeline/WithFilter/WithFilter.md | 5 + .../fiori/Timeline/WithFilter/main.js | 88 +++ .../fiori/Timeline/WithFilter/sample.html | 52 ++ .../fiori/Timeline/WithFilter/sample.tsx | 153 +++++ 12 files changed, 1541 insertions(+), 10 deletions(-) create mode 100644 packages/fiori/test/pages/TimelineHeaderInfoBar.html create mode 100644 packages/main/datetimepicker-investigation.md create mode 100644 packages/website/docs/_samples/fiori/Timeline/WithFilter/WithFilter.md create mode 100644 packages/website/docs/_samples/fiori/Timeline/WithFilter/main.js create mode 100644 packages/website/docs/_samples/fiori/Timeline/WithFilter/sample.html create mode 100644 packages/website/docs/_samples/fiori/Timeline/WithFilter/sample.tsx diff --git a/packages/fiori/cypress/specs/Timeline.cy.tsx b/packages/fiori/cypress/specs/Timeline.cy.tsx index 0e19b814380b..2d0b536a6736 100644 --- a/packages/fiori/cypress/specs/Timeline.cy.tsx +++ b/packages/fiori/cypress/specs/Timeline.cy.tsx @@ -581,4 +581,156 @@ describe("TimelineItem iconTooltip", () => { }); }); +describe("Timeline header and info-bar slots", () => { + it("renders the header slot when content is provided", () => { + cy.mount( + +
Controls
+ +
+ ); + + cy.get("[ui5-timeline]") + .shadow() + .find(".ui5-timeline-header") + .should("exist"); + + cy.get("#header-content").should("be.visible"); + }); + + it("renders the info-bar slot when content is provided", () => { + cy.mount( + +
Active filters: 2
+ +
+ ); + + cy.get("[ui5-timeline]") + .shadow() + .find(".ui5-timeline-info-bar") + .should("exist"); + + cy.get("#info-bar-content").should("be.visible"); + }); + + it("does not render header or info-bar wrappers when slots are empty", () => { + cy.mount( + + + + ); + + cy.get("[ui5-timeline]") + .shadow() + .find(".ui5-timeline-header") + .should("not.exist"); + + cy.get("[ui5-timeline]") + .shadow() + .find(".ui5-timeline-info-bar") + .should("not.exist"); + }); + + it("renders both slots side by side when both are provided", () => { + cy.mount( + +
Search/Filter/Sort
+
Status: 3 items
+ +
+ ); + + cy.get("[ui5-timeline]") + .shadow() + .find(".ui5-timeline-header") + .should("exist"); + + cy.get("[ui5-timeline]") + .shadow() + .find(".ui5-timeline-info-bar") + .should("exist"); + + cy.get("#hdr").should("be.visible"); + cy.get("#ifb").should("be.visible"); + }); + + it("reflects stickyHeader as the [sticky-header] host attribute and applies sticky positioning", () => { + cy.mount( + +
Header
+ +
+ ); + + cy.get("[ui5-timeline]") + .should("have.attr", "sticky-header"); + + cy.get("[ui5-timeline]") + .shadow() + .find(".ui5-timeline-header") + .should("have.css", "position", "sticky"); + }); + + it("reflects stickyInfoBar as the [sticky-info-bar] host attribute and applies sticky positioning", () => { + cy.mount( + +
Info
+ +
+ ); + + cy.get("[ui5-timeline]") + .should("have.attr", "sticky-info-bar"); + + cy.get("[ui5-timeline]") + .shadow() + .find(".ui5-timeline-info-bar") + .should("have.css", "position", "sticky"); + }); + + it("does not apply sticky positioning when stickyHeader is false (default)", () => { + cy.mount( + +
Header
+ +
+ ); + + cy.get("[ui5-timeline]") + .should("not.have.attr", "sticky-header"); + + cy.get("[ui5-timeline]") + .shadow() + .find(".ui5-timeline-header") + .should("not.have.css", "position", "sticky"); + }); + + it("scrollable defaults to true and the host reflects the [scrollable] attribute", () => { + cy.mount( + + + + ); + + cy.get("[ui5-timeline]") + .should("have.attr", "scrollable"); + }); + + it("removes overflow:auto from the scroll container when scrollable is set to false", () => { + cy.mount( + + + + ); + + cy.get("[ui5-timeline]") + .should("not.have.attr", "scrollable"); + + cy.get("[ui5-timeline]") + .shadow() + .find(".ui5-timeline-scroll-container") + .should("not.have.css", "overflow-y", "auto"); + }); +}); diff --git a/packages/fiori/src/Timeline.ts b/packages/fiori/src/Timeline.ts index 77aed3ff5e68..82290010dd49 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"; @@ -70,6 +70,23 @@ 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 and Info Bar Slots + * + * The Timeline exposes two named slots above the items area: + * + * - `header` — for a controls bar (search field, filter trigger, sort toggle, etc.). + * The most common pattern is to place a `ui5-bar` containing a search input and buttons that open + * a filter dialog or toggle sort direction. The Timeline itself performs no filtering, sorting, or + * searching — the application listens for events from its own controls and reorders, hides, or + * adds items in the default slot accordingly. + * + * - `infoBar` — for a status bar that reflects the result of the controls (active filters, + * applied sort, current search query). Typically contains tokens, labels, or a `ui5-bar`. + * + * Either slot can be made sticky using `stickyHeader` and `stickyInfoBar`. Sticky behavior + * applies relative to the Timeline's internal scroll container when `scrollable` is set, and + * relative to the nearest ancestor scroll container when `scrollable={false}`. * @constructor * @extends UI5Element * @public @@ -153,6 +170,46 @@ class Timeline extends UI5Element { @property() growing: `${TimelineGrowingMode}` = "None"; + /** + * Defines whether the Timeline provides its own scroll container for the items area. + * + * When `true` (default), the Timeline scrolls internally and any sticky header or info bar + * sticks to the top of the Timeline. When `false`, the Timeline does not clip or scroll its + * content — the application is expected to provide a scroll container on an ancestor element, + * and sticky slots will stick to that ancestor instead. + * + * **Note:** When the layout is `Horizontal`, items scroll horizontally inside the Timeline + * by default. Setting `scrollable={false}` in horizontal layout means the application must + * also provide horizontal scrolling on an ancestor; otherwise items will overflow without a + * scrollbar. + * + * @default true + * @public + * @since 2.22.0 + */ + @property({ type: Boolean }) + scrollable = true; + + /** + * Defines whether the content of the `header` slot remains visible when the user scrolls the Timeline. + * + * @default false + * @public + * @since 2.22.0 + */ + @property({ type: Boolean }) + stickyHeader = false; + + /** + * Defines whether the content of the `infoBar` slot remains visible when the user scrolls the Timeline. + * + * @default false + * @public + * @since 2.22.0 + */ + @property({ type: Boolean }) + stickyInfoBar = false; + /** * Defines the active state of the `More` button. * @private @@ -167,6 +224,34 @@ class Timeline extends UI5Element { @slot({ type: HTMLElement, individualSlots: true, "default": true }) items!: DefaultSlot; + /** + * Defines the content of the Timeline's header area, displayed above the items. + * + * The most common use case is a controls bar with a search field, sort toggle, + * and a filter trigger. Typically a `ui5-bar` is placed in this slot. The Timeline + * itself does not filter, sort, or search — the application listens for events from + * its own controls and updates the items in the default slot accordingly. + * + * @public + * @since 2.22.0 + */ + @slot() + header!: Slot; + + /** + * Defines the content of the Timeline's info bar area, displayed below the header + * and above the items. + * + * Use this slot to surface state derived from the header controls — for example, + * a list of currently applied filters, the active sort direction, or the current + * search query. + * + * @public + * @since 2.22.0 + */ + @slot() + infoBar!: Slot; + @query(".ui5-timeline-end-marker") timelineEndMarker!: HTMLElement; @@ -195,6 +280,14 @@ class Timeline extends UI5Element { : Timeline.i18nBundle.getText(TIMELINE_ARIA_LABEL); } + get _hasHeader(): boolean { + return this.header.length > 0; + } + + get _hasInfoBar(): boolean { + return this.infoBar.length > 0; + } + get showBusyIndicatorOverlay() { return !this.growsWithButton && this.loading; } diff --git a/packages/fiori/src/TimelineTemplate.tsx b/packages/fiori/src/TimelineTemplate.tsx index 7818da88a07a..113fece566c6 100644 --- a/packages/fiori/src/TimelineTemplate.tsx +++ b/packages/fiori/src/TimelineTemplate.tsx @@ -21,7 +21,16 @@ export default function TimelineTemplate(this: Timeline) { class="ui5-timeline-busy-indicator" >
- + {this._hasHeader && +
+ +
+ } + {this._hasInfoBar && +
+ +
+ }
ui5-timeline +
+

Showcase — Timeline with header & infoBar slots

+

+ The Timeline exposes two named slots above the items area: header for a controls bar + (search / sort / filter triggers) and infoBar for a status bar that reflects the + applied state. Both can be made sticky via stickyHeader / stickyInfoBar. + The Timeline itself does not filter, sort or search — the application owns that logic. +

+ +
+
+ + + + Sort: Ascending + Filter + + + + Sort: Ascending · No filters · No search + + + + + + + + + + + +
+ +
+

Try it

+
    +
  • Type in the search field — items filter live by author or title.
  • +
  • Click Sort — items reorder by date; the info bar updates.
  • +
  • Click Filter — pick authors in the dialog and apply.
  • +
  • Scroll the Timeline — both bars stick to the top of the scroll container.
  • +
+

+ The Timeline does not implement search/sort/filter itself. Application code listens to + events from the controls and reorders, hides or adds items in the default slot. +

+
+
+ + + + Stanislava Baltova + Sarah Kerrigan + John Smith + +
+ Apply + Cancel +
+
+ + +
+

Timeline within Card Vertical

@@ -417,6 +552,49 @@

Timeline with Various Timeline Item States

+ +
+

Timeline with header and info-bar slots (sticky)

+

Demonstrates the canonical pattern: a controls bar in header with search/sort/filter triggers, and an info bar in info-bar reflecting the applied state. Scroll the Timeline to see sticky behavior.

+
+ + + + + + + + Sort: Ascending · No filters + + + + + + + + + + +
+
+ +
+

Timeline with slots, scrollable=false (page-level scrolling)

+

When scrollable is set to false, the Timeline does not clip or scroll its content. Use this when an ancestor (e.g. a DynamicPage) owns scrolling.

+
+ + + Header sticks to the ancestor's scroll + + + + + + + + +
+
diff --git a/packages/fiori/test/pages/TimelineHeaderInfoBar.html b/packages/fiori/test/pages/TimelineHeaderInfoBar.html new file mode 100644 index 000000000000..60ff0e16b1f2 --- /dev/null +++ b/packages/fiori/test/pages/TimelineHeaderInfoBar.html @@ -0,0 +1,540 @@ + + + + + + Timeline — header & infoBar slots showcase + + + + + + + + + +
+ + +
+
+

1. Full filter pipeline (search + sort + multi-criteria filter)

+

Activity log with type, priority, status and author. Header sticks to the top; info bar surfaces every applied criterion as removable tokens.

+
+
+ + + + Sort: Newest + Filter + + + + +
+ Showing 0 of 0 +
+
+ + + Initial alignment with all stakeholders. + Visual design walk-through and feedback. + Backlog refinement and capacity planning. + Authentication endpoint signature changed. + Re-prioritised the next two sprints. + v1.2.4 hotfix to production. + Demo of new dashboard. + What went well, what to improve. + Search latency up by 40%. + Scope and timing for the next release. + Reviewing PR #482. + v1.3.0 rollout. + Roundtable with three pilot customers. + CVE-2017-XXXX mitigation. +
+
+
+ + +
+
+

2. Minimal — search only with live count

+

Smallest meaningful use of the slots: a search input in header, a live count in infoBar. Sticky header only.

+
+
+ + + + + + 8 of 8 activities + + + + + + + + + + +
+
+ + +
+
+

3. scrollable=false + ancestor scroll container

+

The ancestor (dashed border) owns scrolling. The Timeline's sticky header sticks to the ancestor — common when a Timeline is embedded in a DynamicPage.

+
+
+ + + Header sticks to the ancestor + Add row + + + 6 rows + + + + + + + + +
+
+ +
+ + + +
+ +
+ Type + + Meeting + Review + Deploy + Incident + +
+ +
+ Status + + Open + In Progress + Done + +
+ +
+ Author + + Stanislava Baltova + Sarah Kerrigan + John Smith + +
+ +
+ Priority + + Any + Low + Medium + High + +
+ +
+ Date range + +
+
+ +
+ Apply + Clear all + Cancel +
+
+ + + + + + diff --git a/packages/main/datetimepicker-investigation.md b/packages/main/datetimepicker-investigation.md new file mode 100644 index 000000000000..d5441046e0dc --- /dev/null +++ b/packages/main/datetimepicker-investigation.md @@ -0,0 +1,207 @@ +# DateTimePicker Investigation: Value Revert on Manual Edit + +## TL;DR — Is This a Bug or Misusage? + +**It's primarily a misusage, but with a subtle trap in the API.** + +The reporter's issue has **two root causes**: + +1. **The `'Z'` in `valueFormat` is a literal, not a timezone indicator** — this means dates get parsed in local time, not UTC, causing a time offset that makes values appear "wrong" after round-tripping. +2. **React state updates trigger re-render, which triggers `onBeforeRendering` normalization** — when the parent component re-renders (e.g., after the Select changes), the DateTimePicker's `value` prop is re-set from React state, which triggers `onBeforeRendering()` → `normalizeFormattedValue()`, overwriting whatever the user was typing. + +--- + +## The Reporter's Setup + +```jsx + +``` + +--- + +## Issue 1: `'Z'` Is Treated as a Literal Character, Not UTC + +### The Pattern +The reporter uses: `valueFormat="yyyy-MM-dd'T'HH:mm:ss'Z'"` + +In LDML/CLDR pattern syntax, text inside single quotes is **literal**. So `'Z'` means "output the letter Z" — it does NOT mean "this is a UTC timestamp." + +### What Happens Internally +1. `"2025-05-19T12:00:00Z"` is parsed by the DateFormat engine +2. The `T` matches the literal `'T'`, the `Z` matches the literal `'Z'` +3. Since no timezone pattern symbol (`X`, `Z`, `z`, `V`) is present, `tzDiff` remains undefined +4. The engine interprets the time as **local time**, not UTC +5. On re-formatting, it outputs the same string, but the underlying Date object is offset + +### Impact +When the component round-trips the value (parse → Date → format), the time may shift by the user's timezone offset. For a user in UTC+2, "12:00:00" becomes "14:00:00" (or vice versa), making it look like the value changed after round-tripping. + +### Fix for the Reporter +If they want true UTC handling, they should use **`X`** instead of `'Z'`: +``` +valueFormat="yyyy-MM-dd'T'HH:mm:ssX" +``` +The `X` pattern outputs `Z` for UTC and `+0200` for other offsets, and **parses `Z` as UTC+0**. + +--- + +## Issue 2: React Re-Render Triggers Value Normalization + +### The Mechanism + +The DatePicker has this in `onBeforeRendering()` (called before every render): + +```typescript +onBeforeRendering() { + if (!this.isLiveUpdate) { + this.value = this.normalizeFormattedValue(this.value) || this.value; + } + this.liveValue = this.value; +} +``` + +### The `isLiveUpdate` Protection + +During live typing: +- `_onInputInput()` calls `_updateValueAndFireEvents(value, false, ["input"], false)` +- `updateValue = false` → sets `isLiveUpdate = true` +- `onBeforeRendering` sees `isLiveUpdate = true` → **skips normalization** +- `displayValue` getter returns `liveValue` (raw user input) — user sees what they type + +This works correctly **within the web component's own lifecycle**. + +### The Problem with React Controlled Components + +Here's where it breaks: + +1. User selects "7 days" from Select → React state `expiration` = `"2025-05-26T12:00:00Z"` +2. React passes `value="2025-05-26T12:00:00Z"` to `` +3. DateTimePicker's `value` property is set → triggers `onBeforeRendering` +4. `isLiveUpdate` is `false` at this point → value gets normalized +5. User switches to "Specific date" and starts typing +6. **Here's the trap**: If `handleDateChange` fires (the `onChange`) and calls `setExpiration(event.target.value)`, React re-renders, setting `value={expiration}` again +7. The re-render from React resets the property, which fires `onBeforeRendering` again +8. If the user's partially-typed value doesn't parse cleanly via valueFormat, the normalization either returns `""` (failed parse) or reformats it differently + +### The Core Issue + +When using React (or any framework) with **controlled component pattern** (`value={state}`), every re-render **resets the web component's value property from the framework state**. This conflicts with the web component's internal `isLiveUpdate` / `liveValue` mechanism which assumes **it** owns the value during typing. + +The web component's input handling works like this: +- User types → `_onInputInput` → sets `isLiveUpdate = true`, stores `liveValue` +- **But** if React re-renders between keystrokes, it sets `this.value = expiration` (from React state) +- This triggers `onBeforeRendering`, and since `isLiveUpdate` may have been reset by the property change, normalization kicks in + +--- + +## Issue 3: `_checkValueValidity` Validates Against `valueFormat` + +In `_updateValueAndFireEvents`: +```typescript +const valid = this._checkValueValidity(value); // Uses valueFormat! +``` + +But the user types in **display format** (`"dd MMM yyyy, HH:mm:ss 'UTC'"`). The validity check uses `isValidValue()` which parses against `getValueFormat()`. So: +- User types `"19 May 2025, 14:30:00 UTC"` (display format) +- `_checkValueValidity` tries to parse this against `"yyyy-MM-dd'T'HH:mm:ss'Z'"` (value format) +- **Parse fails** → `valid = false` + +When `valid = false` AND `normalizeValue = false` (input event) AND `isLiveUpdate = true`: +- The condition `(valid && normalizeValue) || !this.isLiveUpdate` is `(false && false) || false` = `false` +- So the display value passes through unchanged — **this is actually fine during typing** + +When `valid = false` AND `normalizeValue = true` (change event, blur/Enter) AND `isLiveUpdate = false`: +- The condition is `(false && true) || !false` = `true` +- So it tries `getDisplayValueFromValue(value)` on the user's display-format string +- `getDisplayValueFromValue` first tries `getValueFormat().parse(value)` — this fails (it's display format, not value format) +- When parse fails, it returns the value **unchanged** +- Then `normalizeDisplayValue(value)` is called — this parses with displayFormat and reformats → works correctly + +**So the validation path for direct user input is actually correct** — the problem is only when React resets the value. + +--- + +## Summary of Root Causes + +| # | Issue | Type | Severity | +|---|-------|------|----------| +| 1 | `'Z'` in valueFormat is literal, not UTC | Misusage | High — causes time offset | +| 2 | React controlled pattern conflicts with `onBeforeRendering` normalization | Framework interaction | High — causes input revert | +| 3 | `_checkValueValidity` uses valueFormat for display input | Minor design quirk | Low — doesn't cause the reported issue directly | + +--- + +## Recommendations for the Reporter + +### Fix 1: Use `X` Instead of `'Z'` for UTC + +```jsx +valueFormat="yyyy-MM-dd'T'HH:mm:ssX" +``` + +This makes the `Z` in `"2025-05-19T12:00:00Z"` semantically meaningful (UTC timezone indicator). + +### Fix 2: Don't Use Controlled Pattern During Editing + +Instead of always passing `value={expiration}`, only set it programmatically: + +```jsx + +``` + +Or use a ref-based approach to set the value only when the mode changes: + +```jsx +const datePickerRef = useRef(null); + +useEffect(() => { + if (expirationMode !== "Specific date" && datePickerRef.current) { + datePickerRef.current.value = expiration; + } +}, [expiration, expirationMode]); +``` + +### Fix 3: Use `onInput` Instead of `onChange` for Live Updates + +The `onChange` fires on blur/Enter. If they need live tracking, use the `input` event. But more importantly, avoid React re-rendering the DateTimePicker while the user is typing. + +### Fix 4 (Simplest): Remove valueFormat, Use ISO Directly + +If they don't need the custom valueFormat: +```jsx + +``` + +Then convert between their moment ISO format and the component's internal format in the event handler. + +--- + +## Is There a Component Bug? + +**Arguably yes, but it's a known limitation of web components + React controlled patterns:** + +The `onBeforeRendering` normalization doesn't account for the case where a framework externally sets `value` while the user is actively editing. The `isLiveUpdate` flag is an internal mechanism that gets bypassed when the property is set from outside. + +This is a **general web-component-in-React problem**, not specific to DateTimePicker. The React wrapper could potentially debounce property updates or skip setting `value` when the input is focused, but that's a `@ui5/webcomponents-react` concern, not a `@ui5/webcomponents` bug. + +--- + +## Files Examined + +- `packages/main/src/DateTimePicker.ts` — lines 246-253 (`_formatPattern`), 468-523 (format methods) +- `packages/main/src/DatePicker.ts` — lines 474-487 (`onBeforeRendering`), 582-615 (`_updateValueAndFireEvents`), 630-644 (format conversion), 668-670 (`_onInputInput`), 821-843 (normalization), 875-889 (`displayValue`) +- `packages/main/src/DateComponentBase.ts` — lines 56-80 (properties), 157-171 (format getters), 240-304 (format instances) +- `packages/localization/dist/sap/ui/core/format/DateFormat.js` — pattern parsing, `X` vs `'Z'` symbol handling diff --git a/packages/website/docs/_components_pages/fiori/Timeline/Timeline.mdx b/packages/website/docs/_components_pages/fiori/Timeline/Timeline.mdx index b9f6544b7bbb..bfcff161e6cb 100644 --- a/packages/website/docs/_components_pages/fiori/Timeline/Timeline.mdx +++ b/packages/website/docs/_components_pages/fiori/Timeline/Timeline.mdx @@ -4,6 +4,7 @@ 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 WithFilter from "../../../_samples/fiori/Timeline/WithFilter/WithFilter.md"; <%COMPONENT_OVERVIEW%> @@ -31,4 +32,14 @@ import WithGrowing from "../../../_samples/fiori/Timeline/WithGrowing/WithGrowin ### Timeline with Growing - \ No newline at end of file + + +### Timeline with Header and Info Bar + +The `header` slot accepts any content — typically a `ui5-bar` containing a search field and triggers +for sort and filter dialogs. The `infoBar` slot reflects the resulting state (active filters, +sort direction, current search query). Both slots can be made sticky via the `stickyHeader` and +`stickyInfoBar` properties. The Timeline itself does not filter, sort, or search — the +application owns that logic. + + \ No newline at end of file diff --git a/packages/website/docs/_samples/fiori/Timeline/WithFilter/WithFilter.md b/packages/website/docs/_samples/fiori/Timeline/WithFilter/WithFilter.md new file mode 100644 index 000000000000..0c062a836e84 --- /dev/null +++ b/packages/website/docs/_samples/fiori/Timeline/WithFilter/WithFilter.md @@ -0,0 +1,5 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; +import react from '!!raw-loader!./sample.tsx'; + + diff --git a/packages/website/docs/_samples/fiori/Timeline/WithFilter/main.js b/packages/website/docs/_samples/fiori/Timeline/WithFilter/main.js new file mode 100644 index 000000000000..68373cdce3ef --- /dev/null +++ b/packages/website/docs/_samples/fiori/Timeline/WithFilter/main.js @@ -0,0 +1,88 @@ +import "@ui5/webcomponents/dist/Bar.js"; +import "@ui5/webcomponents/dist/Button.js"; +import "@ui5/webcomponents/dist/Dialog.js"; +import "@ui5/webcomponents/dist/Input.js"; +import "@ui5/webcomponents/dist/Label.js"; +import "@ui5/webcomponents/dist/List.js"; +import "@ui5/webcomponents/dist/ListItemStandard.js"; + +import "@ui5/webcomponents-fiori/dist/Timeline.js"; +import "@ui5/webcomponents-fiori/dist/TimelineItem.js"; + +import "@ui5/webcomponents-icons/dist/calendar.js"; +import "@ui5/webcomponents-icons/dist/sort.js"; +import "@ui5/webcomponents-icons/dist/filter.js"; + +const timeline = document.getElementById("filterableTimeline"); +const searchInput = document.getElementById("searchInput"); +const sortButton = document.getElementById("sortButton"); +const filterButton = document.getElementById("filterButton"); +const filterDialog = document.getElementById("filterDialog"); +const filterList = document.getElementById("filterList"); +const filterDialogApply = document.getElementById("filterDialogApply"); +const filterDialogCancel = document.getElementById("filterDialogCancel"); +const activeStateLabel = document.getElementById("activeStateLabel"); + +const allEntries = Array.from(timeline.querySelectorAll("[ui5-timeline-item]")); +let isAscending = true; +let activeAuthors = []; +let searchQuery = ""; + +function applyFilters() { + const filtered = allEntries.filter(item => { + const author = item.getAttribute("data-author") || ""; + const title = item.getAttribute("title-text") || ""; + + const matchesAuthor = activeAuthors.length === 0 || activeAuthors.includes(author); + const matchesSearch = searchQuery === "" + || author.toLowerCase().includes(searchQuery) + || title.toLowerCase().includes(searchQuery); + + return matchesAuthor && matchesSearch; + }); + + const sorted = filtered.slice().sort((firstItem, secondItem) => { + const firstSubtitle = firstItem.getAttribute("subtitle-text") || ""; + const secondSubtitle = secondItem.getAttribute("subtitle-text") || ""; + return isAscending + ? firstSubtitle.localeCompare(secondSubtitle) + : secondSubtitle.localeCompare(firstSubtitle); + }); + + allEntries.forEach(item => { + if (item.parentElement === timeline) { + timeline.removeChild(item); + } + }); + sorted.forEach(item => timeline.appendChild(item)); + + const sortLabel = `Sort: ${isAscending ? "Ascending" : "Descending"}`; + const filterLabel = activeAuthors.length === 0 ? "No filters" : `Filter: ${activeAuthors.join(", ")}`; + const searchLabel = searchQuery === "" ? "No search" : `Search: "${searchQuery}"`; + activeStateLabel.textContent = `${sortLabel} · ${filterLabel} · ${searchLabel}`; +} + +searchInput.addEventListener("input", event => { + searchQuery = event.target.value.trim().toLowerCase(); + applyFilters(); +}); + +sortButton.addEventListener("click", () => { + isAscending = !isAscending; + sortButton.textContent = `Sort: ${isAscending ? "Ascending" : "Descending"}`; + applyFilters(); +}); + +filterButton.addEventListener("click", () => { + filterDialog.open = true; +}); + +filterDialogApply.addEventListener("click", () => { + activeAuthors = filterList.getSelectedItems().map(item => item.textContent.trim()); + filterDialog.open = false; + applyFilters(); +}); + +filterDialogCancel.addEventListener("click", () => { + filterDialog.open = false; +}); diff --git a/packages/website/docs/_samples/fiori/Timeline/WithFilter/sample.html b/packages/website/docs/_samples/fiori/Timeline/WithFilter/sample.html new file mode 100644 index 000000000000..04bcf45cdc38 --- /dev/null +++ b/packages/website/docs/_samples/fiori/Timeline/WithFilter/sample.html @@ -0,0 +1,52 @@ + + + + + + + + Sample + + + + + + + + + Sort: Ascending + Filter + + + + Sort: Ascending · No filters · No search + + + + + + + + + + + + + + + Stanislava Baltova + Sarah Kerrigan + John Smith + +
+ Apply + Cancel +
+
+ + + + + + + diff --git a/packages/website/docs/_samples/fiori/Timeline/WithFilter/sample.tsx b/packages/website/docs/_samples/fiori/Timeline/WithFilter/sample.tsx new file mode 100644 index 000000000000..a6c798ba05fe --- /dev/null +++ b/packages/website/docs/_samples/fiori/Timeline/WithFilter/sample.tsx @@ -0,0 +1,153 @@ +import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js"; +import { useMemo, useState } from "react"; + +import TimelineClass from "@ui5/webcomponents-fiori/dist/Timeline.js"; +import TimelineItemClass from "@ui5/webcomponents-fiori/dist/TimelineItem.js"; +import BarClass from "@ui5/webcomponents/dist/Bar.js"; +import ButtonClass from "@ui5/webcomponents/dist/Button.js"; +import DialogClass from "@ui5/webcomponents/dist/Dialog.js"; +import InputClass from "@ui5/webcomponents/dist/Input.js"; +import LabelClass from "@ui5/webcomponents/dist/Label.js"; +import ListClass from "@ui5/webcomponents/dist/List.js"; +import ListItemStandardClass from "@ui5/webcomponents/dist/ListItemStandard.js"; + +import "@ui5/webcomponents-icons/dist/calendar.js"; +import "@ui5/webcomponents-icons/dist/sort.js"; +import "@ui5/webcomponents-icons/dist/filter.js"; + +const Timeline = createReactComponent(TimelineClass); +const TimelineItem = createReactComponent(TimelineItemClass); +const Bar = createReactComponent(BarClass); +const Button = createReactComponent(ButtonClass); +const Dialog = createReactComponent(DialogClass); +const Input = createReactComponent(InputClass); +const Label = createReactComponent(LabelClass); +const List = createReactComponent(ListClass); +const ListItemStandard = createReactComponent(ListItemStandardClass); + +type Entry = { + id: string; + titleText: string; + subtitleText: string; + author: string; +}; + +const ENTRIES: Entry[] = [ + { id: "1", titleText: "Project kickoff", subtitleText: "20.02.2017 09:00", author: "Stanislava Baltova" }, + { id: "2", titleText: "Design review", subtitleText: "22.02.2017 11:30", author: "Sarah Kerrigan" }, + { id: "3", titleText: "Sprint planning", subtitleText: "24.02.2017 14:00", author: "John Smith" }, + { id: "4", titleText: "Backlog grooming", subtitleText: "26.02.2017 10:00", author: "Stanislava Baltova" }, + { id: "5", titleText: "Demo to stakeholders", subtitleText: "28.02.2017 15:00", author: "Sarah Kerrigan" }, + { id: "6", titleText: "Retrospective", subtitleText: "01.03.2017 16:00", author: "John Smith" }, + { id: "7", titleText: "Release planning", subtitleText: "03.03.2017 09:30", author: "Stanislava Baltova" }, + { id: "8", titleText: "Deployment", subtitleText: "05.03.2017 12:00", author: "Sarah Kerrigan" }, +]; + +const AUTHORS = ["Stanislava Baltova", "Sarah Kerrigan", "John Smith"]; + +function App() { + const [isAscending, setIsAscending] = useState(true); + const [activeAuthors, setActiveAuthors] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [pendingAuthors, setPendingAuthors] = useState([]); + const [isFilterDialogOpen, setIsFilterDialogOpen] = useState(false); + + const visibleEntries = useMemo(() => { + const query = searchQuery.trim().toLowerCase(); + const filtered = ENTRIES.filter(entry => { + const matchesAuthor = activeAuthors.length === 0 || activeAuthors.includes(entry.author); + const matchesSearch = query === "" + || entry.author.toLowerCase().includes(query) + || entry.titleText.toLowerCase().includes(query); + return matchesAuthor && matchesSearch; + }); + + return filtered.slice().sort((firstEntry, secondEntry) => { + return isAscending + ? firstEntry.subtitleText.localeCompare(secondEntry.subtitleText) + : secondEntry.subtitleText.localeCompare(firstEntry.subtitleText); + }); + }, [activeAuthors, searchQuery, isAscending]); + + const sortLabel = `Sort: ${isAscending ? "Ascending" : "Descending"}`; + const filterLabel = activeAuthors.length === 0 ? "No filters" : `Filter: ${activeAuthors.join(", ")}`; + const searchLabel = searchQuery.trim() === "" ? "No search" : `Search: "${searchQuery.trim()}"`; + + const openFilterDialog = () => { + setPendingAuthors(activeAuthors); + setIsFilterDialogOpen(true); + }; + + return ( + <> + + + setSearchQuery((event.target as HTMLInputElement).value)} + /> + + + + + + + + + {visibleEntries.map(entry => ( + + ))} + + + setIsFilterDialogOpen(false)} + > + + {AUTHORS.map(author => ( + { + setPendingAuthors(prev => + prev.includes(author) + ? prev.filter(name => name !== author) + : [...prev, author] + ); + }} + > + {author} + + ))} + +
+ + +
+
+ + ); +} + +export default App; From dac1fbb9b1a84f8ddce26ccaa27c04d62d257831 Mon Sep 17 00:00:00 2001 From: Stoyan Date: Tue, 19 May 2026 15:22:39 +0300 Subject: [PATCH 2/3] change property name for scroll, as property should have default false value for boolean --- packages/fiori/cypress/specs/Timeline.cy.tsx | 10 +++++----- packages/fiori/src/Timeline.ts | 20 +++++++++---------- packages/fiori/src/themes/Timeline.css | 8 ++++---- packages/fiori/test/pages/Timeline.html | 6 +++--- .../test/pages/TimelineHeaderInfoBar.html | 6 +++--- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/fiori/cypress/specs/Timeline.cy.tsx b/packages/fiori/cypress/specs/Timeline.cy.tsx index 2d0b536a6736..e72ae273b65f 100644 --- a/packages/fiori/cypress/specs/Timeline.cy.tsx +++ b/packages/fiori/cypress/specs/Timeline.cy.tsx @@ -706,7 +706,7 @@ describe("Timeline header and info-bar slots", () => { .should("not.have.css", "position", "sticky"); }); - it("scrollable defaults to true and the host reflects the [scrollable] attribute", () => { + it("noScrollContainer defaults to false and the host has no [no-scroll-container] attribute", () => { cy.mount( @@ -714,18 +714,18 @@ describe("Timeline header and info-bar slots", () => { ); cy.get("[ui5-timeline]") - .should("have.attr", "scrollable"); + .should("not.have.attr", "no-scroll-container"); }); - it("removes overflow:auto from the scroll container when scrollable is set to false", () => { + it("removes overflow:auto from the scroll container when noScrollContainer is set", () => { cy.mount( - + ); cy.get("[ui5-timeline]") - .should("not.have.attr", "scrollable"); + .should("have.attr", "no-scroll-container"); cy.get("[ui5-timeline]") .shadow() diff --git a/packages/fiori/src/Timeline.ts b/packages/fiori/src/Timeline.ts index 82290010dd49..a5c0a5f5e61a 100644 --- a/packages/fiori/src/Timeline.ts +++ b/packages/fiori/src/Timeline.ts @@ -85,8 +85,8 @@ const GROWING_WITH_SCROLL_DEBOUNCE_RATE = 250; // ms * applied sort, current search query). Typically contains tokens, labels, or a `ui5-bar`. * * Either slot can be made sticky using `stickyHeader` and `stickyInfoBar`. Sticky behavior - * applies relative to the Timeline's internal scroll container when `scrollable` is set, and - * relative to the nearest ancestor scroll container when `scrollable={false}`. + * applies relative to the Timeline's internal scroll container by default, and relative + * to the nearest ancestor scroll container when `noScrollContainer` is set. * @constructor * @extends UI5Element * @public @@ -171,24 +171,24 @@ class Timeline extends UI5Element { growing: `${TimelineGrowingMode}` = "None"; /** - * Defines whether the Timeline provides its own scroll container for the items area. + * Defines whether the Timeline relinquishes its internal scroll container. * - * When `true` (default), the Timeline scrolls internally and any sticky header or info bar - * sticks to the top of the Timeline. When `false`, the Timeline does not clip or scroll its - * content — the application is expected to provide a scroll container on an ancestor element, - * and sticky slots will stick to that ancestor instead. + * By default the Timeline scrolls internally; sticky header and info bar stick to the + * top of the Timeline. When set to `true`, the Timeline does not clip or scroll its + * content — the application is expected to provide a scroll container on an ancestor + * element, and sticky slots will stick to that ancestor instead. * * **Note:** When the layout is `Horizontal`, items scroll horizontally inside the Timeline - * by default. Setting `scrollable={false}` in horizontal layout means the application must + * by default. Setting `noScrollContainer` in horizontal layout means the application must * also provide horizontal scrolling on an ancestor; otherwise items will overflow without a * scrollbar. * - * @default true + * @default false * @public * @since 2.22.0 */ @property({ type: Boolean }) - scrollable = true; + noScrollContainer = false; /** * Defines whether the content of the `header` slot remains visible when the user scrolls the Timeline. diff --git a/packages/fiori/src/themes/Timeline.css b/packages/fiori/src/themes/Timeline.css index 365211f744cf..eda0568b745c 100644 --- a/packages/fiori/src/themes/Timeline.css +++ b/packages/fiori/src/themes/Timeline.css @@ -59,20 +59,20 @@ margin-inline-start: var(--_ui5_tl_li_margin_bottom); } -/* Scroll container — opt-in via [scrollable] host attribute (default) */ -:host([scrollable][layout="Horizontal"]) .ui5-timeline-scroll-container { +/* Scroll container — Timeline owns scrolling by default; opt out with [no-scroll-container] */ +:host(:not([no-scroll-container])[layout="Horizontal"]) .ui5-timeline-scroll-container { 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([scrollable][layout="Vertical"]) .ui5-timeline-scroll-container { +:host(:not([no-scroll-container])[layout="Vertical"]) .ui5-timeline-scroll-container { height: 100%; width: 100%; overflow: auto; } -/* When scrollable is off, the scroll container is a transparent flex layer: +/* When [no-scroll-container] is set, the scroll container is a transparent flex layer: no overflow, no clipping. The application owns scrolling on an ancestor. */ .ui5-timeline-scroll-container { display: flex; diff --git a/packages/fiori/test/pages/Timeline.html b/packages/fiori/test/pages/Timeline.html index 99c870bc86c9..bb127a0c87fa 100644 --- a/packages/fiori/test/pages/Timeline.html +++ b/packages/fiori/test/pages/Timeline.html @@ -579,10 +579,10 @@

Timeline with header and info-bar slots (sticky)

-

Timeline with slots, scrollable=false (page-level scrolling)

-

When scrollable is set to false, the Timeline does not clip or scroll its content. Use this when an ancestor (e.g. a DynamicPage) owns scrolling.

+

Timeline with slots, no internal scroll container (page-level scrolling)

+

When noScrollContainer is set, the Timeline does not clip or scroll its content. Use this when an ancestor (e.g. a DynamicPage) owns scrolling.

- + Header sticks to the ancestor's scroll diff --git a/packages/fiori/test/pages/TimelineHeaderInfoBar.html b/packages/fiori/test/pages/TimelineHeaderInfoBar.html index 60ff0e16b1f2..05ec39ddd61e 100644 --- a/packages/fiori/test/pages/TimelineHeaderInfoBar.html +++ b/packages/fiori/test/pages/TimelineHeaderInfoBar.html @@ -195,14 +195,14 @@

2. Minimal — search only with live count

- +
-

3. scrollable=false + ancestor scroll container

+

3. noScrollContainer + ancestor scroll container

The ancestor (dashed border) owns scrolling. The Timeline's sticky header sticks to the ancestor — common when a Timeline is embedded in a DynamicPage.

- + Header sticks to the ancestor Add row From 57d7a0e3a6fec7646a8a01b61673095885c93ad5 Mon Sep 17 00:00:00 2001 From: Stoyan <88034608+hinzzx@users.noreply.github.com> Date: Thu, 21 May 2026 11:03:48 +0300 Subject: [PATCH 3/3] Delete file, added by mistake. --- packages/main/datetimepicker-investigation.md | 207 ------------------ 1 file changed, 207 deletions(-) delete mode 100644 packages/main/datetimepicker-investigation.md diff --git a/packages/main/datetimepicker-investigation.md b/packages/main/datetimepicker-investigation.md deleted file mode 100644 index d5441046e0dc..000000000000 --- a/packages/main/datetimepicker-investigation.md +++ /dev/null @@ -1,207 +0,0 @@ -# DateTimePicker Investigation: Value Revert on Manual Edit - -## TL;DR — Is This a Bug or Misusage? - -**It's primarily a misusage, but with a subtle trap in the API.** - -The reporter's issue has **two root causes**: - -1. **The `'Z'` in `valueFormat` is a literal, not a timezone indicator** — this means dates get parsed in local time, not UTC, causing a time offset that makes values appear "wrong" after round-tripping. -2. **React state updates trigger re-render, which triggers `onBeforeRendering` normalization** — when the parent component re-renders (e.g., after the Select changes), the DateTimePicker's `value` prop is re-set from React state, which triggers `onBeforeRendering()` → `normalizeFormattedValue()`, overwriting whatever the user was typing. - ---- - -## The Reporter's Setup - -```jsx - -``` - ---- - -## Issue 1: `'Z'` Is Treated as a Literal Character, Not UTC - -### The Pattern -The reporter uses: `valueFormat="yyyy-MM-dd'T'HH:mm:ss'Z'"` - -In LDML/CLDR pattern syntax, text inside single quotes is **literal**. So `'Z'` means "output the letter Z" — it does NOT mean "this is a UTC timestamp." - -### What Happens Internally -1. `"2025-05-19T12:00:00Z"` is parsed by the DateFormat engine -2. The `T` matches the literal `'T'`, the `Z` matches the literal `'Z'` -3. Since no timezone pattern symbol (`X`, `Z`, `z`, `V`) is present, `tzDiff` remains undefined -4. The engine interprets the time as **local time**, not UTC -5. On re-formatting, it outputs the same string, but the underlying Date object is offset - -### Impact -When the component round-trips the value (parse → Date → format), the time may shift by the user's timezone offset. For a user in UTC+2, "12:00:00" becomes "14:00:00" (or vice versa), making it look like the value changed after round-tripping. - -### Fix for the Reporter -If they want true UTC handling, they should use **`X`** instead of `'Z'`: -``` -valueFormat="yyyy-MM-dd'T'HH:mm:ssX" -``` -The `X` pattern outputs `Z` for UTC and `+0200` for other offsets, and **parses `Z` as UTC+0**. - ---- - -## Issue 2: React Re-Render Triggers Value Normalization - -### The Mechanism - -The DatePicker has this in `onBeforeRendering()` (called before every render): - -```typescript -onBeforeRendering() { - if (!this.isLiveUpdate) { - this.value = this.normalizeFormattedValue(this.value) || this.value; - } - this.liveValue = this.value; -} -``` - -### The `isLiveUpdate` Protection - -During live typing: -- `_onInputInput()` calls `_updateValueAndFireEvents(value, false, ["input"], false)` -- `updateValue = false` → sets `isLiveUpdate = true` -- `onBeforeRendering` sees `isLiveUpdate = true` → **skips normalization** -- `displayValue` getter returns `liveValue` (raw user input) — user sees what they type - -This works correctly **within the web component's own lifecycle**. - -### The Problem with React Controlled Components - -Here's where it breaks: - -1. User selects "7 days" from Select → React state `expiration` = `"2025-05-26T12:00:00Z"` -2. React passes `value="2025-05-26T12:00:00Z"` to `` -3. DateTimePicker's `value` property is set → triggers `onBeforeRendering` -4. `isLiveUpdate` is `false` at this point → value gets normalized -5. User switches to "Specific date" and starts typing -6. **Here's the trap**: If `handleDateChange` fires (the `onChange`) and calls `setExpiration(event.target.value)`, React re-renders, setting `value={expiration}` again -7. The re-render from React resets the property, which fires `onBeforeRendering` again -8. If the user's partially-typed value doesn't parse cleanly via valueFormat, the normalization either returns `""` (failed parse) or reformats it differently - -### The Core Issue - -When using React (or any framework) with **controlled component pattern** (`value={state}`), every re-render **resets the web component's value property from the framework state**. This conflicts with the web component's internal `isLiveUpdate` / `liveValue` mechanism which assumes **it** owns the value during typing. - -The web component's input handling works like this: -- User types → `_onInputInput` → sets `isLiveUpdate = true`, stores `liveValue` -- **But** if React re-renders between keystrokes, it sets `this.value = expiration` (from React state) -- This triggers `onBeforeRendering`, and since `isLiveUpdate` may have been reset by the property change, normalization kicks in - ---- - -## Issue 3: `_checkValueValidity` Validates Against `valueFormat` - -In `_updateValueAndFireEvents`: -```typescript -const valid = this._checkValueValidity(value); // Uses valueFormat! -``` - -But the user types in **display format** (`"dd MMM yyyy, HH:mm:ss 'UTC'"`). The validity check uses `isValidValue()` which parses against `getValueFormat()`. So: -- User types `"19 May 2025, 14:30:00 UTC"` (display format) -- `_checkValueValidity` tries to parse this against `"yyyy-MM-dd'T'HH:mm:ss'Z'"` (value format) -- **Parse fails** → `valid = false` - -When `valid = false` AND `normalizeValue = false` (input event) AND `isLiveUpdate = true`: -- The condition `(valid && normalizeValue) || !this.isLiveUpdate` is `(false && false) || false` = `false` -- So the display value passes through unchanged — **this is actually fine during typing** - -When `valid = false` AND `normalizeValue = true` (change event, blur/Enter) AND `isLiveUpdate = false`: -- The condition is `(false && true) || !false` = `true` -- So it tries `getDisplayValueFromValue(value)` on the user's display-format string -- `getDisplayValueFromValue` first tries `getValueFormat().parse(value)` — this fails (it's display format, not value format) -- When parse fails, it returns the value **unchanged** -- Then `normalizeDisplayValue(value)` is called — this parses with displayFormat and reformats → works correctly - -**So the validation path for direct user input is actually correct** — the problem is only when React resets the value. - ---- - -## Summary of Root Causes - -| # | Issue | Type | Severity | -|---|-------|------|----------| -| 1 | `'Z'` in valueFormat is literal, not UTC | Misusage | High — causes time offset | -| 2 | React controlled pattern conflicts with `onBeforeRendering` normalization | Framework interaction | High — causes input revert | -| 3 | `_checkValueValidity` uses valueFormat for display input | Minor design quirk | Low — doesn't cause the reported issue directly | - ---- - -## Recommendations for the Reporter - -### Fix 1: Use `X` Instead of `'Z'` for UTC - -```jsx -valueFormat="yyyy-MM-dd'T'HH:mm:ssX" -``` - -This makes the `Z` in `"2025-05-19T12:00:00Z"` semantically meaningful (UTC timezone indicator). - -### Fix 2: Don't Use Controlled Pattern During Editing - -Instead of always passing `value={expiration}`, only set it programmatically: - -```jsx - -``` - -Or use a ref-based approach to set the value only when the mode changes: - -```jsx -const datePickerRef = useRef(null); - -useEffect(() => { - if (expirationMode !== "Specific date" && datePickerRef.current) { - datePickerRef.current.value = expiration; - } -}, [expiration, expirationMode]); -``` - -### Fix 3: Use `onInput` Instead of `onChange` for Live Updates - -The `onChange` fires on blur/Enter. If they need live tracking, use the `input` event. But more importantly, avoid React re-rendering the DateTimePicker while the user is typing. - -### Fix 4 (Simplest): Remove valueFormat, Use ISO Directly - -If they don't need the custom valueFormat: -```jsx - -``` - -Then convert between their moment ISO format and the component's internal format in the event handler. - ---- - -## Is There a Component Bug? - -**Arguably yes, but it's a known limitation of web components + React controlled patterns:** - -The `onBeforeRendering` normalization doesn't account for the case where a framework externally sets `value` while the user is actively editing. The `isLiveUpdate` flag is an internal mechanism that gets bypassed when the property is set from outside. - -This is a **general web-component-in-React problem**, not specific to DateTimePicker. The React wrapper could potentially debounce property updates or skip setting `value` when the input is focused, but that's a `@ui5/webcomponents-react` concern, not a `@ui5/webcomponents` bug. - ---- - -## Files Examined - -- `packages/main/src/DateTimePicker.ts` — lines 246-253 (`_formatPattern`), 468-523 (format methods) -- `packages/main/src/DatePicker.ts` — lines 474-487 (`onBeforeRendering`), 582-615 (`_updateValueAndFireEvents`), 630-644 (format conversion), 668-670 (`_onInputInput`), 821-843 (normalization), 875-889 (`displayValue`) -- `packages/main/src/DateComponentBase.ts` — lines 56-80 (properties), 157-171 (format getters), 240-304 (format instances) -- `packages/localization/dist/sap/ui/core/format/DateFormat.js` — pattern parsing, `X` vs `'Z'` symbol handling