@trackunit/react-date-and-time-components 1.3.221 → 1.4.0

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.
Files changed (27) hide show
  1. package/index.cjs.js +419 -73
  2. package/index.esm.js +417 -75
  3. package/package.json +13 -12
  4. package/src/DayPicker/DayRangePicker.d.ts +7 -14
  5. package/src/DayPicker/DayRangePicker.stories.d.ts +0 -1
  6. package/src/DayPicker/index.d.ts +0 -1
  7. package/src/DayRangeSelect/DayRangeSelect.d.ts +56 -0
  8. package/src/DayRangeSelect/DayRangeSelect.stories.d.ts +10 -0
  9. package/src/DayRangeSelect/DayRangeSelect.variants.d.ts +3 -0
  10. package/src/DayRangeSelect/components/DayRangeSelectOptions.d.ts +11 -0
  11. package/src/DayRangeSelect/hooks/useCalculateCustomDateRangeDisabledDays/useCalculateCustomDateRangeDisabledDays.d.ts +11 -0
  12. package/src/DayRangeSelect/hooks/useCalculateDateRange/useCalculateDateRange.d.ts +9 -0
  13. package/src/DayRangeSelect/hooks/useFilterTemporalPeriodsInRange/useFilterTemporalPeriodsInRange.d.ts +18 -0
  14. package/src/DayRangeSelect/hooks/useFormatTemporalPeriodLabel/useFormatTemporalPeriodLabel.d.ts +9 -0
  15. package/src/DayRangeSelect/hooks/useParseTemporalPeriodFromText/hooks/useFindTemporalDirection.d.ts +5 -0
  16. package/src/DayRangeSelect/hooks/useParseTemporalPeriodFromText/hooks/useFindTemporalUnit.d.ts +5 -0
  17. package/src/DayRangeSelect/hooks/useParseTemporalPeriodFromText/useParseTemporalPeriodFromText.d.ts +10 -0
  18. package/src/DayRangeSelect/index.d.ts +6 -0
  19. package/src/DayRangeSelect/types.d.ts +16 -0
  20. package/src/DayRangeSelect/utils/createTemporalPeriodCombinations/createTemporalPeriodCombinations.d.ts +12 -0
  21. package/src/DayRangeSelect/utils/formatCustomDateRangeLabel/formatCustomDateRangeLabel.d.ts +8 -0
  22. package/src/DayRangeSelect/utils/isTemporalPeriod/isTemporalPeriod.d.ts +8 -0
  23. package/src/DayRangeSelect/utils/temporalUnitMappings.d.ts +3 -0
  24. package/src/index.d.ts +1 -0
  25. package/src/translation.d.ts +2 -2
  26. package/src/DayPicker/DayRangePickerPopover.d.ts +0 -45
  27. 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 reactCoreHooks = require('@trackunit/react-core-hooks');
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
- "dayRangePickerPopover.icon.tooltip.calendar": "Calendar",
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, showQuickOptions, timezone, onClose, }) => {
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: "z-popover flex w-min flex-col gap-4", children: [jsxRuntime.jsxs("div", { className: "flex gap-2", children: [jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: !!showQuickOptions &&
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
- const calcQuickOptions = (numberOfDays) => {
211
- const quickOptions = [];
212
- if (numberOfDays > 7 && numberOfDays < 35) {
213
- quickOptions.push({ last: 1, unit: "days" }, { last: numberOfDays, unit: "days", includeExtraDay: true });
214
- }
215
- else if (numberOfDays >= 35 && numberOfDays < 185) {
216
- quickOptions.push({ last: 1, unit: "days" }, { last: 7, unit: "days", includeExtraDay: false }, { last: 30, unit: "days", includeExtraDay: false }, { last: numberOfDays, unit: "days", includeExtraDay: false });
217
- }
218
- else if (numberOfDays >= 185) {
219
- quickOptions.push({ last: 1, unit: "days" }, { last: 14, unit: "days", includeExtraDay: false }, { last: 30, unit: "days", includeExtraDay: false }, { last: numberOfDays, unit: "days", includeExtraDay: false });
220
- }
221
- return quickOptions;
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
- const QuickOptionButton = ({ option, onClick, timezone }) => {
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 { nowDate, startOf, endOf, subtract } = reactDateAndTimeHooks.useDateAndTime();
226
- const handleClick = react.useCallback(() => {
227
- const from = subtract(startOf(nowDate, "day"), option.includeExtraDay ? option.last : option.last - 1, option.unit);
228
- onClick({
229
- from,
230
- to: endOf(nowDate, "day"),
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
- }, [subtract, startOf, endOf, nowDate, option.last, option.unit, option.includeExtraDay, onClick]);
233
- const translatedUnits = (unit) => {
234
- switch (unit) {
235
- case "hours":
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
- const getLabel = () => {
245
- if (option.label) {
246
- return option.label;
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
- else if (option.last === 1 && option.unit === "days") {
249
- return t("shared.timePeriods.today");
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
- const tKey = translatedUnits(option.unit);
253
- return tKey
254
- ? t(tKey, {
255
- count: option.last,
256
- })
257
- : null;
364
+ return {
365
+ before: nowDate,
366
+ after: add(nowDate, adjustedMaxDaysInRangeCount, "days"),
367
+ };
258
368
  }
259
- };
260
- return (jsxRuntime.jsx(reactComponents.Button, { dataTestId: `${option.unit}${option.last}${option.includeExtraDay ? "-include-extra-day" : ""}`, onClick: handleClick, size: "small", variant: "secondary", children: getLabel() }));
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
- * A popover to select a range of dates. Consider providing quick options.
265
- * quickOptions takes optional "includeExtraDay". This is because customers often want to see 8 days when selecting "week" so they can compare this monday to last weeks monday. Same goes for months. This option will not impact the label of the button.
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
- * @param { DayRangePickerPopoverProps} props - The properties for the DayRangePickerPopover component.
268
- * @param props.interval initial date range
269
- * @param props.showQuickOptions display additional buttons for quick date range selection
270
- * @param props.disabledDays disable days from selecting possibility
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 DayRangePickerPopover = ({ interval = { from: undefined, to: undefined }, showQuickOptions, disabledDays, className, dataTestId, onRangeSelect, variant = "secondary", size = "small", placement, timezone, fullwidth, bindTo, buttonContent, max, disabled, }) => {
284
- const { language } = reactCoreHooks.useCurrentUserLanguage();
449
+ const useParseTemporalPeriodFromText = () => {
285
450
  const [t] = useTranslation();
286
- const handleOnRangeSelect = react.useCallback((range) => {
287
- if (range) {
288
- onRangeSelect(range);
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
- return (jsxRuntime.jsxs(reactComponents.Popover, { placement: placement, children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx(reactComponents.Button, { className: className, dataTestId: dataTestId ?? "show-date-range", disabled: disabled, fullWidth: fullwidth, prefix: jsxRuntime.jsx(reactComponents.Icon, { ariaLabel: t("dayRangePickerPopover.icon.tooltip.calendar"), name: "Calendar", size: "small" }), size: size, variant: variant, children: buttonContent }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { children: close => (jsxRuntime.jsx(reactComponents.Card, { className: "min-w-min p-4", dataTestId: "range-popover-list", children: jsxRuntime.jsx(DayRangePicker, { disabledDays: disabledDays, language: language || "en", max: max, onClose: close, onRangeSelect: handleOnRangeSelect, selectedDays: interval, showQuickOptions: showQuickOptions, timezone: timezone }) })) })] }));
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.DayRangePickerPopover = DayRangePickerPopover;
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;