Skip to content

Commit f0bdc9a

Browse files
feat(ui5-dynamic-date-range): implement date time from to options (#12312)
Introduce new “date-time” options for the UI5 Dynamic Date Range component. This feature expands the existing set of options by enabling date + time selection, including: A new FromDateTime option (start date + time) A new ToDateTime option (end date + time) Parsing / formatting support for date/time values
1 parent 12f391f commit f0bdc9a

File tree

10 files changed

+809
-10
lines changed

10 files changed

+809
-10
lines changed

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

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import DateRange from '../../src/dynamic-date-range-options/DateRange.js';
44
import Today from '../../src/dynamic-date-range-options/Today.js';
55
import LastOptions from '../../src/dynamic-date-range-options/LastOptions.js';
66
import NextOptions from '../../src/dynamic-date-range-options/NextOptions.js';
7+
import FromDateTime from '../../src/dynamic-date-range-options/FromDateTime.js';
8+
import ToDateTime from '../../src/dynamic-date-range-options/ToDateTime.js';
79

810
describe('DynamicDateRange Component', () => {
911
beforeEach(() => {
@@ -548,3 +550,327 @@ describe('DynamicDateRange Last/Next Options', () => {
548550
.should('have.attr', 'selected');
549551
});
550552
});
553+
554+
describe('FromDateTime Option', () => {
555+
beforeEach(() => {
556+
cy.mount(<DynamicDateRange options="FROMDATETIME">
557+
</DynamicDateRange>
558+
);
559+
});
560+
it('should select FromDateTime option and display date/time picker', () => {
561+
const mockOptions: Array<IDynamicDateRangeOption> = [
562+
new FromDateTime(),
563+
];
564+
cy.get('[ui5-dynamic-date-range]')
565+
.as("ddr");
566+
567+
cy.get("@ddr")
568+
.shadow()
569+
.find('[ui5-input]')
570+
.as("input");
571+
572+
cy.get("@input")
573+
.should('exist');
574+
575+
cy.get("@input")
576+
.find('[ui5-icon]')
577+
.click();
578+
579+
cy.get("@ddr")
580+
.shadow()
581+
.find("[ui5-responsive-popover]")
582+
.as("popover");
583+
584+
cy.get("@popover")
585+
.should('exist');
586+
587+
cy.get("@popover")
588+
.find("[ui5-list]")
589+
.as("list");
590+
591+
cy.get("@list")
592+
.find("[ui5-li]")
593+
.contains('From')
594+
.click();
595+
596+
cy.get("@popover")
597+
.find(".ui5-dynamic-date-range-option-datetime-container")
598+
.should('exist');
599+
600+
cy.get("@popover")
601+
.find("[ui5-segmented-button]")
602+
.should('exist');
603+
604+
cy.get("@popover")
605+
.find("[ui5-segmented-button-item][data-ui5-key='Date']")
606+
.should('have.attr', 'selected');
607+
});
608+
609+
it('should toggle between date and time views', () => {
610+
const mockOptions: Array<IDynamicDateRangeOption> = [
611+
new FromDateTime(),
612+
];
613+
cy.get('[ui5-dynamic-date-range]')
614+
.as("ddr");
615+
616+
cy.get("@ddr")
617+
.shadow()
618+
.find('[ui5-input]')
619+
.as("input");
620+
621+
cy.get("@input")
622+
.find('[ui5-icon]')
623+
.click();
624+
625+
cy.get("@ddr")
626+
.shadow()
627+
.find("[ui5-responsive-popover]")
628+
.as("popover");
629+
630+
cy.get("@popover")
631+
.find("[ui5-list]")
632+
.find("[ui5-li]")
633+
.contains('From')
634+
.click();
635+
636+
cy.get("@popover")
637+
.find("[ui5-segmented-button-item][data-ui5-key='Date']")
638+
.should('have.attr', 'selected');
639+
640+
cy.get("@popover")
641+
.find("[ui5-segmented-button-item][data-ui5-key='Time']")
642+
.click();
643+
644+
cy.get("@popover")
645+
.find("[ui5-segmented-button-item][data-ui5-key='Time']")
646+
.should('have.attr', 'selected');
647+
648+
cy.get("@popover")
649+
.find("[ui5-time-selection-clocks]")
650+
.should('exist');
651+
652+
cy.get("@popover")
653+
.find("[ui5-segmented-button-item][data-ui5-key='Date']")
654+
.click();
655+
656+
cy.get("@popover")
657+
.find("[ui5-segmented-button-item][data-ui5-key='Date']")
658+
.should('have.attr', 'selected');
659+
});
660+
661+
it('should select a date and time, then submit the value', () => {
662+
const mockOptions: Array<IDynamicDateRangeOption> = [
663+
new FromDateTime(),
664+
];
665+
cy.get('[ui5-dynamic-date-range]')
666+
.as("ddr");
667+
668+
cy.get("@ddr")
669+
.shadow()
670+
.find('[ui5-input]')
671+
.as("input");
672+
673+
cy.get("@input")
674+
.find('[ui5-icon]')
675+
.click();
676+
677+
cy.get("@ddr")
678+
.shadow()
679+
.find("[ui5-responsive-popover]")
680+
.as("popover");
681+
682+
cy.get("@popover")
683+
.find("[ui5-list]")
684+
.find("[ui5-li]")
685+
.contains('From')
686+
.click();
687+
688+
cy.get("@popover")
689+
.find("[ui5-calendar]")
690+
.as("calendar");
691+
692+
cy.get("@calendar")
693+
.shadow()
694+
.find("ui5-daypicker")
695+
.shadow()
696+
.find(".ui5-dp-daytext")
697+
.eq(15)
698+
.click(); // Select day 13
699+
700+
cy.get("@popover")
701+
.find("[ui5-button][design='Emphasized']")
702+
.click();
703+
704+
cy.get("@input")
705+
.shadow()
706+
.find("input")
707+
.should('contain.value', 'From');
708+
709+
cy.get("@input")
710+
.shadow()
711+
.find("input")
712+
.should('contain.value', 'From Oct 13, 2025');
713+
});
714+
});
715+
716+
describe('ToDateTime Option', () => {
717+
beforeEach(() => {
718+
cy.mount(<DynamicDateRange options="TODATETIME">
719+
</DynamicDateRange>
720+
);
721+
});
722+
it('should select FromDateTime option and display date/time picker', () => {
723+
const mockOptions: Array<IDynamicDateRangeOption> = [
724+
new ToDateTime(),
725+
];
726+
cy.get('[ui5-dynamic-date-range]')
727+
.as("ddr");
728+
729+
cy.get("@ddr")
730+
.shadow()
731+
.find('[ui5-input]')
732+
.as("input");
733+
734+
cy.get("@input")
735+
.should('exist');
736+
737+
cy.get("@input")
738+
.find('[ui5-icon]')
739+
.click();
740+
741+
cy.get("@ddr")
742+
.shadow()
743+
.find("[ui5-responsive-popover]")
744+
.as("popover");
745+
746+
cy.get("@popover")
747+
.should('exist');
748+
749+
cy.get("@popover")
750+
.find("[ui5-list]")
751+
.as("list");
752+
753+
cy.get("@list")
754+
.find("[ui5-li]")
755+
.contains('To')
756+
.click();
757+
758+
cy.get("@popover")
759+
.find(".ui5-dynamic-date-range-option-datetime-container")
760+
.should('exist');
761+
762+
cy.get("@popover")
763+
.find("[ui5-segmented-button]")
764+
.should('exist');
765+
766+
cy.get("@popover")
767+
.find("[ui5-segmented-button-item][data-ui5-key='Date']")
768+
.should('have.attr', 'selected');
769+
});
770+
771+
it('should toggle between date and time views', () => {
772+
const mockOptions: Array<IDynamicDateRangeOption> = [
773+
new ToDateTime(),
774+
];
775+
cy.get('[ui5-dynamic-date-range]')
776+
.as("ddr");
777+
778+
cy.get("@ddr")
779+
.shadow()
780+
.find('[ui5-input]')
781+
.as("input");
782+
783+
cy.get("@input")
784+
.find('[ui5-icon]')
785+
.click();
786+
787+
cy.get("@ddr")
788+
.shadow()
789+
.find("[ui5-responsive-popover]")
790+
.as("popover");
791+
792+
cy.get("@popover")
793+
.find("[ui5-list]")
794+
.find("[ui5-li]")
795+
.contains('To')
796+
.click();
797+
798+
cy.get("@popover")
799+
.find("[ui5-segmented-button-item][data-ui5-key='Date']")
800+
.should('have.attr', 'selected');
801+
802+
cy.get("@popover")
803+
.find("[ui5-segmented-button-item][data-ui5-key='Time']")
804+
.click();
805+
806+
cy.get("@popover")
807+
.find("[ui5-segmented-button-item][data-ui5-key='Time']")
808+
.should('have.attr', 'selected');
809+
810+
cy.get("@popover")
811+
.find("[ui5-time-selection-clocks]")
812+
.should('exist');
813+
814+
cy.get("@popover")
815+
.find("[ui5-segmented-button-item][data-ui5-key='Date']")
816+
.click();
817+
818+
cy.get("@popover")
819+
.find("[ui5-segmented-button-item][data-ui5-key='Date']")
820+
.should('have.attr', 'selected');
821+
});
822+
823+
it('should select a date and time, then submit the value', () => {
824+
const mockOptions: Array<IDynamicDateRangeOption> = [
825+
new ToDateTime(),
826+
];
827+
cy.get('[ui5-dynamic-date-range]')
828+
.as("ddr");
829+
830+
cy.get("@ddr")
831+
.shadow()
832+
.find('[ui5-input]')
833+
.as("input");
834+
835+
cy.get("@input")
836+
.find('[ui5-icon]')
837+
.click();
838+
839+
cy.get("@ddr")
840+
.shadow()
841+
.find("[ui5-responsive-popover]")
842+
.as("popover");
843+
844+
cy.get("@popover")
845+
.find("[ui5-list]")
846+
.find("[ui5-li]")
847+
.contains('To')
848+
.click();
849+
850+
cy.get("@popover")
851+
.find("[ui5-calendar]")
852+
.as("calendar");
853+
854+
cy.get("@calendar")
855+
.shadow()
856+
.find("ui5-daypicker")
857+
.shadow()
858+
.find(".ui5-dp-daytext")
859+
.eq(15)
860+
.click(); // Select day 13
861+
862+
cy.get("@popover")
863+
.find("[ui5-button][design='Emphasized']")
864+
.click();
865+
866+
cy.get("@input")
867+
.shadow()
868+
.find("input")
869+
.should('contain.value', 'To');
870+
871+
cy.get("@input")
872+
.shadow()
873+
.find("input")
874+
.should('contain.value', 'To Oct 13, 2025');
875+
});
876+
});

packages/main/src/DynamicDateRange.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,10 @@ interface IDynamicDateRangeOption {
7676
format: (value: DynamicDateRangeValue) => string;
7777
parse: (value: string) => DynamicDateRangeValue | undefined;
7878
toDates: (value: DynamicDateRangeValue) => Array<Date>;
79-
handleSelectionChange?: (event: CustomEvent) => DynamicDateRangeValue | undefined;
79+
handleSelectionChange?: (event: CustomEvent, value: DynamicDateRangeValue | undefined) => DynamicDateRangeValue | undefined;
8080
template?: JsxTemplate;
8181
isValidString: (value: string) => boolean;
82+
resetState?: () => void;
8283
}
8384

8485
/**
@@ -105,6 +106,8 @@ interface IDynamicDateRangeOption {
105106
* - "TOMORROW" - Represents the next date. An example value is `{ operator: "TOMORROW"}`. Import: `import "@ui5/webcomponents/dist/dynamic-date-range-options/Tomorrow.js";`
106107
* - "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";`
107108
* - "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+
* - "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";`
110+
* - "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";`
108111
* - "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";`
109112
* - "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";`
110113
* - "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";`
@@ -285,7 +288,7 @@ class DynamicDateRange extends UI5Element {
285288

286289
_togglePicker(): void {
287290
if (this.open) {
288-
this.open = false;
291+
this._close();
289292
} else {
290293
this.open = true;
291294
}
@@ -390,11 +393,15 @@ class DynamicDateRange extends UI5Element {
390393
this.value = undefined;
391394
}
392395

396+
this._currentOption?.resetState?.();
397+
393398
this._currentOption = undefined;
394399
this.open = false;
395400
}
396401

397402
_close() {
403+
this._currentOption?.resetState?.();
404+
398405
this._currentOption = undefined;
399406
this.open = false;
400407
}
@@ -432,7 +439,12 @@ class DynamicDateRange extends UI5Element {
432439
}
433440

434441
handleSelectionChange(e: CustomEvent) {
435-
this.currentValue = this._currentOption?.handleSelectionChange && this._currentOption?.handleSelectionChange(e) as DynamicDateRangeValue;
442+
const value = this._currentOption?.handleSelectionChange?.(e, this.currentValue) as DynamicDateRangeValue;
443+
444+
this.currentValue = JSON.parse(JSON.stringify(value)); // deep clone
445+
if (this.currentValue) {
446+
this.currentValue.values = value?.values;
447+
}
436448

437449
// Update _currentOption if the operator changed
438450
if (this.currentValue && this.currentValue.operator !== this._currentOption?.operator) {

0 commit comments

Comments
 (0)