Skip to content

feat(ui5-dynamic-date-range): introduce last/next X options #11621

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Aug 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6c89dd7
feat(ui5-dynamic-date-range): add new options
tsanislavgatev May 29, 2025
2b8de12
Merge branch 'main' into ddr-next-last-options
hinzzx Jun 1, 2025
6341bec
feat(ui5-dynamic-date-range): introduce last/next options
hinzzx Jun 9, 2025
80d8440
Merge branch 'main' into ddr-next-last-options
hinzzx Jun 10, 2025
0e21ded
fix: address conflicts
hinzzx Jun 10, 2025
83a3166
fix: some lint changes
hinzzx Jun 10, 2025
8e04a2d
fix: change date calculations to be inclusive
hinzzx Jun 11, 2025
37b9376
fix(ui5-dynamic-date-range): merge options with same constructor, use…
hinzzx Jun 25, 2025
7dc20f5
Merge branch 'main' into ddr-next-last-options
hinzzx Jun 25, 2025
1f3056d
chore: fix lint
hinzzx Jun 25, 2025
c814f39
fix: single date work, manual input invalidation last/next options
hinzzx Jul 2, 2025
355f24d
fix: correct single date value
hinzzx Jul 2, 2025
c48cd99
chore: remove redunant code
hinzzx Jul 7, 2025
da9cf09
fix: address comments
hinzzx Jul 14, 2025
5effee6
chore: remove any type from template
hinzzx Jul 14, 2025
5b24fa2
fix: address comments
hinzzx Jul 14, 2025
9b93ea0
chore: revert deleted docs by mistake
hinzzx Jul 14, 2025
c140b41
fix: address discussion comments
hinzzx Jul 21, 2025
54330f6
Merge branch 'main' into ddr-next-last-options
hinzzx Jul 21, 2025
bef84ae
fix: lint error
hinzzx Jul 22, 2025
59ca47f
Merge branch 'main' into ddr-next-last-options
hinzzx Jul 25, 2025
da25379
Merge branch 'main' into ddr-next-last-options
hinzzx Jul 28, 2025
a8fecc5
fix: address comments, remove redunant code, use UI5date, reduce comp…
hinzzx Jul 28, 2025
8483974
fix: add docs, and style classes to templates
hinzzx Jul 28, 2025
1ab4cd6
fix: remove redunant method
hinzzx Jul 28, 2025
5ce1b58
fix: remove utc
hinzzx Aug 1, 2025
2d09055
fix: address comments, move default value handling
hinzzx Aug 4, 2025
b71626a
Merge branch 'main' into ddr-next-last-options
hinzzx Aug 6, 2025
53b758a
fix: address comments, fix merge conflicts
hinzzx Aug 6, 2025
eabf35c
fix: change setTime to setDate
hinzzx Aug 6, 2025
11df238
fix: address comments
hinzzx Aug 7, 2025
8f01a88
fix: reduce ifs
hinzzx Aug 7, 2025
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
437 changes: 384 additions & 53 deletions packages/main/cypress/specs/DynamicDateRange.cy.tsx

Large diffs are not rendered by default.

