Skip to content

Commit 2f78deb

Browse files
IvanKitanov17Ivan KitanovddariborkaraivanovSisIvanova
authored
Adding Date Range Picker Component (#1614)
* feat: Adding IgcDateRangePickerComponent Closes #1596 --------- Co-authored-by: Ivan Kitanov <[email protected]> Co-authored-by: ddaribo <[email protected]> Co-authored-by: Bozhidara Pachilova <[email protected]> Co-authored-by: Radoslav Karaivanov <[email protected]> Co-authored-by: sivanova <[email protected]> Co-authored-by: Silvia Ivanova <[email protected]> Co-authored-by: Maya <[email protected]>
1 parent 09b07bf commit 2f78deb

File tree

60 files changed

+7331
-136
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+7331
-136
lines changed

package-lock.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/calendar/helpers.ts

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
last,
77
modulo,
88
} from '../common/util.js';
9+
import type { DateRangeValue } from '../date-range-picker/date-range-picker.js';
910
import {
1011
CalendarDay,
1112
type CalendarRangeParams,
@@ -71,6 +72,33 @@ export function convertToDate(value?: Date | string | null): Date | null {
7172
return isString(value) ? parseISODate(value) : isValidDate(value);
7273
}
7374

75+
/**
76+
* Converts the given value to a DateRangeValue object.
77+
*
78+
* If the value is already a valid DateRangeValue object, it is returned directly.
79+
* If the value is a string, it is parsed to object and returned if it fields are valid dates.
80+
* If the value is null or undefined, null is returned.
81+
* If the parsing fails, null is returned.
82+
*/
83+
export function convertToDateRange(
84+
value?: DateRangeValue | string | null
85+
): DateRangeValue | null {
86+
if (!value) {
87+
return null;
88+
}
89+
90+
if (isString(value)) {
91+
const obj = JSON.parse(value);
92+
const start = convertToDate(obj.start);
93+
const end = convertToDate(obj.end);
94+
return {
95+
start: start ? CalendarDay.from(start).native : null,
96+
end: end ? CalendarDay.from(end).native : null,
97+
};
98+
}
99+
return value;
100+
}
101+
74102
/**
75103
* Converts a Date object to an ISO 8601 string.
76104
*
@@ -136,26 +164,41 @@ export function isPreviousMonth(target: DayParameter, origin: DayParameter) {
136164
}
137165

138166
/**
139-
* Returns a generator yielding day values between `start` and `end` (non-inclusive)
167+
* Returns a generator yielding day values between `start` and `end` (non-inclusive by default)
140168
* by a given `unit` as a step.
169+
* To include the end date set the `inclusive` option to true.
141170
*
142171
* @remarks
143172
* By default, `unit` is set to 'day'.
144173
*/
145-
export function* calendarRange(options: CalendarRangeParams) {
146-
let low = toCalendarDay(options.start);
147-
const unit = options.unit ?? 'day';
148-
const high =
149-
typeof options.end === 'number'
150-
? low.add(unit, options.end)
151-
: toCalendarDay(options.end);
152-
153-
const reverse = high.lessThan(low);
154-
const step = reverse ? -1 : 1;
155-
156-
while (!reverse ? low.lessThan(high) : low.greaterThan(high)) {
157-
yield low;
158-
low = low.add(unit, step);
174+
export function* calendarRange(
175+
options: CalendarRangeParams
176+
): Generator<CalendarDay, void, unknown> {
177+
const { start, end, unit = 'day', inclusive = false } = options;
178+
179+
let currentDate = toCalendarDay(start);
180+
const endDate =
181+
typeof end === 'number'
182+
? toCalendarDay(start).add(unit, end)
183+
: toCalendarDay(end);
184+
185+
const isReversed = endDate.lessThan(currentDate);
186+
const step = isReversed ? -1 : 1;
187+
188+
const shouldContinue = () => {
189+
if (inclusive) {
190+
return isReversed
191+
? currentDate.greaterThanOrEqual(endDate)
192+
: currentDate.lessThanOrEqual(endDate);
193+
}
194+
return isReversed
195+
? currentDate.greaterThan(endDate)
196+
: currentDate.lessThan(endDate);
197+
};
198+
199+
while (shouldContinue()) {
200+
yield currentDate;
201+
currentDate = currentDate.add(unit, step);
159202
}
160203
}
161204

src/components/calendar/model.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type CalendarRangeParams = {
44
start: DayParameter;
55
end: DayParameter | number;
66
unit?: DayInterval;
7+
inclusive?: boolean;
78
};
89

910
type DayInterval = 'year' | 'quarter' | 'month' | 'week' | 'day';
@@ -43,6 +44,26 @@ export class CalendarDay {
4344
});
4445
}
4546

47+
/**
48+
* Compares the date portion of two date objects.
49+
*
50+
* @returns
51+
* ```
52+
* first === second // 0
53+
* first > second // 1
54+
* first < second // -1
55+
* ```
56+
*/
57+
public static compare(first: DayParameter, second: DayParameter) {
58+
const a = toCalendarDay(first);
59+
const b = toCalendarDay(second);
60+
61+
if (a.equalTo(b)) {
62+
return 0;
63+
}
64+
return a.greaterThan(b) ? 1 : -1;
65+
}
66+
4667
constructor(args: CalendarDayParams) {
4768
this._date = new Date(args.year, args.month, args.date ?? 1);
4869
}
@@ -142,10 +163,18 @@ export class CalendarDay {
142163
return this.timestamp > toCalendarDay(value).timestamp;
143164
}
144165

166+
public greaterThanOrEqual(value: DayParameter) {
167+
return this.timestamp >= toCalendarDay(value).timestamp;
168+
}
169+
145170
public lessThan(value: DayParameter) {
146171
return this.timestamp < toCalendarDay(value).timestamp;
147172
}
148173

174+
public lessThanOrEqual(value: DayParameter) {
175+
return this.timestamp <= toCalendarDay(value).timestamp;
176+
}
177+
149178
public toString() {
150179
return `${this.native}`;
151180
}

src/components/common/definitions/defineAllComponents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import IgcSwitchComponent from '../../checkbox/switch.js';
2020
import IgcChipComponent from '../../chip/chip.js';
2121
import IgcComboComponent from '../../combo/combo.js';
2222
import IgcDatePickerComponent from '../../date-picker/date-picker.js';
23+
import IgcDateRangePickerComponent from '../../date-range-picker/date-range-picker.js';
2324
import IgcDateTimeInputComponent from '../../date-time-input/date-time-input.js';
2425
import IgcDialogComponent from '../../dialog/dialog.js';
2526
import IgcDividerComponent from '../../divider/divider.js';
@@ -90,6 +91,7 @@ const allComponents: IgniteComponent[] = [
9091
IgcChipComponent,
9192
IgcComboComponent,
9293
IgcDatePickerComponent,
94+
IgcDateRangePickerComponent,
9395
IgcDropdownComponent,
9496
IgcDropdownGroupComponent,
9597
IgcDropdownHeaderComponent,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {
2+
IgcCalendarResourceStringEN,
3+
type IgcCalendarResourceStrings,
4+
} from './calendar.resources.js';
5+
6+
/* blazorSuppress */
7+
export interface IgcDateRangePickerResourceStrings
8+
extends IgcCalendarResourceStrings {
9+
separator: string;
10+
done: string;
11+
cancel: string;
12+
last7Days: string;
13+
last30Days: string;
14+
currentMonth: string;
15+
yearToDate: string;
16+
}
17+
18+
export const IgcDateRangePickerResourceStringsEN: IgcDateRangePickerResourceStrings =
19+
{
20+
separator: 'to',
21+
done: 'Done',
22+
cancel: 'Cancel',
23+
last7Days: 'Last 7 days',
24+
last30Days: 'Last 30 days',
25+
currentMonth: 'Current month',
26+
yearToDate: 'Year to date',
27+
...IgcCalendarResourceStringEN,
28+
};

src/components/common/mixins/forms/form-value.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { convertToDate, getDateFormValue } from '../../../calendar/helpers.js';
1+
import {
2+
convertToDate,
3+
convertToDateRange,
4+
getDateFormValue,
5+
} from '../../../calendar/helpers.js';
6+
import type { DateRangeValue } from '../../../date-range-picker/date-range-picker.js';
27
import { asNumber } from '../../util.js';
38
import type { FormValueType, IgcFormControl } from './types.js';
49

@@ -78,6 +83,48 @@ export const defaultFileListTransformer: Partial<
7883
},
7984
};
8085

