Skip to content

Commit fd0d36f

Browse files
authored
feat(ui5-dynamic-date-range): introduce From/To (Date & Time) option (#12341)
With this PR we introduce a new "From/To (Date & Time)" option for the ui5-dynamic-date-range, allowing users to select a range of dates with times. Related to: #12182
1 parent fd8ed79 commit fd0d36f

File tree

12 files changed

+491
-271
lines changed

12 files changed

+491
-271
lines changed

packages/main/cypress/specs/DynamicDateRange.cy.tsx

Lines changed: 150 additions & 269 deletions
Large diffs are not rendered by default.

packages/main/cypress/support/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import "./commands/ColorPalettePopover.commands.js";
4747
import "./commands/ColorPicker.commands.js";
4848
import "./commands/DateTimePicker.commands.js";
4949
import "./commands/DateRangePicker.commands.js";
50+
import "./commands/DynamicDateRange.commands.js";
5051
import "./commands/Dialog.commands.ts";
5152
import "./commands/Popover.commands.ts";
5253
import "./commands/ResponsivePopover.commands.js";
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
Cypress.Commands.add("ui5DynamicDateRangeOpen", { prevSubject: true }, (prevSubject) => {
2+
cy.wrap(prevSubject)
3+
.as("ddr")
4+
.shadow()
5+
.find("[ui5-input]")
6+
.as("input");
7+
8+
cy.get("@input")
9+
.shadow()
10+
.find("input")
11+
.as("innerInput");
12+
13+
cy.get("@input")
14+
.find('[ui5-icon]')
15+
.as("icon");
16+
17+
cy.get("@icon")
18+
.realClick();
19+
20+
cy.get("@ddr")
21+
.ui5DynamicDateRangeOpened();
22+
23+
return cy.wrap(prevSubject);
24+
});
25+
26+
Cypress.Commands.add("ui5DynamicDateRangeOpened", { prevSubject: true }, (subject) => {
27+
cy.wrap(subject)
28+
.as("ddr");
29+
30+
cy.get("@ddr")
31+
.should("have.attr", "open");
32+
33+
cy.get("@ddr")
34+
.shadow()
35+
.find("[ui5-responsive-popover]")
36+
.as("popover")
37+
.should("have.attr", "open");
38+
39+
cy.get("@popover")
40+
.find("[ui5-list]")
41+
.as("list")
42+
.should("be.visible");
43+
});
44+
45+
Cypress.Commands.add("ui5DynamicDateRangeGetOptionsList", { prevSubject: true }, (subject) => {
46+
cy.wrap(subject)
47+
.as("ddr");
48+
49+
cy.get("@ddr")
50+
.shadow()
51+
.find("[ui5-responsive-popover]")
52+
.as("popover");
53+
54+
cy.get("@popover")
55+
.should('exist');
56+
57+
cy.get("@popover")
58+
.find("[ui5-list]")
59+
.as("list");
60+
61+
return cy.get('@list')
62+
.find("[ui5-li]");
63+
});
64+
65+
Cypress.Commands.add("ui5DynamicDateRangeSelectOption", { prevSubject: true }, (prevSubject, index?: number) => {
66+
const optionIndex = index ?? 0;
67+
68+
cy.wrap(prevSubject)
69+
.as("ddr");
70+
71+
cy.get("@ddr")
72+
.ui5DynamicDateRangeGetOptionsList()
73+
.eq(optionIndex)
74+
.realClick();
75+
76+
return cy.wrap(prevSubject);
77+
});
78+
79+
Cypress.Commands.add("ui5DynamicDateRangeSetDateTime", { prevSubject: true }, (prevSubject, pickerId: string, dateTimeValue: string) => {
80+
cy.wrap(prevSubject)
81+
.as("ddr");
82+
83+
cy.get("@ddr")
84+
.shadow()
85+
.find("[ui5-responsive-popover]")
86+
.find(`[ui5-datetime-picker]#${pickerId}`)
87+
.as("picker");
88+
89+
cy.get("@picker")
90+
.shadow()
91+
.find("[ui5-datetime-input]")
92+
.as("input");
93+
94+
cy.get("@input")
95+
.shadow()
96+
.find("input")
97+
.as("innerInput");
98+
99+
cy.get("@innerInput")
100+
.clear()
101+
.realType(dateTimeValue)
102+
.realPress("Enter");
103+
104+
return cy.wrap(prevSubject);
105+
});
106+
107+
Cypress.Commands.add("ui5DynamicDateRangeSubmit", { prevSubject: true }, (prevSubject) => {
108+
cy.wrap(prevSubject)
109+
.as("ddr");
110+
111+
cy.get("@ddr")
112+
.shadow()
113+
.find("[ui5-responsive-popover]")
114+
.as("popover");
115+
116+
cy.get("@popover")
117+
.find("[ui5-button][design='Emphasized']")
118+
.as("submitButton");
119+
120+
cy.get("@submitButton")
121+
.realClick();
122+
});
123+
124+
declare global {
125+
namespace Cypress {
126+
interface Chainable {
127+
ui5DynamicDateRangeOpen(): Chainable<JQuery<HTMLElement>>
128+
ui5DynamicDateRangeOpened(): Chainable<void>
129+
ui5DynamicDateRangeGetOptionsList(): Chainable<JQuery<HTMLElement>>
130+
ui5DynamicDateRangeSelectOption(index?: number): Chainable<JQuery<HTMLElement>>
131+
ui5DynamicDateRangeSetDateTime(pickerId: string, dateTimeValue: string): Chainable<JQuery<HTMLElement>>
132+
ui5DynamicDateRangeSubmit(): Chainable<void>
133+
}
134+
}
135+
}

packages/main/src/DynamicDateRange.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ interface IDynamicDateRangeOption {
106106
* - "TOMORROW" - Represents the next date. An example value is `{ operator: "TOMORROW"}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/Tomorrow.js";`
107107
* - "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";`
108108
* - "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";`
109+
* - "DATETIMERANGE" - Represents a range of dates with times. An example value is `{ operator: "DATETIMERANGE", values: [new Date(), new Date()]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/DateTimeRange.js";`
109110
* - "FROMDATETIME" - Represents a range from date and time. An example value is `{ operator: "FROMDATETIME", values: [new Date()]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/FromDateTime.js";`
110111
* - "TODATETIME" - Represents a range to date and time. An example value is `{ operator: "TODATETIME", values: [new Date()]}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/ToDateTime.js";`
111112
* - "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";`

packages/main/src/bundle.esm.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import Yesterday from "./dynamic-date-range-options/Yesterday.js";
5656
import Tomorrow from "./dynamic-date-range-options/Tomorrow.js";
5757
import SingleDate from "./dynamic-date-range-options/SingleDate.js";
5858
import DateRange from "./dynamic-date-range-options/DateRange.js";
59+
import DateTimeRange from "./dynamic-date-range-options/DateTimeRange.js";
5960
import FromDateTime from "./dynamic-date-range-options/FromDateTime.js";
6061
import ToDateTime from "./dynamic-date-range-options/ToDateTime.js";
6162
import ExpandableText from "./ExpandableText.js";
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import DateTimeRangeTemplate from "./DateTimeRangeTemplate.js";
2+
import type { DynamicDateRangeValue, IDynamicDateRangeOption } from "../DynamicDateRange.js";
3+
import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js";
4+
import type { JsxTemplate } from "@ui5/webcomponents-base/dist/index.js";
5+
import {
6+
DYNAMIC_DATE_TIME_RANGE_TEXT,
7+
} from "../generated/i18n/i18n-defaults.js";
8+
import { dateTimeRangeOptionToDates } from "./toDates.js";
9+
import DynamicDateRange from "../DynamicDateRange.js";
10+
import getCachedLocaleDataInstance from "@ui5/webcomponents-localization/dist/getCachedLocaleDataInstance.js";
11+
import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js";
12+
13+
const DEFAULT_DELIMITER = "-";
14+
15+
/**
16+
* @class
17+
* @constructor
18+
* @public
19+
* @since 2.16.0
20+
*/
21+
22+
class DateTimeRange implements IDynamicDateRangeOption {
23+
template: JsxTemplate;
24+
25+
constructor() {
26+
this.template = DateTimeRangeTemplate;
27+
}
28+
29+
parse(value: string): DynamicDateRangeValue {
30+
const returnValue = { operator: this.operator, values: [] } as DynamicDateRangeValue;
31+
32+
if (!value) {
33+
return returnValue;
34+
}
35+
const splitValue = value.split(DEFAULT_DELIMITER);
36+
const startDate = this._parseDate(splitValue[0].trim()) as Date;
37+
const endDate = this._parseDate(splitValue[1].trim()) as Date;
38+
39+
returnValue.values = [startDate, endDate];
40+
41+
if (returnValue.values[0].getTime() > returnValue.values[1].getTime()) {
42+
returnValue.values.reverse();
43+
}
44+
45+
return returnValue;
46+
}
47+
48+
_parseDate(value: string): Date | undefined {
49+
return this.getFormat().parse(value) as Date;
50+
}
51+
52+
format(value: DynamicDateRangeValue) {
53+
const valuesArray = value?.values as Array<Date>;
54+
55+
if (!valuesArray || valuesArray.length !== 2 || !valuesArray[0] || !valuesArray[1]) {
56+
return "";
57+
}
58+
59+
const startDate = this._formatDate(valuesArray[0]);
60+
const endDate = this._formatDate(valuesArray[1]);
61+
62+
return `${startDate} ${DEFAULT_DELIMITER} ${endDate}`;
63+
}
64+
65+
_formatDate(date: Date) {
66+
return this.getFormat().format(date);
67+
}
68+
69+
toDates(value: DynamicDateRangeValue): Array<Date> {
70+
return dateTimeRangeOptionToDates(value);
71+
}
72+
73+
get text(): string {
74+
return DynamicDateRange.i18nBundle.getText(DYNAMIC_DATE_TIME_RANGE_TEXT);
75+
}
76+
77+
get operator() {
78+
return "DATETIMERANGE";
79+
}
80+
81+
get icon() {
82+
return "appointment-2";
83+
}
84+
85+
isValidString(value: string): boolean {
86+
const splitValue = value.split(DEFAULT_DELIMITER);
87+
const startDate = this._parseDate(splitValue[0].trim()) as Date;
88+
const endDate = this._parseDate(splitValue[1].trim()) as Date;
89+
90+
if (!startDate || !endDate || Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
91+
return false;
92+
}
93+
94+
return true;
95+
}
96+
97+
getFormatPattern() {
98+
const localeData = getCachedLocaleDataInstance(getLocale());
99+
return localeData.getCombinedDateTimePattern("medium", "medium");
100+
}
101+
102+
getFormat(): DateFormat {
103+
return DateFormat.getDateInstance({
104+
strictParsing: true,
105+
pattern: this.getFormatPattern(),
106+
});
107+
}
108+
}
109+
110+
DynamicDateRange.register("DATETIMERANGE", DateTimeRange);
111+
112+
export default DateTimeRange;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import DynamicDateRange from "../DynamicDateRange.js";
2+
import DateTimePicker from "../DateTimePicker.js";
3+
import Label from "../Label.js";
4+
import {
5+
DYNAMIC_DATE_TIME_RANGE_TEXT_TO_LABEL,
6+
DYNAMIC_DATE_TIME_RANGE_TEXT_FROM_LABEL,
7+
} from "../generated/i18n/i18n-defaults.js";
8+
9+
export default function DateTimeRangeTemplate(this: DynamicDateRange) {
10+
const currentOperator = this.currentValue?.operator || "DATETIMERANGE";
11+
12+
const getDateFromValue = (index = 0) => {
13+
if (this.value?.operator === currentOperator && this.value.values && this.value.values.length === 2) {
14+
return this.getOption(this.value.operator)?.format(this.value)?.split("-")[index].trim();
15+
}
16+
return undefined;
17+
};
18+
19+
const handleSelectionChange = () => {
20+
const fromPicker = this.shadowRoot?.querySelector("[ui5-datetime-picker]#from-picker") as DateTimePicker;
21+
const toPicker = this.shadowRoot?.querySelector("[ui5-datetime-picker]#to-picker") as DateTimePicker;
22+
23+
const fromDateValue = fromPicker.dateValue;
24+
const toDateValue = toPicker.dateValue;
25+
26+
// If there are no dates selected, clear the value
27+
if (!(fromDateValue && toDateValue)) {
28+
this.currentValue = {
29+
operator: currentOperator,
30+
values: []
31+
};
32+
return;
33+
}
34+
35+
const newValue = [fromDateValue, toDateValue];
36+
37+
if (newValue[0] && newValue[1] && newValue[0].getTime() > newValue[1].getTime()) {
38+
newValue.reverse();
39+
}
40+
41+
this.currentValue = {
42+
operator: currentOperator,
43+
values: newValue,
44+
};
45+
};
46+
return (
47+
<div class="ui5-last-next-container ui5-last-next-container-padded">
48+
<Label class="ui5-ddr-label">{DynamicDateRange.i18nBundle.getText(DYNAMIC_DATE_TIME_RANGE_TEXT_FROM_LABEL)}</Label>
49+
<DateTimePicker
50+
id="from-picker"
51+
onChange={handleSelectionChange}
52+
value={getDateFromValue()}>
53+
</DateTimePicker>
54+
<Label class="ui5-ddr-label">{DynamicDateRange.i18nBundle.getText(DYNAMIC_DATE_TIME_RANGE_TEXT_TO_LABEL)}</Label>
55+
<DateTimePicker
56+
id="to-picker"
57+
onChange={handleSelectionChange}
58+
value={getDateFromValue(1)}>
59+
</DateTimePicker>
60+
</div>
61+
);
62+
}

packages/main/src/dynamic-date-range-options/toDates.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import type { DynamicDateRangeValue, IDynamicDateRangeOption } from "../DynamicD
22
import UI5Date from "@ui5/webcomponents-localization/dist/dates/UI5Date.js";
33

44
const dateOptionToDates = (value: DynamicDateRangeValue): Array<Date> => {
5+
if (!value || !value.values || value.values.length !== 1) {
6+
return [];
7+
}
8+
59
const startDate = value.values ? value.values[0] as Date : UI5Date.getInstance();
610
const endDate = UI5Date.getInstance(startDate.getTime());
711

@@ -12,6 +16,10 @@ const dateOptionToDates = (value: DynamicDateRangeValue): Array<Date> => {
1216
};
1317

1418
const dateRangeOptionToDates = (value: DynamicDateRangeValue): Array<Date> => {
19+
if (!value || !value.values || value.values.length !== 2) {
20+
return [];
21+
}
22+
1523
const startDate = value.values ? value.values[0] as Date : UI5Date.getInstance();
1624
const endDate = value.values ? value.values[1] as Date : UI5Date.getInstance();
1725

@@ -21,6 +29,17 @@ const dateRangeOptionToDates = (value: DynamicDateRangeValue): Array<Date> => {
2129
return [startDate, endDate];
2230
};
2331

32+
const dateTimeRangeOptionToDates = (value: DynamicDateRangeValue): Array<Date> => {
33+
if (!value || !value.values || value.values.length !== 2) {
34+
return [];
35+
}
36+
37+
const startDate = value.values ? value.values[0] as Date : UI5Date.getInstance();
38+
const endDate = value.values ? value.values[1] as Date : UI5Date.getInstance();
39+
40+
return [startDate, endDate];
41+
};
42+
2443
const todayToDates = (): Array<Date> => {
2544
const startDate = UI5Date.getInstance();
2645
const endDate = UI5Date.getInstance();
@@ -194,6 +213,7 @@ const dateTimeOptionToDates = (value: DynamicDateRangeValue): Array<Date> => {
194213
export {
195214
dateOptionToDates,
196215
dateRangeOptionToDates,
216+
dateTimeRangeOptionToDates,
197217
todayToDates,
198218
tomorrowToDates,
199219
yesterdayToDates,

packages/main/src/i18n/messagebundle.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,12 @@ DYNAMIC_DATE_RANGE_TO_TEXT=To (Date Time)
817817
#XFLD: Text for the selected date in the DynamicDateRange component.
818818
DYNAMIC_DATE_RANGE_SELECTED_TEXT=Selected
819819

820+
DYNAMIC_DATE_TIME_RANGE_TEXT=From / To (Date and Time)
821+
822+
DYNAMIC_DATE_TIME_RANGE_TEXT_TO_LABEL=To
823+
824+
DYNAMIC_DATE_TIME_RANGE_TEXT_FROM_LABEL=From
825+
820826
#FLD: Text for the selected date section when there's no value in the DynamicDateRange component.
821827
DYNAMIC_DATE_RANGE_EMPTY_SELECTED_TEXT=Choose Dates
822828

0 commit comments

Comments
 (0)