134 changes: 100 additions & 34 deletions packages/main/src/DynamicDateRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { isF4, isShow } from "@ui5/webcomponents-base/dist/Keys.js";
import DynamicDateRangeTemplate from "./DynamicDateRangeTemplate.js";
import IconMode from "./types/IconMode.js";
import type Input from "./Input.js";
import type List from "./List.js";
import type ListItem from "./ListItem.js";
import {
DYNAMIC_DATE_RANGE_SELECTED_TEXT,
DYNAMIC_DATE_RANGE_EMPTY_SELECTED_TEXT,
Expand All @@ -25,8 +27,6 @@ import "@ui5/webcomponents-localization/dist/features/calendar/Gregorian.js";
import dynamicDateRangeCss from "./generated/themes/DynamicDateRange.css.js";
import dynamicDateRangePopoverCss from "./generated/themes/DynamicDateRangePopover.css.js";
import ResponsivePopoverCommonCss from "./generated/themes/ResponsivePopoverCommon.css.js";
import type List from "./List.js";
import type ListItem from "./ListItem.js";

type DynamicDateRangeValue = {
/**
Expand All @@ -41,7 +41,7 @@ type DynamicDateRangeValue = {
* @default []
* @public
*/
values?: Date[] | number[];
values?: Array<Date> | Array<number>;
}

/**
Expand All @@ -61,7 +61,7 @@ type DynamicDateRangeValue = {
* Methods:
* - `format(value: DynamicDateRangeValue): string`: Formats the given dynamic date range value into a string representation.
* - `parse(value: string): DynamicDateRangeValue | undefined`: Parses a string into a dynamic date range value.
* - `toDates(value: DynamicDateRangeValue): Date[]`: Converts a dynamic date range value into an array of `Date` objects.
* - `toDates(value: DynamicDateRangeValue): Array<Date>`: Converts a dynamic date range value into an array of `Date` objects.
* - `handleSelectionChange?(event: CustomEvent): DynamicDateRangeValue | undefined`: (Optional) Handles selection changes in the UI of the dynamic date range option.
* - `isValidString(value: string): boolean`: Validates whether a given string is a valid representation of the dynamic date range value.
*
Expand All @@ -74,7 +74,7 @@ interface IDynamicDateRangeOption {
text: string;
format: (value: DynamicDateRangeValue) => string;
parse: (value: string) => DynamicDateRangeValue | undefined;
toDates: (value: DynamicDateRangeValue) => Date[];
toDates: (value: DynamicDateRangeValue) => Array<Date>;
handleSelectionChange?: (event: CustomEvent) => DynamicDateRangeValue | undefined;
template?: JsxTemplate;
isValidString: (value: string) => boolean;
Expand Down Expand Up @@ -104,6 +104,16 @@ interface IDynamicDateRangeOption {
* - "TOMORROW" - Represents the next date. An example value is `{ operator: "TOMORROW"}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/Tomorrow.js";`
* - "DATE" - Represents a single date. An example value is `{ operator: "DATE", values: [new Date()]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/SingleDate.js";`
* - "DATERANGE" - Represents a range of dates. An example value is `{ operator: "DATERANGE", values: [new Date(), new Date()]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/DateRange.js";`
* - "LASTDAYS" - Represents Last X Days from today. An example value is `{ operator: "LASTDAYS", values: [2]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/LastOptions.js";`
* - "LASTWEEKS" - Represents Last X Weeks from today. An example value is `{ operator: "LASTWEEKS", values: [3]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/LastOptions.js";`
* - "LASTMONTHS" - Represents Last X Months from today. An example value is `{ operator: "LASTMONTHS", values: [6]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/LastOptions.js";`
* - "LASTQUARTERS" - Represents Last X Quarters from today. An example value is `{ operator: "LASTQUARTERS", values: [2]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/LastOptions.js";`
* - "LASTYEARS" - Represents Last X Years from today. An example value is `{ operator: "LASTYEARS", values: [1]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/LastOptions.js";`
* - "NEXTDAYS" - Represents Next X Days from today. An example value is `{ operator: "NEXTDAYS", values: [2]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/NextOptions.js";`
* - "NEXTWEEKS" - Represents Next X Weeks from today. An example value is `{ operator: "NEXTWEEKS", values: [3]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/NextOptions.js";`
* - "NEXTMONTHS" - Represents Next X Months from today. An example value is `{ operator: "NEXTMONTHS", values: [6]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/NextOptions.js";`
* - "NEXTQUARTERS" - Represents Next X Quarters from today. An example value is `{ operator: "NEXTQUARTERS", values: [2]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/NextOptions.js";`
* - "NEXTYEARS" - Represents Next X Years from today. An example value is `{ operator: "NEXTYEARS", values: [1]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/NextOptions.js";`
*
* ### ES6 Module Import
*
Expand Down Expand Up @@ -173,9 +183,9 @@ class DynamicDateRange extends UI5Element {
@property({ type: Object })
currentValue?: DynamicDateRangeValue;

optionsObjects: IDynamicDateRangeOption[] = [];
optionsObjects: Array<IDynamicDateRangeOption> = [];

static optionsClasses: Map<string, new () => IDynamicDateRangeOption> = new Map();
static optionsClasses: Map<string, new (operators?: Array<string>) => IDynamicDateRangeOption> = new Map();

@query("[ui5-input]")
_input?: Input;
Expand All @@ -184,31 +194,51 @@ class DynamicDateRange extends UI5Element {
_list?: List;

onBeforeRendering() {
const optionKeys = this.options.split(",").map(option => option.trim());
this.optionsObjects = this._createNormalizedOptions();
this._focusSelectedItem();
}

this.optionsObjects = optionKeys.map(option => {
const OptionClass = DynamicDateRange.getOptionClass(option);
let optionObject;
/**
* Creates and normalizes options from the options string
*/
_createNormalizedOptions(): Array<IDynamicDateRangeOption> {
if (!this.optionsObjects.length) { // initialize options on first use
const optionKeys = this.splitOptions(this.options).filter(Boolean);
const createdOptions: Array<IDynamicDateRangeOption> = [];
const classToOperators = new Map<new(operators?: Array<string>) => IDynamicDateRangeOption, Array<string>>();

// Group operators by their class
optionKeys.forEach(option => {
const OptionClass = DynamicDateRange.getOptionClass(option);
if (OptionClass) {
const operators = classToOperators.get(OptionClass) || [];
operators.push(option);
classToOperators.set(OptionClass, operators);
}
});

if (OptionClass) {
optionObject = new OptionClass();
}
classToOperators.forEach((operators, OptionClass) => {
createdOptions.push(new OptionClass(operators));
});

return optionObject;
}).filter(optionObject => optionObject !== undefined);
return createdOptions;
}
return this.optionsObjects;
}