86+
/**
87+
* Converts a DateDateRangeValue object to FormData with
88+
* start and end Date values as ISO 8601 strings.
89+
* The keys are prefixed with the host name
90+
* and suffixed with 'start' or 'end' accordingly.
91+
* In case the host does not have a name, it does not participate in form submission.
92+
*
93+
* If the date values are null or undefined, the form data values
94+
* are empty strings ''.
95+
*/
96+
export function getDateRangeFormValue(
97+
value: DateRangeValue | null,
98+
host: IgcFormControl
99+
): FormValueType {
100+
if (!host.name) {
101+
return null;
102+
}
103+
104+
const start = value?.start?.toISOString();
105+
const end = value?.end?.toISOString();
106+
107+
const fd = new FormData();
108+
const prefix = `${host.name}-`;
109+
110+
if (start) {
111+
fd.append(`${prefix}start`, start);
112+
}
113+
if (end) {
114+
fd.append(`${prefix}end`, end);
115+
}
116+
117+
return fd;
118+
}
119+
120+
export const defaultDateRangeTransformers: Partial<
121+
FormValueTransformers<DateRangeValue | null>
122+
> = {
123+
setValue: convertToDateRange,
124+
setDefaultValue: convertToDateRange,
125+
setFormValue: getDateRangeFormValue,
126+
};
127+
81128
/* blazorSuppress */
82129
export class FormValue<T> {
83130
private static readonly setFormValueKey = '_setFormValue' as const;

src/components/common/utils.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from '@open-wc/testing';
88
import type { TemplateResult } from 'lit';
99

10+
import { type CalendarDay, toCalendarDay } from '../calendar/model.js';
1011
import IgcValidationContainerComponent from '../validation-container/validation-container.js';
1112
import { parseKeys } from './controllers/key-bindings.js';
1213
import type { IgniteComponent } from './definitions/register.js';
@@ -146,6 +147,18 @@ class FormAssociatedTestBed<T extends IgcFormControl> {
146147
expect(this.submit().getAll(this.element.name), msg).to.eql(value);
147148
}
148149

150+
/**
151+
* Whether the form is submitted and contains the given 'key'-'value' pair
152+
* in its form data.
153+
*/
154+
public assertSubmitHasKeyValue = (
155+
key: string,
156+
value: unknown,
157+
msg?: string
158+
) => {
159+
expect(this.submit().get(key), msg).to.eql(value);
160+
};
161+
149162
/**
150163
* Whether the form fails to submit.
151164
* The component will be in invalid state and the form data will be empty.
@@ -522,3 +535,10 @@ export function compareStyles(
522535
([key, value]) => computed.getPropertyValue(toKebabCase(key)) === value
523536
);
524537
}
538+
539+
/**
540+
* Compares two date values
541+
*/
542+
export function checkDatesEqual(a: CalendarDay | Date, b: CalendarDay | Date) {
543+
expect(toCalendarDay(a).equalTo(toCalendarDay(b))).to.be.true;
544+
}

src/components/common/validators.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DateTimeUtil } from '../date-time-input/date-util.js';
1+
import { CalendarDay } from '../calendar/model.js';
22
import validatorMessages from './localization/validation-en.js';
33
import {
44
asNumber,
@@ -135,9 +135,7 @@ export const minDateValidator: Validator<{
135135
key: 'rangeUnderflow',
136136
message: ({ min }) => formatString(validatorMessages.min, min),
137137
isValid: ({ value, min }) =>
138-
value && min
139-
? !DateTimeUtil.lessThanMinValue(value, min, false, true)
140-
: true,
138+
value && min ? CalendarDay.compare(value, min) >= 0 : true,
141139
};
142140

143141
export const maxDateValidator: Validator<{
@@ -147,7 +145,5 @@ export const maxDateValidator: Validator<{
147145
key: 'rangeOverflow',
148146
message: ({ max }) => formatString(validatorMessages.max, max),
149147
isValid: ({ value, max }) =>
150-
value && max
151-
? !DateTimeUtil.greaterThanMaxValue(value, max, false, true)
152-
: true,
148+
value && max ? CalendarDay.compare(value, max) <= 0 : true,
153149
};

src/components/date-picker/date-picker.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
isEmpty,
4242
} from '../common/util.js';
4343
import IgcDateTimeInputComponent from '../date-time-input/date-time-input.js';
44-
import type { DatePart } from '../date-time-input/date-util.js';
44+
import { type DatePart, DateTimeUtil } from '../date-time-input/date-util.js';
4545
import IgcDialogComponent from '../dialog/dialog.js';
4646
import IgcFocusTrapComponent from '../focus-trap/focus-trap.js';
4747
import IgcIconComponent from '../icon/icon.js';
@@ -67,8 +67,6 @@ export interface IgcDatePickerComponentEventMap {
6767
igcInput: CustomEvent<Date>;
6868
}
6969

70-
const formats = new Set(['short', 'medium', 'long', 'full']);
71-
7270
/* blazorIndirectRender */
7371
/* blazorSupportsVisualChildren */
7472
/**
@@ -658,9 +656,8 @@ export default class IgcDatePickerComponent extends FormAssociatedRequiredMixin(
658656
//#region Render methods
659657

660658
private _renderClearIcon() {
661-
return !this.value
662-
? nothing
663-
: html`
659+
return this.value
660+
? html`
664661
<span
665662
slot="suffix"
666663
part="clear-icon"
@@ -674,7 +671,8 @@ export default class IgcDatePickerComponent extends FormAssociatedRequiredMixin(
674671
></igc-icon>
675672
</slot>
676673
</span>
677-
`;
674+
`
675+
: nothing;
678676
}
679677

680678
private _renderCalendarIcon() {
@@ -801,9 +799,9 @@ export default class IgcDatePickerComponent extends FormAssociatedRequiredMixin(
801799
}
802800

803801
protected _renderInput(id: string) {
804-
const format = formats.has(this._displayFormat!)
805-
? `${this._displayFormat}Date`
806-
: this._displayFormat;
802+
const format = DateTimeUtil.predefinedToDateDisplayFormat(
803+
this._displayFormat!
804+
);
807805

808806
// Dialog mode is always readonly, rest depends on configuration
809807
const readOnly = !this._isDropDown || this.readOnly || this.nonEditable;
@@ -848,10 +846,9 @@ export default class IgcDatePickerComponent extends FormAssociatedRequiredMixin(
848846
const id = this.id || this._inputId;
849847

850848
return html`
851-
${!this._isMaterialTheme ? this._renderLabel(id) : nothing}
852-
${this._renderInput(id)}${this._renderPicker(
853-
id
854-
)}${this._renderHelperText()}
849+
${this._isMaterialTheme ? nothing : this._renderLabel(id)}
850+
${this._renderInput(id)} ${this._renderPicker(id)}
851+
${this._renderHelperText()}
855852
`;
856853
}
857854

0 commit comments

Comments
 (0)