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