@trackunit/react-date-and-time-components 1.3.222 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs.js +419 -73
- package/index.esm.js +417 -75
- package/package.json +13 -12
- package/src/DayPicker/DayRangePicker.d.ts +7 -14
- package/src/DayPicker/DayRangePicker.stories.d.ts +0 -1
- package/src/DayPicker/index.d.ts +0 -1
- package/src/DayRangeSelect/DayRangeSelect.d.ts +56 -0
- package/src/DayRangeSelect/DayRangeSelect.stories.d.ts +10 -0
- package/src/DayRangeSelect/DayRangeSelect.variants.d.ts +3 -0
- package/src/DayRangeSelect/components/DayRangeSelectOptions.d.ts +11 -0
- package/src/DayRangeSelect/hooks/useCalculateCustomDateRangeDisabledDays/useCalculateCustomDateRangeDisabledDays.d.ts +11 -0
- package/src/DayRangeSelect/hooks/useCalculateDateRange/useCalculateDateRange.d.ts +9 -0
- package/src/DayRangeSelect/hooks/useFilterTemporalPeriodsInRange/useFilterTemporalPeriodsInRange.d.ts +18 -0
- package/src/DayRangeSelect/hooks/useFormatTemporalPeriodLabel/useFormatTemporalPeriodLabel.d.ts +9 -0
- package/src/DayRangeSelect/hooks/useParseTemporalPeriodFromText/hooks/useFindTemporalDirection.d.ts +5 -0
- package/src/DayRangeSelect/hooks/useParseTemporalPeriodFromText/hooks/useFindTemporalUnit.d.ts +5 -0
- package/src/DayRangeSelect/hooks/useParseTemporalPeriodFromText/useParseTemporalPeriodFromText.d.ts +10 -0
- package/src/DayRangeSelect/index.d.ts +6 -0
- package/src/DayRangeSelect/types.d.ts +16 -0
- package/src/DayRangeSelect/utils/createTemporalPeriodCombinations/createTemporalPeriodCombinations.d.ts +12 -0
- package/src/DayRangeSelect/utils/formatCustomDateRangeLabel/formatCustomDateRangeLabel.d.ts +8 -0
- package/src/DayRangeSelect/utils/isTemporalPeriod/isTemporalPeriod.d.ts +8 -0
- package/src/DayRangeSelect/utils/temporalUnitMappings.d.ts +3 -0
- package/src/index.d.ts +1 -0
- package/src/translation.d.ts +2 -2
- package/src/DayPicker/DayRangePickerPopover.d.ts +0 -45
- package/src/DayPicker/DayRangePickerPopover.stories.d.ts +0 -10
package/index.cjs.js
CHANGED
|
@@ -11,15 +11,31 @@ var sharedUtils = require('@trackunit/shared-utils');
|
|
|
11
11
|
var reactDayPicker = require('react-day-picker');
|
|
12
12
|
var tailwindMerge = require('tailwind-merge');
|
|
13
13
|
var locale = require('date-fns/locale');
|
|
14
|
-
var
|
|
14
|
+
var reactFormComponents = require('@trackunit/react-form-components');
|
|
15
15
|
var cssClassVarianceUtilities = require('@trackunit/css-class-variance-utilities');
|
|
16
|
+
var stringTs = require('string-ts');
|
|
16
17
|
|
|
17
18
|
var defaultTranslations = {
|
|
18
19
|
"dateTime.instant.now": "Now",
|
|
19
|
-
"
|
|
20
|
+
"input.combined": "{{direction}} {{count}} {{unit}}",
|
|
21
|
+
"input.direction.last": "Last",
|
|
22
|
+
"input.direction.next": "Next",
|
|
23
|
+
"input.icon.tooltip.calendar": "Calendar",
|
|
24
|
+
"input.noOptions": "No options available",
|
|
25
|
+
"input.placeholder.customRange.last": "Type a date range (e.g., “Last 4 days”)",
|
|
26
|
+
"input.placeholder.customRange.next": "Type a date range (e.g., “Next 4 days”)",
|
|
27
|
+
"input.today": "Today",
|
|
28
|
+
"input.unit.day": "day",
|
|
29
|
+
"input.unit.days": "days",
|
|
30
|
+
"input.unit.month": "month",
|
|
31
|
+
"input.unit.months": "months",
|
|
32
|
+
"input.unit.week": "week",
|
|
33
|
+
"input.unit.weeks": "weeks",
|
|
20
34
|
"layout.actions.apply": "Apply",
|
|
35
|
+
"layout.actions.back": "Back",
|
|
21
36
|
"layout.actions.cancel": "Cancel",
|
|
22
37
|
"layout.actions.clear": "Clear",
|
|
38
|
+
"layout.actions.reset": "Reset",
|
|
23
39
|
"shared.timePeriods.days": "{{count}} day",
|
|
24
40
|
"shared.timePeriods.days_plural": "{{count}} days",
|
|
25
41
|
"shared.timePeriods.hours": "{{count}} hour",
|
|
@@ -29,7 +45,9 @@ var defaultTranslations = {
|
|
|
29
45
|
"shared.timePeriods.today": "Today",
|
|
30
46
|
"shared.timePeriods.weeks": "{{count}} week",
|
|
31
47
|
"shared.timePeriods.weeks_plural": "{{count}} weeks",
|
|
32
|
-
"timeline.loadMore": "Click for more..."
|
|
48
|
+
"timeline.loadMore": "Click for more...",
|
|
49
|
+
"trigger.customRange": "Custom date range",
|
|
50
|
+
"trigger.selectDateRange": "Select date range"
|
|
33
51
|
};
|
|
34
52
|
|
|
35
53
|
/** The translation namespace for this library */
|
|
@@ -169,7 +187,7 @@ const DayPicker = ({ onDaySelect, disabledDays, selectedDays, language, classNam
|
|
|
169
187
|
* @param {DayRangePickerProps} props - The props for the DayRangePicker component
|
|
170
188
|
* @returns {ReactElement} DayRangePicker component
|
|
171
189
|
*/
|
|
172
|
-
const DayRangePicker = ({ onRangeSelect, selectedDays, disabledDays, dataTestId, language, className, max,
|
|
190
|
+
const DayRangePicker = ({ onRangeSelect, selectedDays, disabledDays, dataTestId, language, className, max, timezone, cancelButtonLabel, onClose, }) => {
|
|
173
191
|
const [newRange, setNewRange] = react.useState(selectedDays ?? { from: undefined, to: undefined });
|
|
174
192
|
const [t] = useTranslation();
|
|
175
193
|
const locale = useLocale(language);
|
|
@@ -204,91 +222,415 @@ const DayRangePicker = ({ onRangeSelect, selectedDays, disabledDays, dataTestId,
|
|
|
204
222
|
onRangeSelect && onRangeSelect(newRange);
|
|
205
223
|
onClose && onClose();
|
|
206
224
|
}, [onRangeSelect, newRange, onClose]);
|
|
207
|
-
return (jsxRuntime.jsxs("div", { className: "
|
|
208
|
-
calcQuickOptions(showQuickOptions.dataRetention).map(option => (jsxRuntime.jsx(QuickOptionButton, { onClick: handleOnRangeSelect, option: option, timezone: timezone }, `${option.unit}${option.last}${option.includeExtraDay ? "-include-extra-day" : ""}`))) }), !!onClose && (jsxRuntime.jsx("div", { className: "flex-grow text-right", children: jsxRuntime.jsx(reactComponents.IconButton, { dataTestId: "close-button", icon: jsxRuntime.jsx(reactComponents.Icon, { name: "XMark", size: "small" }), onClick: () => handleApply(), variant: "ghost-neutral" }) }))] }), jsxRuntime.jsx(reactDayPicker.DayPicker, { className: tailwindMerge.twMerge("custom-day-picker", "range-picker", className), defaultMonth: selectedDays ? selectedDays.from : undefined, disabled: disabledDays, footer: jsxRuntime.jsx("div", { "data-testid": dataTestId }), locale: locale, max: max, mode: "range", onSelect: handleOnRangeSelect, selected: newRange }), jsxRuntime.jsxs("div", { className: "flex w-full gap-2", children: [jsxRuntime.jsx(reactComponents.Button, { className: "mr-auto", dataTestId: "range-popover-clear-button", onClick: clearSelectedDays, variant: "secondary", children: t("layout.actions.clear") }), jsxRuntime.jsx(reactComponents.Button, { dataTestId: "range-popover-cancel-button", onClick: () => handleCancel(), variant: "secondary", children: t("layout.actions.cancel") }), jsxRuntime.jsx(reactComponents.Button, { dataTestId: "range-popover-apply-button", disabled: !newRange.from || !newRange.to, onClick: () => handleApply(), children: t("layout.actions.apply") })] })] }));
|
|
225
|
+
return (jsxRuntime.jsxs("div", { className: "flex w-min flex-col gap-4 p-2", children: [jsxRuntime.jsx(reactDayPicker.DayPicker, { className: tailwindMerge.twMerge("custom-day-picker", "range-picker", className, "p-0"), defaultMonth: selectedDays ? selectedDays.from : undefined, disabled: disabledDays, footer: jsxRuntime.jsx("div", { "data-testid": dataTestId }), locale: locale, max: max, mode: "range", onSelect: handleOnRangeSelect, selected: newRange }), jsxRuntime.jsxs("div", { className: "flex w-full gap-2", children: [jsxRuntime.jsx(reactComponents.Button, { className: "mr-auto", dataTestId: "range-popover-clear-button", onClick: clearSelectedDays, variant: "secondary", children: t("layout.actions.clear") }), jsxRuntime.jsx(reactComponents.Button, { dataTestId: "range-popover-cancel-button", onClick: () => handleCancel(), variant: "ghost-neutral", children: cancelButtonLabel ? cancelButtonLabel : t("layout.actions.cancel") }), jsxRuntime.jsx(reactComponents.Button, { dataTestId: "range-popover-apply-button", disabled: !newRange.from || !newRange.to, onClick: () => handleApply(), children: t("layout.actions.apply") })] })] }));
|
|
209
226
|
};
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
227
|
+
|
|
228
|
+
const temporalArithmeticTypeMapping = {
|
|
229
|
+
day: "days",
|
|
230
|
+
days: "days",
|
|
231
|
+
week: "weeks",
|
|
232
|
+
weeks: "weeks",
|
|
233
|
+
month: "months",
|
|
234
|
+
months: "months",
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Calculate a date range based on a temporal period.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* getDataRange({ direction: "next", count: 1, unit: "month" }); // { from: 2025-06-01, to: 2025-07-01 }
|
|
242
|
+
* getDataRange({ direction: "last", count: 2, unit: "weeks" }); // { from: 2025-05-24, to: 2025-06-01 }
|
|
243
|
+
*/
|
|
244
|
+
const useCalculateDateRange = () => {
|
|
245
|
+
const { nowDate, startOf, endOf, subtract, add } = reactDateAndTimeHooks.useDateAndTime();
|
|
246
|
+
return react.useCallback(({ direction, count, unit }) => {
|
|
247
|
+
const unitType = temporalArithmeticTypeMapping[unit];
|
|
248
|
+
if (direction === "next") {
|
|
249
|
+
// To get the right number of days, calculate the end date and subtract 1 day
|
|
250
|
+
const endDate = add(nowDate, count, unitType);
|
|
251
|
+
const adjustedEndDate = subtract(endDate, 1, "days");
|
|
252
|
+
return {
|
|
253
|
+
from: startOf(nowDate, "day"),
|
|
254
|
+
to: endOf(adjustedEndDate, "day"),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// To get the right number of days, calculate the start date and add 1 day
|
|
259
|
+
const startDate = subtract(nowDate, count, unitType);
|
|
260
|
+
const adjustedStartDate = add(startDate, 1, "days");
|
|
261
|
+
return {
|
|
262
|
+
from: startOf(adjustedStartDate, "day"),
|
|
263
|
+
to: endOf(nowDate, "day"),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}, [nowDate, startOf, endOf, subtract, add]);
|
|
222
267
|
};
|
|
223
|
-
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Format a temporal period into a label.
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* formatTemporalPeriodLabel({ direction: "last", count: 2, unit: "days" }); // Last 2 days
|
|
274
|
+
* formatTemporalPeriodLabel({ direction: "next", count: 1, unit: "week" }); // Next week
|
|
275
|
+
*/
|
|
276
|
+
const useFormatTemporalPeriodLabel = () => {
|
|
277
|
+
const [t] = useTranslation();
|
|
278
|
+
return react.useCallback((option) => {
|
|
279
|
+
// Return "Today" for "1 day" no matter the direction as in the current implementation it's the same day.
|
|
280
|
+
if (option.count === 1 && option.unit === "day") {
|
|
281
|
+
return t("input.today");
|
|
282
|
+
}
|
|
283
|
+
return t("input.combined", {
|
|
284
|
+
direction: t(`input.direction.${option.direction === "next" ? "next" : "last"}`),
|
|
285
|
+
count: option.count > 1 ? option.count : undefined,
|
|
286
|
+
unit: t(`input.unit.${option.unit}`),
|
|
287
|
+
});
|
|
288
|
+
}, [t]);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Renders a list of options for the DayRangeSelect component.
|
|
293
|
+
*/
|
|
294
|
+
const DayRangeSelectOptions = ({ dataTestId, temporalPeriods, selectedDateRange, onSelect, }) => {
|
|
224
295
|
const [t] = useTranslation();
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
296
|
+
const formatTemporalPeriodLabel = useFormatTemporalPeriodLabel();
|
|
297
|
+
const calculateDateRange = useCalculateDateRange();
|
|
298
|
+
const handleSelect = react.useCallback((index) => {
|
|
299
|
+
const temporalPeriod = temporalPeriods[index];
|
|
300
|
+
if (!temporalPeriod) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
onSelect({
|
|
304
|
+
dateRange: calculateDateRange(temporalPeriod),
|
|
305
|
+
temporalPeriod: temporalPeriod,
|
|
231
306
|
});
|
|
232
|
-
}, [
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
case "days":
|
|
237
|
-
case "months":
|
|
238
|
-
case "weeks":
|
|
239
|
-
return `shared.timePeriods.${unit}`;
|
|
240
|
-
default:
|
|
241
|
-
return null;
|
|
307
|
+
}, [calculateDateRange, onSelect, temporalPeriods]);
|
|
308
|
+
const options = react.useMemo(() => {
|
|
309
|
+
if (!temporalPeriods.length) {
|
|
310
|
+
return (jsxRuntime.jsx(reactComponents.MenuItem, { dataTestId: dataTestId ? `${dataTestId}-no-options` : undefined, disabled: true, label: t("input.noOptions") }));
|
|
242
311
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
312
|
+
return temporalPeriods.map((temporalPeriod, index) => {
|
|
313
|
+
// NOTE: Check for temporalPeriod as well to make sure not to select an options when it's a custom date range selection
|
|
314
|
+
const isSelected = selectedDateRange &&
|
|
315
|
+
selectedDateRange.temporalPeriod &&
|
|
316
|
+
stringifyTemporalPeriod(selectedDateRange.temporalPeriod) === stringifyTemporalPeriod(temporalPeriod);
|
|
317
|
+
return (jsxRuntime.jsx(reactComponents.MenuItem, { dataTestId: dataTestId ? `${dataTestId}-${index.toString()}` : undefined, label: formatTemporalPeriodLabel(temporalPeriod), onClick: () => handleSelect(index), selected: isSelected, suffix: isSelected ? jsxRuntime.jsx(reactComponents.Icon, { color: "primary", name: "Check", size: "small" }) : null }, index));
|
|
318
|
+
});
|
|
319
|
+
}, [temporalPeriods, selectedDateRange, formatTemporalPeriodLabel, handleSelect, dataTestId, t]);
|
|
320
|
+
return options;
|
|
321
|
+
};
|
|
322
|
+
const stringifyTemporalPeriod = (temporalPeriod) => `${temporalPeriod.direction}-${temporalPeriod.count}-${temporalPeriod.unit}`;
|
|
323
|
+
|
|
324
|
+
const cvaTriggerButton = cssClassVarianceUtilities.cvaMerge(["justify-start"], {
|
|
325
|
+
variants: {
|
|
326
|
+
active: {
|
|
327
|
+
true: "text-primary-600 hover:text-primary-700 active:text-primary-800 focus:text-primary-800",
|
|
328
|
+
false: "",
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Calculate the disabled days for the custom date range picker based on the maxDaysInRange and allowedDirection.
|
|
335
|
+
* The range is inclusive of both start and end dates, so we subtract 1 from maxDaysInRange to get the correct number of days.
|
|
336
|
+
*/
|
|
337
|
+
const useCalculateCustomDateRangeDisabledDays = () => {
|
|
338
|
+
const { subtract, add, nowDate } = reactDateAndTimeHooks.useDateAndTime();
|
|
339
|
+
return react.useCallback(({ maxDaysInRange, allowedDirection, }) => {
|
|
340
|
+
if (!allowedDirection) {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
if (!sharedUtils.nonNullable(maxDaysInRange)) {
|
|
344
|
+
if (allowedDirection === "last") {
|
|
345
|
+
return {
|
|
346
|
+
after: nowDate,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
return {
|
|
351
|
+
before: nowDate,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
247
354
|
}
|
|
248
|
-
|
|
249
|
-
|
|
355
|
+
// Subtract 1 from maxDaysInRange since the range is inclusive of both start and end dates
|
|
356
|
+
const adjustedMaxDaysInRangeCount = maxDaysInRange - 1;
|
|
357
|
+
if (allowedDirection === "last") {
|
|
358
|
+
return {
|
|
359
|
+
before: subtract(nowDate, adjustedMaxDaysInRangeCount, "days"),
|
|
360
|
+
after: nowDate,
|
|
361
|
+
};
|
|
250
362
|
}
|
|
251
363
|
else {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
})
|
|
257
|
-
: null;
|
|
364
|
+
return {
|
|
365
|
+
before: nowDate,
|
|
366
|
+
after: add(nowDate, adjustedMaxDaysInRangeCount, "days"),
|
|
367
|
+
};
|
|
258
368
|
}
|
|
259
|
-
};
|
|
260
|
-
|
|
369
|
+
}, [nowDate, subtract, add]);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Filter temporal periods to only include those within a max day count.
|
|
374
|
+
*
|
|
375
|
+
* @example
|
|
376
|
+
* getTemporalPeriodsInRange({
|
|
377
|
+
* temporalPeriods: [
|
|
378
|
+
* { direction: "next", count: 1, unit: "week" },
|
|
379
|
+
* { direction: "next", count: 1, unit: "month" },
|
|
380
|
+
* ],
|
|
381
|
+
* maxDaysInRange: 7,
|
|
382
|
+
* });
|
|
383
|
+
* ); // [{ direction: "next", count: 1, unit: "week" }]
|
|
384
|
+
*/
|
|
385
|
+
const useFilterTemporalPeriodsInRange = () => {
|
|
386
|
+
const { difference } = reactDateAndTimeHooks.useDateAndTime();
|
|
387
|
+
const calculateDateRange = useCalculateDateRange();
|
|
388
|
+
const getDayCountInRange = react.useCallback((temporalPeriod) => {
|
|
389
|
+
const { from, to } = calculateDateRange(temporalPeriod);
|
|
390
|
+
return difference(from, to, "day");
|
|
391
|
+
}, [calculateDateRange, difference]);
|
|
392
|
+
return react.useCallback(({ temporalPeriods, maxDaysInRange, }) => {
|
|
393
|
+
if (!maxDaysInRange) {
|
|
394
|
+
return temporalPeriods;
|
|
395
|
+
}
|
|
396
|
+
return temporalPeriods.filter(period => {
|
|
397
|
+
const daysInRange = getDayCountInRange(period);
|
|
398
|
+
return sharedUtils.nonNullable(daysInRange) && daysInRange <= maxDaysInRange;
|
|
399
|
+
});
|
|
400
|
+
}, [getDayCountInRange]);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Find the TemporalDirection type matching the value.
|
|
405
|
+
*/
|
|
406
|
+
const useFindTemporalDirection = () => {
|
|
407
|
+
const [t] = useTranslation();
|
|
408
|
+
const temporalDirectionsMap = react.useMemo(() => ({
|
|
409
|
+
last: t("input.direction.last"),
|
|
410
|
+
next: t("input.direction.next"),
|
|
411
|
+
}), [t]);
|
|
412
|
+
return react.useCallback((value) => {
|
|
413
|
+
if (!value) {
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
return sharedUtils.objectKeys(temporalDirectionsMap).find(direction => stringTs.lowerCase(temporalDirectionsMap[direction]) === stringTs.lowerCase(value));
|
|
417
|
+
}, [temporalDirectionsMap]);
|
|
261
418
|
};
|
|
262
419
|
|
|
263
420
|
/**
|
|
264
|
-
*
|
|
265
|
-
|
|
421
|
+
* Find the TemporalUnit type matching the value.
|
|
422
|
+
*/
|
|
423
|
+
const useFindTemporalUnit = () => {
|
|
424
|
+
const [t] = useTranslation();
|
|
425
|
+
const temporalUnitsMap = react.useMemo(() => ({
|
|
426
|
+
day: t("input.unit.day"),
|
|
427
|
+
days: t("input.unit.days"),
|
|
428
|
+
week: t("input.unit.week"),
|
|
429
|
+
weeks: t("input.unit.weeks"),
|
|
430
|
+
month: t("input.unit.month"),
|
|
431
|
+
months: t("input.unit.months"),
|
|
432
|
+
}), [t]);
|
|
433
|
+
return react.useCallback((value) => {
|
|
434
|
+
if (!value) {
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
return sharedUtils.objectKeys(temporalUnitsMap).find(unit => stringTs.lowerCase(temporalUnitsMap[unit]) === stringTs.lowerCase(value));
|
|
438
|
+
}, [temporalUnitsMap]);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Parse a text string into a temporal period object.
|
|
266
443
|
*
|
|
267
|
-
* @
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* @param props.className custom className to be added to the component
|
|
272
|
-
* @param props.dataTestId id used for tests
|
|
273
|
-
* @param props.onRangeSelect hook function to handle applied date range
|
|
274
|
-
* @param props.variant popover button variant
|
|
275
|
-
* @param props.size popover button size
|
|
276
|
-
* @param props.placement popover button placement
|
|
277
|
-
* @param props.timezone timezone for DayRangePicker
|
|
278
|
-
* @param props.fullwidth popover button fill width
|
|
279
|
-
* @param props.buttonContent popover button content
|
|
280
|
-
* @param props.max The maximum amount of days that can be selected
|
|
281
|
-
* @param props.disabled disabling popover button
|
|
444
|
+
* @example
|
|
445
|
+
* parseTemporalPeriodFromText("Last 2 days"); // { direction: "last", count: 2, unit: "days" }
|
|
446
|
+
* parseTemporalPeriodFromText("next 3"); // { direction: "next", count: 3 }
|
|
447
|
+
* parseTemporalPeriodFromText("1 month"); // {count: 1, unit: "month" }
|
|
282
448
|
*/
|
|
283
|
-
const
|
|
284
|
-
const { language } = reactCoreHooks.useCurrentUserLanguage();
|
|
449
|
+
const useParseTemporalPeriodFromText = () => {
|
|
285
450
|
const [t] = useTranslation();
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
451
|
+
const findTemporalDirection = useFindTemporalDirection();
|
|
452
|
+
const findTemporalUnit = useFindTemporalUnit();
|
|
453
|
+
return react.useCallback((value) => {
|
|
454
|
+
const directionMatch = value.match(
|
|
455
|
+
// Matches any of the translated direction strings (next, last)
|
|
456
|
+
new RegExp(`${t("input.direction.next")}|${t("input.direction.last")}`, "i"))?.[0];
|
|
457
|
+
const countMatch = value.match(/\d+/)?.[0];
|
|
458
|
+
const unitMatch = value.match(
|
|
459
|
+
// Matches any of the translated time unit strings both plural and singular (days, day, weeks, week, months, month)
|
|
460
|
+
new RegExp(`${t("input.unit.days")}|${t("input.unit.day")}|${t("input.unit.weeks")}|${t("input.unit.week")}|${t("input.unit.months")}|${t("input.unit.month")}`, "i"))?.[0];
|
|
461
|
+
return {
|
|
462
|
+
direction: findTemporalDirection(directionMatch),
|
|
463
|
+
count: sharedUtils.nonNullable(countMatch) ? parseInt(countMatch) : undefined,
|
|
464
|
+
unit: findTemporalUnit(unitMatch),
|
|
465
|
+
};
|
|
466
|
+
}, [t, findTemporalDirection, findTemporalUnit]);
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const DEFAULT_DIRECTIONS = ["last", "next"];
|
|
470
|
+
const DEFAULT_COUNT = 1;
|
|
471
|
+
/**
|
|
472
|
+
* Generate all possible temporal period combinations for a given temporal period.
|
|
473
|
+
* If direction is not provided, both "last" and "next" directions are used.
|
|
474
|
+
* If count is not provided, the count defaults to 1.
|
|
475
|
+
* If unit is not provided, both "day", "week", and "month" are used in plural or singular form depending on the count.
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* createTemporalPeriodCombinations({ direction: "last", count: 2, unit: "days" }); // [{ direction: "last", count: 2, unit: "days" }]
|
|
479
|
+
* createTemporalPeriodCombinations({ direction: "next", count: 1 }); // [{ direction: "next", count: 1, unit: "day" }, { direction: "next", count: 1, unit: "week" }, { direction: "next", count: 1, unit: "month" }]
|
|
480
|
+
*/
|
|
481
|
+
const createTemporalPeriodCombinations = ({ direction, count, unit, }) => {
|
|
482
|
+
const directions = direction ? [direction] : DEFAULT_DIRECTIONS;
|
|
483
|
+
const isCountPlural = sharedUtils.nonNullable(count) ? count > 1 : false;
|
|
484
|
+
const units = unit
|
|
485
|
+
? [unit]
|
|
486
|
+
: isCountPlural
|
|
487
|
+
? ["days", "weeks", "months"]
|
|
488
|
+
: ["day", "week", "month"];
|
|
489
|
+
return (directions
|
|
490
|
+
.flatMap(_direction => {
|
|
491
|
+
return units.map((_unit) => {
|
|
492
|
+
return {
|
|
493
|
+
direction: _direction,
|
|
494
|
+
count: count ?? DEFAULT_COUNT,
|
|
495
|
+
unit: _unit,
|
|
496
|
+
};
|
|
497
|
+
});
|
|
498
|
+
})
|
|
499
|
+
// If temporal direction isn't provided, filter out either of the 1 day options as they are both rendered as "Today"
|
|
500
|
+
.filter((period, index, array) => {
|
|
501
|
+
const isOneDayPeriod = period.count === 1 && period.unit === "day";
|
|
502
|
+
if (!isOneDayPeriod) {
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
const hasEarlierOneDayPeriod = array
|
|
506
|
+
.slice(0, index)
|
|
507
|
+
.some(_period => _period.count === 1 && _period.unit === "day");
|
|
508
|
+
return !hasEarlierOneDayPeriod;
|
|
509
|
+
}));
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Format a label representing the provided date range.
|
|
514
|
+
*
|
|
515
|
+
* @example
|
|
516
|
+
* formatCustomDateRangeLabel({ from: new Date("2025-06-16"), to: new Date("2025-06-17") }); // "June 16, 2025 - June 17, 2025"
|
|
517
|
+
*/
|
|
518
|
+
const formatCustomDateRangeLabel = (dateRange) => {
|
|
519
|
+
return (dateAndTimeUtils.formatDateUtil(dateRange.from, { dateFormat: "medium", selectFormat: "dateOnly" }) +
|
|
520
|
+
" - " +
|
|
521
|
+
dateAndTimeUtils.formatDateUtil(dateRange.to, { dateFormat: "medium", selectFormat: "dateOnly" }));
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Check if a value is a temporal period.
|
|
526
|
+
*
|
|
527
|
+
* @param value - The value to check.
|
|
528
|
+
* @returns {boolean} - Whether the value is a temporal period.
|
|
529
|
+
*/
|
|
530
|
+
const isTemporalPeriod = (value) => !!value && typeof value === "object" && "direction" in value && "count" in value && "unit" in value;
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Day range select component.
|
|
534
|
+
*/
|
|
535
|
+
const DayRangeSelect = ({ className, dataTestId, disabled, selectedDateRange, onRangeSelect, allowedDirection = undefined, initialDateRangeOptions, showDateRangeSearch = true, showCustomDateRangeOption = true, timezone, maxDaysInRange, size = "medium", fullWidth = false, }) => {
|
|
536
|
+
const [t] = useTranslation();
|
|
537
|
+
const parseTemporalPeriodFromText = useParseTemporalPeriodFromText();
|
|
538
|
+
const filterTemporalPeriodsInRange = useFilterTemporalPeriodsInRange();
|
|
539
|
+
const formatTemporalPeriodLabel = useFormatTemporalPeriodLabel();
|
|
540
|
+
const calculateDateRange = useCalculateDateRange();
|
|
541
|
+
const calculateCustomDateRangeDisabledDays = useCalculateCustomDateRangeDisabledDays();
|
|
542
|
+
const customDateRangeDisabledDays = react.useMemo(() => calculateCustomDateRangeDisabledDays({ maxDaysInRange, allowedDirection }), [calculateCustomDateRangeDisabledDays, maxDaysInRange, allowedDirection]);
|
|
543
|
+
const defaultTemporalPeriods = react.useMemo(() => {
|
|
544
|
+
if (initialDateRangeOptions) {
|
|
545
|
+
return filterTemporalPeriodsInRange({ temporalPeriods: initialDateRangeOptions, maxDaysInRange });
|
|
546
|
+
}
|
|
547
|
+
const defaultCombinations = [
|
|
548
|
+
{ direction: allowedDirection ?? "last", count: 1, unit: "day" },
|
|
549
|
+
{ direction: allowedDirection ?? "last", count: 1, unit: "week" },
|
|
550
|
+
{ direction: allowedDirection ?? "last", count: 1, unit: "month" },
|
|
551
|
+
{ direction: allowedDirection ?? "last", count: 12, unit: "months" },
|
|
552
|
+
];
|
|
553
|
+
return filterTemporalPeriodsInRange({ temporalPeriods: defaultCombinations, maxDaysInRange });
|
|
554
|
+
}, [initialDateRangeOptions, filterTemporalPeriodsInRange, allowedDirection, maxDaysInRange]);
|
|
555
|
+
const [temporalPeriods, setTemporalPeriods] = react.useState(defaultTemporalPeriods);
|
|
556
|
+
const [dateRangeSearchValue, setDateRangeSearchValue] = react.useState();
|
|
557
|
+
const [selectedRange, setSelectedRange] = react.useState();
|
|
558
|
+
const currentSelectedRange = react.useMemo(() => {
|
|
559
|
+
// Internal selected range takes precedence
|
|
560
|
+
if (selectedRange) {
|
|
561
|
+
return selectedRange;
|
|
289
562
|
}
|
|
563
|
+
if (!selectedDateRange) {
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
// Renders the selected date range as a selected option
|
|
567
|
+
if (isTemporalPeriod(selectedDateRange)) {
|
|
568
|
+
return { dateRange: calculateDateRange(selectedDateRange), temporalPeriod: selectedDateRange };
|
|
569
|
+
}
|
|
570
|
+
// Renders the selected date range as a custom date range
|
|
571
|
+
return { dateRange: selectedDateRange, temporalPeriod: undefined };
|
|
572
|
+
}, [selectedRange, selectedDateRange, calculateDateRange]);
|
|
573
|
+
const isCustomDateRangeSelected = react.useMemo(() => currentSelectedRange && !currentSelectedRange.temporalPeriod, [currentSelectedRange]);
|
|
574
|
+
const [isCustomDateRangeOpen, setIsCustomDateRangeOpen] = react.useState(false);
|
|
575
|
+
const popoverTriggerLabel = react.useMemo(() => {
|
|
576
|
+
return currentSelectedRange
|
|
577
|
+
? currentSelectedRange.temporalPeriod
|
|
578
|
+
? formatTemporalPeriodLabel(currentSelectedRange.temporalPeriod)
|
|
579
|
+
: formatCustomDateRangeLabel(currentSelectedRange.dateRange)
|
|
580
|
+
: undefined;
|
|
581
|
+
}, [currentSelectedRange, formatTemporalPeriodLabel]);
|
|
582
|
+
const handleDateRangeSearch = react.useCallback((value) => {
|
|
583
|
+
setDateRangeSearchValue(value);
|
|
584
|
+
if (!sharedUtils.nonNullable(value) || value === "") {
|
|
585
|
+
setTemporalPeriods(defaultTemporalPeriods);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const temporalPeriod = parseTemporalPeriodFromText(value);
|
|
589
|
+
const uniqueCombinations = createTemporalPeriodCombinations({
|
|
590
|
+
...temporalPeriod,
|
|
591
|
+
direction: allowedDirection ?? temporalPeriod.direction,
|
|
592
|
+
});
|
|
593
|
+
const combinationsInRange = filterTemporalPeriodsInRange({
|
|
594
|
+
temporalPeriods: uniqueCombinations,
|
|
595
|
+
maxDaysInRange,
|
|
596
|
+
});
|
|
597
|
+
setTemporalPeriods(combinationsInRange);
|
|
598
|
+
}, [
|
|
599
|
+
parseTemporalPeriodFromText,
|
|
600
|
+
allowedDirection,
|
|
601
|
+
filterTemporalPeriodsInRange,
|
|
602
|
+
defaultTemporalPeriods,
|
|
603
|
+
maxDaysInRange,
|
|
604
|
+
]);
|
|
605
|
+
const handleReset = react.useCallback(() => {
|
|
606
|
+
setTemporalPeriods(defaultTemporalPeriods);
|
|
607
|
+
setSelectedRange(undefined);
|
|
608
|
+
onRangeSelect(undefined);
|
|
609
|
+
handleDateRangeSearch("");
|
|
610
|
+
}, [handleDateRangeSearch, onRangeSelect, defaultTemporalPeriods]);
|
|
611
|
+
const handleOptionsSelect = react.useCallback((value, closeMenu) => {
|
|
612
|
+
closeMenu();
|
|
613
|
+
setSelectedRange(value);
|
|
614
|
+
onRangeSelect(value);
|
|
290
615
|
}, [onRangeSelect]);
|
|
291
|
-
|
|
616
|
+
const handleCustomDateRangeSelect = react.useCallback((dateRange, closeMenu) => {
|
|
617
|
+
if (!dateRange || !dateRange.from || !dateRange.to) {
|
|
618
|
+
handleReset();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
closeMenu();
|
|
622
|
+
const _selectedDateRange = {
|
|
623
|
+
dateRange: { from: dateRange.from, to: dateRange.to },
|
|
624
|
+
temporalPeriod: undefined,
|
|
625
|
+
};
|
|
626
|
+
setSelectedRange(_selectedDateRange);
|
|
627
|
+
onRangeSelect(_selectedDateRange);
|
|
628
|
+
}, [onRangeSelect, handleReset]);
|
|
629
|
+
return (jsxRuntime.jsxs(reactComponents.Popover, { onOpenStateChange: state => !state && setIsCustomDateRangeOpen(false), placement: "bottom-start", children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx(reactComponents.Button, { className: cvaTriggerButton({ active: !!selectedDateRange, className }), dataTestId: dataTestId, disabled: disabled, fullWidth: fullWidth, prefix: jsxRuntime.jsx(reactComponents.Icon, { ariaLabel: t("input.icon.tooltip.calendar"), color: disabled ? "secondary" : selectedDateRange ? "primary" : undefined, name: "Calendar", size: size === "large" ? "medium" : "small" }), size: size, variant: "secondary", children: popoverTriggerLabel ?? t("trigger.selectDateRange") }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { dataTestId: dataTestId ? `${dataTestId}-popover-content` : undefined, children: closeMenu => {
|
|
630
|
+
return !isCustomDateRangeOpen ? (jsxRuntime.jsxs(reactComponents.MenuList, { className: "min-w-[280px]", children: [jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [showDateRangeSearch ? (jsxRuntime.jsx(reactFormComponents.Search, { className: "w-full", dataTestId: dataTestId ? `${dataTestId}-search-input` : undefined, onChange: event => handleDateRangeSearch(event.target.value), onClear: () => handleDateRangeSearch(""), placeholder: allowedDirection === "last"
|
|
631
|
+
? t("input.placeholder.customRange.last")
|
|
632
|
+
: t("input.placeholder.customRange.next"), value: dateRangeSearchValue })) : null, jsxRuntime.jsx(reactComponents.Button, { className: "ml-auto", dataTestId: dataTestId ? `${dataTestId}-reset-button` : undefined, disabled: !selectedDateRange, onClick: handleReset, size: "small", variant: "ghost", children: t("layout.actions.reset") })] }), jsxRuntime.jsx("hr", { className: "border-secondary-200 my-1", role: "separator" }), jsxRuntime.jsx(DayRangeSelectOptions, { dataTestId: dataTestId ? `${dataTestId}-options` : undefined, onSelect: value => handleOptionsSelect(value, closeMenu), selectedDateRange: currentSelectedRange, temporalPeriods: temporalPeriods }), showCustomDateRangeOption ? (jsxRuntime.jsxs("section", { children: [jsxRuntime.jsx(reactComponents.MenuDivider, {}), jsxRuntime.jsx(reactComponents.MenuItem, { dataTestId: dataTestId ? `${dataTestId}-custom-range` : undefined, label: t("trigger.customRange"), onClick: () => setIsCustomDateRangeOpen(true), selected: isCustomDateRangeSelected, suffix: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [isCustomDateRangeSelected ? jsxRuntime.jsx(reactComponents.Icon, { color: "primary", name: "Check", size: "small" }) : null, jsxRuntime.jsx(reactComponents.Icon, { name: "ChevronRight", size: "small" })] }) })] })) : null] })) : (jsxRuntime.jsx(reactComponents.Card, { children: jsxRuntime.jsx(DayRangePicker, { cancelButtonLabel: t("layout.actions.back"), disabledDays: customDateRangeDisabledDays, language: "en", max: maxDaysInRange, onClose: () => setIsCustomDateRangeOpen(false), onRangeSelect: dateRange => handleCustomDateRangeSelect(dateRange, closeMenu), selectedDays: currentSelectedRange?.dateRange, timezone: timezone }) }));
|
|
633
|
+
} })] }));
|
|
292
634
|
};
|
|
293
635
|
|
|
294
636
|
const cvaTimelineElement = cssClassVarianceUtilities.cvaMerge([
|
|
@@ -526,7 +868,11 @@ exports.DateTime = DateTime;
|
|
|
526
868
|
exports.DateTimeHumanized = DateTimeHumanized;
|
|
527
869
|
exports.DayPicker = DayPicker;
|
|
528
870
|
exports.DayRangePicker = DayRangePicker;
|
|
529
|
-
exports.
|
|
871
|
+
exports.DayRangeSelect = DayRangeSelect;
|
|
530
872
|
exports.MS_PER_HOUR = MS_PER_HOUR;
|
|
531
873
|
exports.Timeline = Timeline;
|
|
532
874
|
exports.TimelineElement = TimelineElement;
|
|
875
|
+
exports.formatCustomDateRangeLabel = formatCustomDateRangeLabel;
|
|
876
|
+
exports.isTemporalPeriod = isTemporalPeriod;
|
|
877
|
+
exports.useCalculateDateRange = useCalculateDateRange;
|
|
878
|
+
exports.useFormatTemporalPeriodLabel = useFormatTemporalPeriodLabel;
|