if (this.value) {
const selectedItem = this._list?.items.find(item => {
const option = this.optionsObjects.find(x => x.operator === this.value?.operator);
return option && item.textContent === option.text;
}) as ListItem;
splitOptions(options: string): Array<string> {
return options.split(",").map(s => s.trim());
}

this._list?.focusItem(selectedItem);
_focusSelectedItem() {
if (!this.value) {
return;
}
}

get _optionsTitles(): Array<string> {
return this.optionsObjects.map(option => option.text);
const listItem = this._list?.items.find(item => (item as ListItem).selected === true);
if (listItem) {
this._list?.focusItem(listItem as ListItem);
}
}

/**
Expand All @@ -233,23 +263,37 @@ class DynamicDateRange extends UI5Element {

_selectOption(e: CustomEvent): void {
this._currentOption = this.optionsObjects.find(option => option.text === e.detail.item.textContent);

if (!this._currentOption?.template) {
this.currentValue = this._currentOption?.parse(this._currentOption.text);
this._submitValue();
} else if (!this.currentValue || this.currentValue.operator !== this._currentOption.operator) {
this.currentValue = undefined;
}

if (this._currentOption?.operator === this.value?.operator) {
this.currentValue = this.value;
}
}

getOption(operator: string) {
getOption(operator?: string) {
if (!operator) {
return this._currentOption;
}

const resultOption = this.optionsObjects.find(option => option.operator === operator);

if (!resultOption) {
const OptionClass = DynamicDateRange.getOptionClass(operator);

if (OptionClass) {
const existingOption = this.optionsObjects.find(option => option.constructor === OptionClass);

if (existingOption) {
existingOption.operator = operator;
return existingOption;
}

const optionObject = new OptionClass();
this.optionsObjects.push(optionObject);

Expand Down Expand Up @@ -290,23 +334,26 @@ class DynamicDateRange extends UI5Element {
* @param value The option to convert into an array of date ranges
* @returns An array of two `Date` objects representing the start and end dates.
*/
toDates(value: DynamicDateRangeValue): Date[] {
return this.getOption(value.operator)?.toDates(value) as Date[];
toDates(value: DynamicDateRangeValue): Array<Date> {
return this.getOption(value.operator)?.toDates(value) as Array<Date>;
}

get _hasCurrentOptionTemplate(): boolean {
return !!this._currentOption?.template;
}

_submitValue() {
const stringValue = this._currentOption?.format(this.currentValue!) as string;
const valueToSubmit = this.currentValue || { operator: this._currentOption?.operator || "", values: [] };
const displayString = this._currentOption?.format(valueToSubmit) || "";

if (this._input) {
this._input.value = stringValue;
this._input.value = displayString;
}

if (this._currentOption?.isValidString(stringValue)) {
this.value = this.currentValue as DynamicDateRangeValue;
if (!this._currentOption || !valueToSubmit.operator) {
this.value = undefined;
} else if (this._currentOption.isValidString(displayString)) {
this.value = valueToSubmit;
this.fireDecoratorEvent("change");
} else {
this.value = undefined;
Expand All @@ -332,15 +379,34 @@ class DynamicDateRange extends UI5Element {
}

get currentValueText() {
if (this.currentValue && this.currentValue.operator === this._currentOption?.operator) {
return `${DynamicDateRange.i18nBundle.getText(DYNAMIC_DATE_RANGE_SELECTED_TEXT)}: ${this._currentOption?.format(this.currentValue)}`;
if (this.currentValue) {
const correctOption = this.getOption(this.currentValue.operator);
if (correctOption) {
const dates = correctOption.toDates(this.currentValue);
const displayValue = { ...this.currentValue, values: dates };
const displayText = correctOption.format(displayValue);
return `${DynamicDateRange.i18nBundle.getText(DYNAMIC_DATE_RANGE_SELECTED_TEXT)}: ${displayText}`;
}
}

if (this._currentOption) {
const emptyValue = { operator: this._currentOption.operator, values: [] };
const displayText = this._currentOption.format(emptyValue);
if (displayText && displayText.trim()) {
return `${DynamicDateRange.i18nBundle.getText(DYNAMIC_DATE_RANGE_SELECTED_TEXT)}: ${displayText}`;
}
}

return DynamicDateRange.i18nBundle.getText(DYNAMIC_DATE_RANGE_EMPTY_SELECTED_TEXT);
}

handleSelectionChange(e: CustomEvent) {
this.currentValue = this._currentOption?.handleSelectionChange && this._currentOption?.handleSelectionChange(e) as DynamicDateRangeValue;

// Update _currentOption if the operator changed
if (this.currentValue && this.currentValue.operator !== this._currentOption?.operator) {
this._currentOption = this.getOption(this.currentValue.operator);
}
}

onInputKeyDown(e: KeyboardEvent) {
Expand Down
7 changes: 1 addition & 6 deletions packages/main/src/DynamicDateRangeInputTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import appointment from "@ui5/webcomponents-icons/dist/appointment-2.js";

export default function DynamicDateRangeInputTemplate(this: DynamicDateRange) {
return (
<div
class="ui5-dynamic-date-range-root"
style={{
width: "100%",
}}
>
<div class="ui5-dynamic-date-range-root">
<Input
data-sap-focus-ref
id={`${this._id}-inner`}
Expand Down
12 changes: 7 additions & 5 deletions packages/main/src/DynamicDateRangePopoverTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,17 @@ export default function DynamicDateRangePopoverTemplate(this: DynamicDateRange)
selectionMode="Single"
onItemClick={this._selectOption}
>
{this.optionsObjects.map(option => {
return <ListItemStandard
{this.optionsObjects.map(option => (
<ListItemStandard
selected={option.operator === this.value?.operator}
iconEnd={true}
icon={option.icon}
type={!option.template ? ListItemType.Active : ListItemType.Navigation}>
wrappingType="Normal"
type={option.template ? ListItemType.Navigation : ListItemType.Active}
>
{option.text}
</ListItemStandard>;
})}
</ListItemStandard>
))}
</List>
</div>
:
Expand Down
2 changes: 2 additions & 0 deletions packages/main/src/bundle.esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,12 @@ import Input from "./Input.js";
import SuggestionItemCustom from "./SuggestionItemCustom.js";
import MultiInput from "./MultiInput.js";
import Label from "./Label.js";
import LastOptions from "./dynamic-date-range-options/LastOptions.js";
import Link from "./Link.js";
import Menu from "./Menu.js";
import MenuItem from "./MenuItem.js";
import MenuSeparator from "./MenuSeparator.js";
import NextOptions from "./dynamic-date-range-options/NextOptions.js";
import MenuItemGroup from "./MenuItemGroup.js";
import Popover from "./Popover.js";
import Panel from "./Panel.js";
Expand Down
34 changes: 23 additions & 11 deletions packages/main/src/dynamic-date-range-options/DateRange.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import DateRangeRangeTemplate from "./DateRangeTemplate.js";
import DateRangeTemplate from "./DateRangeTemplate.js";
import type { DynamicDateRangeValue, IDynamicDateRangeOption } from "../DynamicDateRange.js";
import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js";
import UI5Date from "@ui5/webcomponents-localization/dist/dates/UI5Date.js";
import type { JsxTemplate } from "@ui5/webcomponents-base/dist/index.js";
import {
DYNAMIC_DATE_RANGE_DATERANGE_TEXT,
Expand All @@ -15,11 +16,11 @@ import DynamicDateRange from "../DynamicDateRange.js";
* @since 2.11.0
*/

class DateRangeRange implements IDynamicDateRangeOption {
class DateRange implements IDynamicDateRangeOption {
template: JsxTemplate;

constructor() {
this.template = DateRangeRangeTemplate;
this.template = DateRangeTemplate;
}

parse(value: string): DynamicDateRangeValue {
Expand All @@ -32,9 +33,9 @@ class DateRangeRange implements IDynamicDateRangeOption {
}

format(value: DynamicDateRangeValue) {
const valuesArray = value?.values as Date[];
const valuesArray = value?.values as Array<Date>;

if (!valuesArray || valuesArray.length !== 2) {
if (!valuesArray || valuesArray.length !== 2 || !valuesArray[1]) {
return "";
}

Expand All @@ -43,7 +44,7 @@ class DateRangeRange implements IDynamicDateRangeOption {
return formattedValue;
}

toDates(value: DynamicDateRangeValue): Date[] {
toDates(value: DynamicDateRangeValue): Array<Date> {
return dateRangeOptionToDates(value);
}

Expand All @@ -60,7 +61,7 @@ class DateRangeRange implements IDynamicDateRangeOption {
}

isValidString(value: string): boolean {
const dates = this.getFormat().parse(value) as Date[];
const dates = this.getFormat().parse(value) as Array<Date>;

if (!dates[0] || !dates[1] || Number.isNaN(dates[0].getTime()) || Number.isNaN(dates[1].getTime())) {
return false;
Expand All @@ -83,17 +84,28 @@ class DateRangeRange implements IDynamicDateRangeOption {
currentValue.operator = this.operator;

if (e.detail.selectedDates[0]) {
currentValue.values[0] = new Date(e.detail.selectedDates[0] * 1000);
currentValue.values[0] = UI5Date.getInstance(e.detail.selectedDates[0] * 1000);
}

if (e.detail.selectedDates[1]) {
currentValue.values[1] = new Date(e.detail.selectedDates[1] * 1000);
currentValue.values[1] = UI5Date.getInstance(e.detail.selectedDates[1] * 1000);
}

// Handle backwards date ranges by automatically flipping them
if (currentValue.values.length === 2 && currentValue.values[0] && currentValue.values[1]) {
const startDate = currentValue.values[0] as UI5Date;
const endDate = currentValue.values[1] as UI5Date;

// If start date is after end date, flip them
if (startDate.getTime() > endDate.getTime()) {
currentValue.values = [endDate, startDate];
}
}

return currentValue;
}
}

DynamicDateRange.register("DATERANGE", DateRangeRange);
DynamicDateRange.register("DATERANGE", DateRange);

export default DateRangeRange;
export default DateRange;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type DynamicDateRange from "../DynamicDateRange.js";
import Calendar from "../Calendar.js";
import CalendarDateRange from "../CalendarDateRange.js";

export default function DateRangeRangeTemplate(this: DynamicDateRange) {
export default function DateRangeTemplate(this: DynamicDateRange) {
return (
<Calendar onSelectionChange={this.handleSelectionChange} selectionMode="Range">
<CalendarDateRange
Expand Down
Loading
Loading