@mirohq/design-system-calendar 2.4.42 → 2.5.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/dist/module.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import React, { createContext, useContext, useState, useMemo, useCallback, useRef, useEffect } from 'react';
3
- import { useDateFormatter, useCalendarCell, useFocusRing, mergeProps, useLocale, useCalendarGrid, useCalendar, useRangeCalendar, I18nProvider } from 'react-aria';
2
+ import React, { createContext, useContext, useState, useMemo, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
3
+ import { useLocale, useDateFormatter, useCalendarCell, useFocusRing, mergeProps, useCalendarGrid, useCalendar, useRangeCalendar, I18nProvider } from 'react-aria';
4
4
  import * as RadixPopover from '@radix-ui/react-popover';
5
5
  import { Trigger as Trigger$1, Content as Content$1 } from '@radix-ui/react-popover';
6
- import { IconCross, IconCalendarBlank, IconChevronLeft, IconChevronRight } from '@mirohq/design-system-icons';
6
+ import { IconCross, IconCalendarBlank, IconChevronLeft, IconChevronRight, IconClock } from '@mirohq/design-system-icons';
7
7
  import { useFormFieldContext } from '@mirohq/design-system-base-form';
8
8
  import { stringAttrValue, booleanify } from '@mirohq/design-system-utils';
9
9
  import { Primitive } from '@mirohq/design-system-primitive';
@@ -14,7 +14,7 @@ import { BaseButton } from '@mirohq/design-system-base-button';
14
14
  import { Tooltip } from '@mirohq/design-system-tooltip';
15
15
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
16
16
  import { useCalendarState, useRangeCalendarState } from 'react-stately';
17
- import { isSameMonth, isSameDay, isToday, getLocalTimeZone, getWeeksInMonth, createCalendar } from '@internationalized/date';
17
+ import { getLocalTimeZone, startOfWeek, endOfWeek, today, isSameDay, isSameMonth, isToday, getWeeksInMonth, CalendarDate, createCalendar } from '@internationalized/date';
18
18
  import { Flex } from '@mirohq/design-system-flex';
19
19
  import { Heading } from '@mirohq/design-system-typography';
20
20
  import { IconButton } from '@mirohq/design-system-icon-button';
@@ -103,6 +103,18 @@ const ClearAction = React.forwardRef(({ "aria-label": ariaLabel, label, ...restP
103
103
  /* @__PURE__ */ jsx(Tooltip.Content, { children: label })
104
104
  ] }));
105
105
 
106
+ const PERIOD_TO_VIEW_MODE = {
107
+ quarter: "quarters",
108
+ month: "months"
109
+ };
110
+ const getNavigationYear = (value) => {
111
+ var _a;
112
+ if (value == null) {
113
+ return (/* @__PURE__ */ new Date()).getFullYear();
114
+ }
115
+ const date = "start" in value ? value.start : value;
116
+ return (_a = date == null ? void 0 : date.year) != null ? _a : (/* @__PURE__ */ new Date()).getFullYear();
117
+ };
106
118
  const CalendarContext = createContext({});
107
119
  const CalendarProvider = ({
108
120
  children,
@@ -116,7 +128,7 @@ const CalendarProvider = ({
116
128
  onClose,
117
129
  ...restProps
118
130
  }) => {
119
- var _a;
131
+ var _a, _b;
120
132
  const [immediateSave, setImmediateSave] = useState(false);
121
133
  const [internalCalendarValue, setInternalCalendarValue] = useState(valueProp != null ? valueProp : defaultValueProp);
122
134
  const visibleDuration = useMemo(
@@ -130,6 +142,13 @@ const CalendarProvider = ({
130
142
  defaultProp: defaultValueProp,
131
143
  onChange: onValueChange
132
144
  });
145
+ const defaultViewMode = (_a = restProps.period != null ? PERIOD_TO_VIEW_MODE[restProps.period] : void 0) != null ? _a : "days";
146
+ const [viewMode, setViewMode] = useState(defaultViewMode);
147
+ const [navigationYear, setNavigationYear] = useState(
148
+ () => getNavigationYear(value)
149
+ );
150
+ const valueRef = useRef(value);
151
+ valueRef.current = value;
133
152
  const [open = false, setOpen] = useControllableState({
134
153
  prop: openProp,
135
154
  defaultProp: defaultOpenProp,
@@ -137,10 +156,20 @@ const CalendarProvider = ({
137
156
  if (state) {
138
157
  onOpen == null ? void 0 : onOpen();
139
158
  } else {
159
+ setViewMode(defaultViewMode);
140
160
  onClose == null ? void 0 : onClose();
141
161
  }
142
162
  }
143
163
  });
164
+ const valueYear = getNavigationYear(value);
165
+ useEffect(() => {
166
+ setNavigationYear(valueYear);
167
+ }, [valueYear]);
168
+ useEffect(() => {
169
+ if (open) {
170
+ setNavigationYear(getNavigationYear(valueRef.current));
171
+ }
172
+ }, [open]);
144
173
  const onCalendarDateClick = useCallback(
145
174
  (value2) => {
146
175
  if (immediateSave) {
@@ -156,11 +185,15 @@ const CalendarProvider = ({
156
185
  setValue(internalCalendarValue);
157
186
  setOpen(false);
158
187
  };
188
+ const periodImpliesNavigation = restProps.period === "quarter" || restProps.period === "month" || restProps.period === "week";
189
+ const effectivePeriodNavigation = restProps.navigation === true && visibleMonths <= 1 || periodImpliesNavigation;
190
+ const effectiveViewMode = effectivePeriodNavigation ? viewMode : "days";
159
191
  return /* @__PURE__ */ jsx(
160
192
  CalendarContext.Provider,
161
193
  {
162
194
  value: {
163
195
  ...restProps,
196
+ navigation: effectivePeriodNavigation,
164
197
  visibleDuration,
165
198
  value,
166
199
  setValue,
@@ -169,8 +202,11 @@ const CalendarProvider = ({
169
202
  onCalendarDateClick,
170
203
  setImmediateSave,
171
204
  onSaveClick,
172
- // value should not be undefined otherwise react-aria calendar will complain in console
173
- calendarValue: (_a = immediateSave ? internalCalendarValue : value) != null ? _a : null
205
+ calendarValue: (_b = immediateSave ? internalCalendarValue : value) != null ? _b : null,
206
+ viewMode: effectiveViewMode,
207
+ setViewMode,
208
+ navigationYear,
209
+ setNavigationYear
174
210
  },
175
211
  children
176
212
  }
@@ -233,6 +269,298 @@ const Trigger = React.forwardRef(
233
269
  }
234
270
  );
235
271
 
272
+ const DECADE_SIZE = 12;
273
+
274
+ const StyledValue = styled(Primitive.span, {});
275
+
276
+ const en = {
277
+ quarter: (q) => "Q".concat(q),
278
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
279
+ views: { months: "months", quarters: "quarters", years: "years" },
280
+ switchView: (title, nextView) => "".concat(title, ", switch to ").concat(nextView, " view"),
281
+ previousYear: "Previous year",
282
+ nextYear: "Next year",
283
+ previousYears: (n) => "Previous ".concat(n, " years"),
284
+ nextYears: (n) => "Next ".concat(n, " years")
285
+ };
286
+ const STRINGS = {
287
+ en,
288
+ de: {
289
+ quarter: (q) => "Q".concat(q),
290
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
291
+ views: { months: "Monate", quarters: "Quartale", years: "Jahre" },
292
+ switchView: (title, nextView) => "".concat(title, ", zur Ansicht ").concat(nextView, " wechseln"),
293
+ previousYear: "Vorheriges Jahr",
294
+ nextYear: "N\xE4chstes Jahr",
295
+ previousYears: (n) => "Vorherige ".concat(n, " Jahre"),
296
+ nextYears: (n) => "N\xE4chste ".concat(n, " Jahre")
297
+ },
298
+ es: {
299
+ quarter: (q) => "T".concat(q),
300
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
301
+ views: { months: "meses", quarters: "trimestres", years: "a\xF1os" },
302
+ switchView: (title, nextView) => "".concat(title, ", cambiar a la vista de ").concat(nextView),
303
+ previousYear: "A\xF1o anterior",
304
+ nextYear: "A\xF1o siguiente",
305
+ previousYears: (n) => "".concat(n, " a\xF1os anteriores"),
306
+ nextYears: (n) => "".concat(n, " a\xF1os siguientes")
307
+ },
308
+ fr: {
309
+ quarter: (q) => "T".concat(q),
310
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
311
+ views: { months: "mois", quarters: "trimestres", years: "ann\xE9es" },
312
+ switchView: (title, nextView) => "".concat(title, ", passer \xE0 la vue ").concat(nextView),
313
+ previousYear: "Ann\xE9e pr\xE9c\xE9dente",
314
+ nextYear: "Ann\xE9e suivante",
315
+ previousYears: (n) => "".concat(n, " ann\xE9es pr\xE9c\xE9dentes"),
316
+ nextYears: (n) => "".concat(n, " ann\xE9es suivantes")
317
+ },
318
+ it: {
319
+ quarter: (q) => "T".concat(q),
320
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
321
+ views: { months: "mesi", quarters: "trimestri", years: "anni" },
322
+ switchView: (title, nextView) => "".concat(title, ", passa alla vista ").concat(nextView),
323
+ previousYear: "Anno precedente",
324
+ nextYear: "Anno successivo",
325
+ previousYears: (n) => "".concat(n, " anni precedenti"),
326
+ nextYears: (n) => "".concat(n, " anni successivi")
327
+ },
328
+ ja: {
329
+ quarter: (q) => "\u7B2C".concat(q, "\u56DB\u534A\u671F"),
330
+ quarterWithYear: (label, year) => "".concat(year, "\u5E74").concat(label),
331
+ views: { months: "\u6708", quarters: "\u56DB\u534A\u671F", years: "\u5E74" },
332
+ switchView: (title, nextView) => "".concat(title, "\u3001").concat(nextView, "\u8868\u793A\u306B\u5207\u308A\u66FF\u3048"),
333
+ previousYear: "\u524D\u5E74",
334
+ nextYear: "\u7FCC\u5E74",
335
+ previousYears: (n) => "\u524D\u306E".concat(n, "\u5E74"),
336
+ nextYears: (n) => "\u6B21\u306E".concat(n, "\u5E74")
337
+ },
338
+ ko: {
339
+ quarter: (q) => "".concat(q, "\uBD84\uAE30"),
340
+ quarterWithYear: (label, year) => "".concat(year, "\uB144 ").concat(label),
341
+ views: { months: "\uC6D4", quarters: "\uBD84\uAE30", years: "\uB144" },
342
+ switchView: (title, nextView) => "".concat(title, ", ").concat(nextView, " \uBCF4\uAE30\uB85C \uC804\uD658"),
343
+ previousYear: "\uC774\uC804 \uD574",
344
+ nextYear: "\uB2E4\uC74C \uD574",
345
+ previousYears: (n) => "\uC774\uC804 ".concat(n, "\uB144"),
346
+ nextYears: (n) => "\uB2E4\uC74C ".concat(n, "\uB144")
347
+ },
348
+ nl: {
349
+ quarter: (q) => "K".concat(q),
350
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
351
+ views: { months: "maanden", quarters: "kwartalen", years: "jaren" },
352
+ switchView: (title, nextView) => "".concat(title, ", ga naar weergave ").concat(nextView),
353
+ previousYear: "Vorig jaar",
354
+ nextYear: "Volgend jaar",
355
+ previousYears: (n) => "Vorige ".concat(n, " jaar"),
356
+ nextYears: (n) => "Volgende ".concat(n, " jaar")
357
+ },
358
+ pl: {
359
+ quarter: (q) => "K".concat(q),
360
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
361
+ views: { months: "miesi\u0105ce", quarters: "kwarta\u0142y", years: "lata" },
362
+ switchView: (title, nextView) => "".concat(title, ", prze\u0142\u0105cz na widok: ").concat(nextView),
363
+ previousYear: "Poprzedni rok",
364
+ nextYear: "Nast\u0119pny rok",
365
+ previousYears: (n) => "Poprzednie ".concat(n, " lat"),
366
+ nextYears: (n) => "Nast\u0119pne ".concat(n, " lat")
367
+ },
368
+ pt: {
369
+ quarter: (q) => "T".concat(q),
370
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
371
+ views: { months: "meses", quarters: "trimestres", years: "anos" },
372
+ switchView: (title, nextView) => "".concat(title, ", mudar para a visualiza\xE7\xE3o de ").concat(nextView),
373
+ previousYear: "Ano anterior",
374
+ nextYear: "Pr\xF3ximo ano",
375
+ previousYears: (n) => "".concat(n, " anos anteriores"),
376
+ nextYears: (n) => "Pr\xF3ximos ".concat(n, " anos")
377
+ },
378
+ ru: {
379
+ quarter: (q) => "".concat(q, " \u043A\u0432."),
380
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
381
+ views: { months: "\u043C\u0435\u0441\u044F\u0446\u044B", quarters: "\u043A\u0432\u0430\u0440\u0442\u0430\u043B\u044B", years: "\u0433\u043E\u0434\u044B" },
382
+ switchView: (title, nextView) => "".concat(title, ", \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0430 ").concat(nextView),
383
+ previousYear: "\u041F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0438\u0439 \u0433\u043E\u0434",
384
+ nextYear: "\u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0433\u043E\u0434",
385
+ previousYears: (n) => "\u041F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0438\u0435 ".concat(n, " \u043B\u0435\u0442"),
386
+ nextYears: (n) => "\u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0435 ".concat(n, " \u043B\u0435\u0442")
387
+ },
388
+ tr: {
389
+ quarter: (q) => "\xC7".concat(q),
390
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
391
+ views: { months: "aylar", quarters: "\xE7eyrekler", years: "y\u0131llar" },
392
+ switchView: (title, nextView) => "".concat(title, ", ").concat(nextView, " g\xF6r\xFCn\xFCm\xFCne ge\xE7"),
393
+ previousYear: "\xD6nceki y\u0131l",
394
+ nextYear: "Sonraki y\u0131l",
395
+ previousYears: (n) => "\xD6nceki ".concat(n, " y\u0131l"),
396
+ nextYears: (n) => "Sonraki ".concat(n, " y\u0131l")
397
+ },
398
+ uk: {
399
+ quarter: (q) => "".concat(q, " \u043A\u0432."),
400
+ quarterWithYear: (label, year) => "".concat(label, " ").concat(year),
401
+ views: { months: "\u043C\u0456\u0441\u044F\u0446\u0456", quarters: "\u043A\u0432\u0430\u0440\u0442\u0430\u043B\u0438", years: "\u0440\u043E\u043A\u0438" },
402
+ switchView: (title, nextView) => "".concat(title, ", \u043F\u0435\u0440\u0435\u0439\u0442\u0438 \u0434\u043E ").concat(nextView),
403
+ previousYear: "\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u0440\u0456\u043A",
404
+ nextYear: "\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u0440\u0456\u043A",
405
+ previousYears: (n) => "\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456 ".concat(n, " \u0440\u043E\u043A\u0456\u0432"),
406
+ nextYears: (n) => "\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0456 ".concat(n, " \u0440\u043E\u043A\u0456\u0432")
407
+ },
408
+ zh: {
409
+ quarter: (q) => "\u7B2C".concat(q, "\u5B63\u5EA6"),
410
+ quarterWithYear: (label, year) => "".concat(year, "\u5E74").concat(label),
411
+ views: { months: "\u6708\u4EFD", quarters: "\u5B63\u5EA6", years: "\u5E74\u4EFD" },
412
+ switchView: (title, nextView) => "".concat(title, "\uFF0C\u5207\u6362\u5230").concat(nextView, "\u89C6\u56FE"),
413
+ previousYear: "\u4E0A\u4E00\u5E74",
414
+ nextYear: "\u4E0B\u4E00\u5E74",
415
+ previousYears: (n) => "\u524D".concat(n, "\u5E74"),
416
+ nextYears: (n) => "\u540E".concat(n, "\u5E74")
417
+ }
418
+ };
419
+ const getCalendarStrings = (locale) => {
420
+ var _a;
421
+ return (_a = STRINGS[locale.split(/[-_]/)[0].toLowerCase()]) != null ? _a : en;
422
+ };
423
+ const useCalendarStrings = () => {
424
+ const { locale } = useLocale();
425
+ return useMemo(() => getCalendarStrings(locale), [locale]);
426
+ };
427
+
428
+ const DEFAULT_FORMAT = {
429
+ day: "2-digit",
430
+ month: "short",
431
+ year: "numeric"
432
+ };
433
+ const PERIOD_DAY_FORMAT = {
434
+ day: "numeric",
435
+ month: "short"
436
+ };
437
+ const PERIOD_WEEK_FORMAT = PERIOD_DAY_FORMAT;
438
+ const PERIOD_MONTH_FORMAT = {
439
+ month: "short",
440
+ year: "2-digit"
441
+ };
442
+ function formatDate(date, formatter, timeZone) {
443
+ return formatter.format(date.toDate(timeZone));
444
+ }
445
+ function formatValue(value, formatter, timeZone) {
446
+ if (value == null || value === void 0) {
447
+ return void 0;
448
+ }
449
+ if ("start" in value) {
450
+ if (value.start == null || value.end == null) {
451
+ return void 0;
452
+ }
453
+ return "".concat(formatDate(value.start, formatter, timeZone), " - ").concat(formatDate(
454
+ value.end,
455
+ formatter,
456
+ timeZone
457
+ ));
458
+ }
459
+ return formatDate(value, formatter, timeZone);
460
+ }
461
+ function isCurrentPeriod(value, period, locale) {
462
+ const todayDate = today(getLocalTimeZone());
463
+ switch (period) {
464
+ case "day":
465
+ case void 0:
466
+ return isSameDay(value, todayDate);
467
+ case "week": {
468
+ const currentWeekStart = startOfWeek(todayDate, locale);
469
+ const selectedWeekStart = startOfWeek(value, locale);
470
+ return isSameDay(currentWeekStart, selectedWeekStart);
471
+ }
472
+ case "month":
473
+ return value.year === todayDate.year && value.month === todayDate.month;
474
+ case "quarter": {
475
+ const currentQuarter = Math.ceil(todayDate.month / 3);
476
+ const selectedQuarter = Math.ceil(value.month / 3);
477
+ return value.year === todayDate.year && selectedQuarter === currentQuarter;
478
+ }
479
+ default:
480
+ return false;
481
+ }
482
+ }
483
+ const Value = React.forwardRef(({ format, currentLabel, ...restProps }, forwardRef) => {
484
+ const { value, period, formatQuarterLabel } = useCalendarContext();
485
+ const { locale } = useLocale();
486
+ const strings = useCalendarStrings();
487
+ const yearFormatter = useMemo(
488
+ () => new Intl.NumberFormat(locale, { useGrouping: false }),
489
+ [locale]
490
+ );
491
+ const timeZone = useMemo(getLocalTimeZone, []);
492
+ const effectivePeriod = period != null ? period : "day";
493
+ const periodFormat = useMemo(() => {
494
+ if (format != null) return format;
495
+ if (period == null) return DEFAULT_FORMAT;
496
+ switch (period) {
497
+ case "day":
498
+ return PERIOD_DAY_FORMAT;
499
+ case "week":
500
+ return PERIOD_WEEK_FORMAT;
501
+ case "month":
502
+ return PERIOD_MONTH_FORMAT;
503
+ default:
504
+ return DEFAULT_FORMAT;
505
+ }
506
+ }, [format, period]);
507
+ const formatWithTimeZone = useMemo(
508
+ () => ({ ...periodFormat, timeZone }),
509
+ [periodFormat, timeZone]
510
+ );
511
+ const dateFormatter = useDateFormatter(formatWithTimeZone);
512
+ const dayOnlyFormat = useMemo(
513
+ () => ({ day: "numeric", timeZone }),
514
+ [timeZone]
515
+ );
516
+ const dayOnlyFormatter = useDateFormatter(dayOnlyFormat);
517
+ let valueToDisplay;
518
+ if (value == null) {
519
+ valueToDisplay = void 0;
520
+ } else if ("start" in value) {
521
+ valueToDisplay = formatValue(value, dateFormatter, timeZone);
522
+ } else {
523
+ switch (effectivePeriod) {
524
+ case "week": {
525
+ const calendarDate = value;
526
+ const weekStart = startOfWeek(calendarDate, locale);
527
+ const weekEnd = endOfWeek(calendarDate, locale);
528
+ const startStr = formatDate(weekStart, dateFormatter, timeZone);
529
+ if (weekStart.month === weekEnd.month) {
530
+ const endDay = dayOnlyFormatter.format(weekEnd.toDate(timeZone));
531
+ valueToDisplay = "".concat(startStr, " - ").concat(endDay);
532
+ } else {
533
+ const endStr = formatDate(weekEnd, dateFormatter, timeZone);
534
+ if (startStr != null && endStr != null) {
535
+ valueToDisplay = "".concat(startStr, " - ").concat(endStr);
536
+ }
537
+ }
538
+ break;
539
+ }
540
+ case "quarter": {
541
+ const quarter = Math.ceil(value.month / 3);
542
+ valueToDisplay = strings.quarterWithYear(
543
+ (formatQuarterLabel != null ? formatQuarterLabel : strings.quarter)(quarter),
544
+ yearFormatter.format(value.year)
545
+ );
546
+ break;
547
+ }
548
+ case "month": {
549
+ valueToDisplay = formatDate(value, dateFormatter, timeZone);
550
+ break;
551
+ }
552
+ default: {
553
+ valueToDisplay = formatDate(value, dateFormatter, timeZone);
554
+ break;
555
+ }
556
+ }
557
+ if (valueToDisplay != null && currentLabel != null && !("start" in value) && isCurrentPeriod(value, period, locale)) {
558
+ valueToDisplay = "".concat(valueToDisplay, " (").concat(currentLabel, ")");
559
+ }
560
+ }
561
+ return /* @__PURE__ */ jsx(StyledValue, { ref: forwardRef, ...restProps, children: valueToDisplay });
562
+ });
563
+
236
564
  const StyledContent = styled(Content$1, {
237
565
  ...floating.css(),
238
566
  zIndex: "$calendar",
@@ -254,12 +582,54 @@ const StyledClearContent = styled(Primitive.div, {
254
582
  padding: "$200",
255
583
  borderTop: "1px solid $border-default"
256
584
  });
585
+ const StyledGoToCurrent = styled(BaseButton, {
586
+ display: "flex",
587
+ alignItems: "center",
588
+ gap: "$100",
589
+ marginTop: "$150",
590
+ padding: "$100 $150",
591
+ borderRadius: "$calendar",
592
+ color: "$text-secondary",
593
+ fontSize: "$body-xs",
594
+ lineHeight: "$body-xs",
595
+ fontWeight: "$body-xs",
596
+ "& svg": {
597
+ width: "16px",
598
+ height: "16px",
599
+ color: "$icon-secondary"
600
+ },
601
+ _hover: {
602
+ backgroundColor: "$background-interactive-surface-secondary-hover"
603
+ },
604
+ ...focus.css({
605
+ boxShadow: "$focus",
606
+ outline: "1px solid transparent"
607
+ })
608
+ });
257
609
 
258
610
  const StyledHeaderTitle = styled(Heading, {
259
611
  margin: 0,
260
612
  flex: "2",
261
613
  textAlign: "center"
262
614
  });
615
+ const StyledHeaderTitleButton = styled(BaseButton, {
616
+ display: "inline-block",
617
+ textAlign: "center",
618
+ fontSize: "$250",
619
+ fontWeight: "$bold",
620
+ lineHeight: "$250",
621
+ color: "$text-interactive-primary",
622
+ borderRadius: "$button",
623
+ padding: "$50 $100",
624
+ ...focus.css({
625
+ boxShadow: "$focus",
626
+ outline: "1px solid transparent"
627
+ }),
628
+ _hover: {
629
+ color: "$text-interactive-primary-hover",
630
+ backgroundColor: "$background-interactive-surface-secondary-hover"
631
+ }
632
+ });
263
633
  const StyledHeaderPlaceholder = styled("div", {
264
634
  height: "$8",
265
635
  // to compensate icon buttons' width and space between 2 calendars
@@ -287,25 +657,88 @@ const Header = ({
287
657
  state,
288
658
  prevButtonProps,
289
659
  nextButtonProps,
290
- visibleMonths
660
+ visibleMonths,
661
+ yearFormatter,
662
+ onPrevOverride,
663
+ onNextOverride,
664
+ onZoomOut,
665
+ isPrevDisabled,
666
+ isNextDisabled
291
667
  }) => {
668
+ const { navigation, viewMode, navigationYear, formatSwitchViewLabel } = useCalendarContext();
669
+ const strings = useCalendarStrings();
292
670
  const monthDateFormatter = useDateFormatter({
293
671
  month: "long",
294
672
  year: "numeric",
295
673
  timeZone: state.timeZone
296
674
  });
675
+ const getTitleText = () => {
676
+ switch (viewMode) {
677
+ case "months":
678
+ case "quarters":
679
+ return yearFormatter.format(navigationYear);
680
+ case "years": {
681
+ const decadeStart = Math.floor(navigationYear / DECADE_SIZE) * DECADE_SIZE;
682
+ return "".concat(yearFormatter.format(decadeStart), "\u2013").concat(yearFormatter.format(
683
+ decadeStart + DECADE_SIZE - 1
684
+ ));
685
+ }
686
+ default:
687
+ return monthDateFormatter.format(
688
+ state.visibleRange.start.toDate(state.timeZone)
689
+ );
690
+ }
691
+ };
692
+ const getNextViewLabel = () => {
693
+ if (viewMode === "days") return strings.views.months;
694
+ if (viewMode === "months" || viewMode === "quarters") {
695
+ return strings.views.years;
696
+ }
697
+ return "";
698
+ };
699
+ const isTopLevel = viewMode === "years";
700
+ const showClickableTitle = navigation === true && !isTopLevel;
701
+ const isDayView = viewMode === "days";
702
+ const stepLabels = viewMode === "years" ? {
703
+ prev: strings.previousYears(DECADE_SIZE),
704
+ next: strings.nextYears(DECADE_SIZE)
705
+ } : { prev: strings.previousYear, next: strings.nextYear };
297
706
  return /* @__PURE__ */ jsxs(StyledHeader, { children: [
298
- /* @__PURE__ */ jsx(HeaderButton, { ...prevButtonProps, children: /* @__PURE__ */ jsx(IconChevronLeft, {}) }),
299
- /* @__PURE__ */ jsx(StyledHeaderTitle, { level: 2, styledAs: "h3", children: monthDateFormatter.format(
300
- state.visibleRange.start.toDate(state.timeZone)
301
- ) }),
302
- visibleMonths === 2 && /* @__PURE__ */ jsxs(Fragment, { children: [
707
+ isDayView ? /* @__PURE__ */ jsx(HeaderButton, { ...prevButtonProps, children: /* @__PURE__ */ jsx(IconChevronLeft, {}) }) : /* @__PURE__ */ jsx(
708
+ HeaderButton,
709
+ {
710
+ "aria-label": stepLabels.prev,
711
+ isDisabled: isPrevDisabled,
712
+ onClick: onPrevOverride,
713
+ children: /* @__PURE__ */ jsx(IconChevronLeft, {})
714
+ }
715
+ ),
716
+ /* @__PURE__ */ jsx(StyledHeaderTitle, { level: 2, styledAs: "h3", children: showClickableTitle ? /* @__PURE__ */ jsx(
717
+ StyledHeaderTitleButton,
718
+ {
719
+ onClick: onZoomOut,
720
+ "aria-label": (formatSwitchViewLabel != null ? formatSwitchViewLabel : strings.switchView)(
721
+ getTitleText(),
722
+ getNextViewLabel()
723
+ ),
724
+ children: getTitleText()
725
+ }
726
+ ) : getTitleText() }),
727
+ visibleMonths === 2 && isDayView && /* @__PURE__ */ jsxs(Fragment, { children: [
303
728
  /* @__PURE__ */ jsx(StyledHeaderPlaceholder, {}),
304
729
  /* @__PURE__ */ jsx(StyledHeaderTitle, { level: 2, styledAs: "h3", children: monthDateFormatter.format(
305
730
  state.visibleRange.start.add({ months: 1 }).toDate(state.timeZone)
306
731
  ) })
307
732
  ] }),
308
- /* @__PURE__ */ jsx(HeaderButton, { ...nextButtonProps, children: /* @__PURE__ */ jsx(IconChevronRight, {}) })
733
+ isDayView ? /* @__PURE__ */ jsx(HeaderButton, { ...nextButtonProps, children: /* @__PURE__ */ jsx(IconChevronRight, {}) }) : /* @__PURE__ */ jsx(
734
+ HeaderButton,
735
+ {
736
+ "aria-label": stepLabels.next,
737
+ isDisabled: isNextDisabled,
738
+ onClick: onNextOverride,
739
+ children: /* @__PURE__ */ jsx(IconChevronRight, {})
740
+ }
741
+ )
309
742
  ] });
310
743
  };
311
744
 
@@ -385,13 +818,30 @@ const StyledCellContent = styled(Primitive.div, {
385
818
  true: {
386
819
  display: "none"
387
820
  }
821
+ },
822
+ outsideMonth: {
823
+ true: {
824
+ color: "$text-secondary",
825
+ '&[aria-disabled="true"]': {
826
+ textDecoration: "none"
827
+ }
828
+ }
388
829
  }
830
+ },
831
+ // Week period: the row is the week, so hovering or focusing anywhere in it
832
+ // highlights the whole week. Cells in the selected week carry no
833
+ // data-week-hover, so the selected styling is not overridden.
834
+ "tr:hover > td > &[data-week-hover], tr:focus-within > td > &[data-week-hover]": {
835
+ backgroundColor: "$background-interactive-surface-secondary-hover",
836
+ borderRadius: 0
389
837
  }
390
838
  });
391
839
 
392
840
  const Cell = (props) => {
393
841
  let { state, date, currentMonth } = props;
394
842
  const ref = useRef(null);
843
+ const { period, minDate, maxDate, onCalendarDateClick } = useCalendarContext();
844
+ const { locale } = useLocale();
395
845
  const { cellProps, buttonProps, isSelected, formattedDate } = useCalendarCell(
396
846
  { date },
397
847
  state,
@@ -404,15 +854,65 @@ const Cell = (props) => {
404
854
  const { focusProps } = useFocusRing();
405
855
  const isRange = isSelected && (!isSelectionStart || !isSelectionEnd);
406
856
  const rangeVariant = isRange ? isSelectionStart ? "start" : isSelectionEnd ? "end" : "middle" : void 0;
407
- return /* @__PURE__ */ jsx(StyledCell, { ...cellProps, children: /* @__PURE__ */ jsx(
857
+ let effectiveSelected = isSelected && (isSelectionStart || isSelectionEnd);
858
+ let effectiveRange = rangeVariant;
859
+ const isWeekPeriod = period === "week";
860
+ if (isWeekPeriod) {
861
+ const selectedValue = props.state.value;
862
+ if (selectedValue != null) {
863
+ const weekStart2 = startOfWeek(selectedValue, locale);
864
+ const weekEnd2 = endOfWeek(selectedValue, locale);
865
+ const inWeek = date.compare(weekStart2) >= 0 && date.compare(weekEnd2) <= 0;
866
+ if (inWeek) {
867
+ const isStart = isSameDay(date, weekStart2);
868
+ const isEnd = isSameDay(date, weekEnd2);
869
+ effectiveSelected = isStart || isEnd;
870
+ effectiveRange = isStart ? "start" : isEnd ? "end" : "middle";
871
+ } else {
872
+ effectiveSelected = false;
873
+ effectiveRange = void 0;
874
+ }
875
+ }
876
+ }
877
+ const weekStart = isWeekPeriod ? startOfWeek(date, locale) : null;
878
+ const weekEnd = isWeekPeriod ? endOfWeek(date, locale) : null;
879
+ const isWeekOutOfBounds = minDate != null && weekStart != null && weekStart.compare(minDate) < 0 || maxDate != null && weekEnd != null && weekEnd.compare(maxDate) > 0;
880
+ const enableOutsideDay = isWeekPeriod && isOutsideMonth && !isWeekOutOfBounds;
881
+ const selectWeek = () => {
882
+ if (weekStart != null) {
883
+ onCalendarDateClick(weekStart);
884
+ }
885
+ };
886
+ let weekDayProps = {};
887
+ if (isWeekPeriod && isWeekOutOfBounds) {
888
+ weekDayProps = { "aria-disabled": true, tabIndex: -1 };
889
+ } else if (enableOutsideDay) {
890
+ weekDayProps = {
891
+ "aria-disabled": void 0,
892
+ tabIndex: effectiveRange === "start" ? 0 : -1,
893
+ onClick: selectWeek,
894
+ onKeyDown: (event) => {
895
+ if (event.key === "Enter" || event.key === " ") {
896
+ event.preventDefault();
897
+ event.stopPropagation();
898
+ selectWeek();
899
+ }
900
+ }
901
+ };
902
+ }
903
+ const gridCellProps = isWeekPeriod && (enableOutsideDay || isWeekOutOfBounds) ? { ...cellProps, "aria-disabled": isWeekOutOfBounds ? true : void 0 } : cellProps;
904
+ return /* @__PURE__ */ jsx(StyledCell, { ...gridCellProps, children: /* @__PURE__ */ jsx(
408
905
  StyledCellContent,
409
906
  {
410
907
  ...mergeProps(buttonProps, focusProps),
908
+ ...weekDayProps,
411
909
  ref,
412
- selected: isSelected && (isSelectionStart || isSelectionEnd),
910
+ selected: effectiveSelected,
413
911
  today: isToday(date, getLocalTimeZone()),
414
- range: rangeVariant,
415
- hidden: isOutsideMonth,
912
+ range: effectiveRange,
913
+ hidden: isOutsideMonth && !isWeekPeriod,
914
+ outsideMonth: isOutsideMonth && isWeekPeriod,
915
+ "data-week-hover": isWeekPeriod && effectiveRange == null ? "" : void 0,
416
916
  children: formattedDate
417
917
  }
418
918
  ) });
@@ -456,6 +956,507 @@ const Grid = React.forwardRef(
456
956
  }
457
957
  );
458
958
 
959
+ const StyledPeriodGrid = styled(Primitive.div, {
960
+ display: "grid",
961
+ gap: "$50",
962
+ padding: "$50 0",
963
+ // Match the day grid width: 7 cells × $10 (40px) = 280px
964
+ width: "calc($10 * 7)",
965
+ variants: {
966
+ columns: {
967
+ 2: { gridTemplateColumns: "repeat(2, 1fr)" },
968
+ 3: { gridTemplateColumns: "repeat(3, 1fr)" },
969
+ 4: { gridTemplateColumns: "repeat(4, 1fr)" }
970
+ }
971
+ },
972
+ defaultVariants: {
973
+ columns: 4
974
+ }
975
+ });
976
+ const StyledPeriodRow = styled(Primitive.div, {
977
+ display: "contents"
978
+ });
979
+ const StyledPeriodGridCell = styled(Primitive.div, {
980
+ display: "contents"
981
+ });
982
+ const StyledPeriodCell = styled(BaseButton, {
983
+ justifyContent: "center",
984
+ height: "$10",
985
+ color: "$text-default",
986
+ fontSize: "$200",
987
+ ...focus.css({
988
+ position: "relative",
989
+ zIndex: "1",
990
+ boxShadow: "$focus",
991
+ outline: "1px solid transparent"
992
+ }),
993
+ '&:not([aria-disabled="true"])': {
994
+ _hover: {
995
+ backgroundColor: "$background-interactive-surface-secondary-hover"
996
+ }
997
+ },
998
+ '&[aria-disabled="true"]': {
999
+ color: "$text-disabled"
1000
+ },
1001
+ variants: {
1002
+ current: {
1003
+ true: {
1004
+ backgroundColor: "$background-interactive-surface-secondary-hover",
1005
+ color: "$text-default"
1006
+ }
1007
+ },
1008
+ selected: {
1009
+ true: {
1010
+ backgroundColor: "$background-interactive-selected",
1011
+ color: "$text-on-interactive-primary",
1012
+ "& span": {
1013
+ color: "inherit"
1014
+ },
1015
+ '&:not([aria-disabled="true"])': {
1016
+ _hover: {
1017
+ backgroundColor: "$background-interactive-primary-hover",
1018
+ color: "$text-on-interactive-primary"
1019
+ }
1020
+ }
1021
+ }
1022
+ },
1023
+ tall: {
1024
+ true: {
1025
+ height: "$14",
1026
+ flexDirection: "column",
1027
+ gap: "$25"
1028
+ }
1029
+ }
1030
+ }
1031
+ });
1032
+ const StyledPeriodCellLabel = styled(Primitive.span, {
1033
+ fontSize: "$100",
1034
+ color: "$text-secondary"
1035
+ });
1036
+
1037
+ const useGridNavigation = ({
1038
+ rows,
1039
+ columns,
1040
+ selectedIndex,
1041
+ currentIndex,
1042
+ resetKey,
1043
+ focusOnMount
1044
+ }) => {
1045
+ const cellRefs = useRef(/* @__PURE__ */ new Map());
1046
+ const [focusedRow, setFocusedRow] = useState(0);
1047
+ const [focusedCol, setFocusedCol] = useState(0);
1048
+ const hasFocusedRef = useRef(false);
1049
+ const key = (r, c) => "".concat(r, "-").concat(c);
1050
+ const isEnabled = useCallback((r, c) => {
1051
+ const el = cellRefs.current.get(key(r, c));
1052
+ return el != null && el.getAttribute("aria-disabled") !== "true";
1053
+ }, []);
1054
+ const findFirstEnabled = useCallback(() => {
1055
+ for (let r = 0; r < rows; r++) {
1056
+ for (let c = 0; c < columns; c++) {
1057
+ if (isEnabled(r, c)) {
1058
+ return [r, c];
1059
+ }
1060
+ }
1061
+ }
1062
+ return null;
1063
+ }, [rows, columns, isEnabled]);
1064
+ const focusCell = useCallback(
1065
+ (row, col) => {
1066
+ var _a;
1067
+ if (isEnabled(row, col)) {
1068
+ (_a = cellRefs.current.get(key(row, col))) == null ? void 0 : _a.focus();
1069
+ }
1070
+ },
1071
+ [isEnabled]
1072
+ );
1073
+ const resolveFocusTarget = useCallback(() => {
1074
+ for (const index of [selectedIndex, currentIndex]) {
1075
+ if (index == null || index < 0 || index >= rows * columns) {
1076
+ continue;
1077
+ }
1078
+ const candidate = [
1079
+ Math.floor(index / columns),
1080
+ index % columns
1081
+ ];
1082
+ if (isEnabled(...candidate)) {
1083
+ return candidate;
1084
+ }
1085
+ }
1086
+ return findFirstEnabled();
1087
+ }, [selectedIndex, currentIndex, rows, columns, isEnabled, findFirstEnabled]);
1088
+ useLayoutEffect(() => {
1089
+ const target = resolveFocusTarget();
1090
+ if (target == null) {
1091
+ return;
1092
+ }
1093
+ const [row, col] = target;
1094
+ setFocusedRow(row);
1095
+ setFocusedCol(col);
1096
+ if (focusOnMount === true && !hasFocusedRef.current) {
1097
+ hasFocusedRef.current = true;
1098
+ focusCell(row, col);
1099
+ }
1100
+ }, [resolveFocusTarget, focusCell, focusOnMount, resetKey]);
1101
+ const findNextEnabled = useCallback(
1102
+ (row, col, dr, dc) => {
1103
+ let r = row + dr;
1104
+ let c = col + dc;
1105
+ while (r >= 0 && r < rows && c >= 0 && c < columns) {
1106
+ if (isEnabled(r, c)) {
1107
+ return [r, c];
1108
+ }
1109
+ r += dr;
1110
+ c += dc;
1111
+ }
1112
+ return [row, col];
1113
+ },
1114
+ [rows, columns, isEnabled]
1115
+ );
1116
+ const getCellProps = useCallback(
1117
+ (row, col) => ({
1118
+ tabIndex: row === focusedRow && col === focusedCol ? 0 : -1,
1119
+ ref: (el) => {
1120
+ const k = key(row, col);
1121
+ if (el != null) {
1122
+ cellRefs.current.set(k, el);
1123
+ } else {
1124
+ cellRefs.current.delete(k);
1125
+ }
1126
+ },
1127
+ onFocus: () => {
1128
+ setFocusedRow(row);
1129
+ setFocusedCol(col);
1130
+ },
1131
+ onKeyDown: (e) => {
1132
+ let nextRow = row;
1133
+ let nextCol = col;
1134
+ switch (e.key) {
1135
+ case "ArrowRight":
1136
+ if (col < columns - 1) {
1137
+ [nextRow, nextCol] = findNextEnabled(row, col, 0, 1);
1138
+ }
1139
+ break;
1140
+ case "ArrowLeft":
1141
+ if (col > 0) {
1142
+ [nextRow, nextCol] = findNextEnabled(row, col, 0, -1);
1143
+ }
1144
+ break;
1145
+ case "ArrowDown":
1146
+ if (row < rows - 1) {
1147
+ [nextRow, nextCol] = findNextEnabled(row, col, 1, 0);
1148
+ }
1149
+ break;
1150
+ case "ArrowUp":
1151
+ if (row > 0) {
1152
+ [nextRow, nextCol] = findNextEnabled(row, col, -1, 0);
1153
+ }
1154
+ break;
1155
+ case "Home": {
1156
+ for (let r = 0; r < rows; r++) {
1157
+ for (let c = 0; c < columns; c++) {
1158
+ if (isEnabled(r, c)) {
1159
+ [nextRow, nextCol] = [r, c];
1160
+ r = rows;
1161
+ break;
1162
+ }
1163
+ }
1164
+ }
1165
+ break;
1166
+ }
1167
+ case "End": {
1168
+ for (let r = rows - 1; r >= 0; r--) {
1169
+ for (let c = columns - 1; c >= 0; c--) {
1170
+ if (isEnabled(r, c)) {
1171
+ [nextRow, nextCol] = [r, c];
1172
+ r = -1;
1173
+ break;
1174
+ }
1175
+ }
1176
+ }
1177
+ break;
1178
+ }
1179
+ default:
1180
+ return;
1181
+ }
1182
+ if (nextRow !== row || nextCol !== col) {
1183
+ e.preventDefault();
1184
+ focusCell(nextRow, nextCol);
1185
+ }
1186
+ }
1187
+ }),
1188
+ [
1189
+ focusedRow,
1190
+ focusedCol,
1191
+ rows,
1192
+ columns,
1193
+ focusCell,
1194
+ findNextEnabled,
1195
+ isEnabled
1196
+ ]
1197
+ );
1198
+ return { getCellProps };
1199
+ };
1200
+
1201
+ const ROWS$2 = 3;
1202
+ const COLUMNS$2 = 4;
1203
+ const MonthGrid = ({
1204
+ navigationYear,
1205
+ timeZone,
1206
+ onMonthSelect,
1207
+ currentMonth,
1208
+ currentYear,
1209
+ selectedMonth,
1210
+ selectedYear,
1211
+ minDate,
1212
+ maxDate,
1213
+ yearFormatter,
1214
+ focusOnMount
1215
+ }) => {
1216
+ const monthFormatter = useDateFormatter({
1217
+ month: "short",
1218
+ timeZone
1219
+ });
1220
+ const { getCellProps } = useGridNavigation({
1221
+ rows: ROWS$2,
1222
+ columns: COLUMNS$2,
1223
+ selectedIndex: selectedYear === navigationYear && selectedMonth != null ? selectedMonth - 1 : -1,
1224
+ currentIndex: currentYear === navigationYear && currentMonth != null ? currentMonth - 1 : -1,
1225
+ resetKey: navigationYear,
1226
+ focusOnMount
1227
+ });
1228
+ const isMonthDisabled = (month) => {
1229
+ if (minDate != null) {
1230
+ const minYear = minDate.year;
1231
+ const minMonth = minDate.month;
1232
+ if (navigationYear < minYear || navigationYear === minYear && month < minMonth) {
1233
+ return true;
1234
+ }
1235
+ }
1236
+ if (maxDate != null) {
1237
+ const maxYear = maxDate.year;
1238
+ const maxMonth = maxDate.month;
1239
+ if (navigationYear > maxYear || navigationYear === maxYear && month > maxMonth) {
1240
+ return true;
1241
+ }
1242
+ }
1243
+ return false;
1244
+ };
1245
+ return /* @__PURE__ */ jsx(
1246
+ StyledPeriodGrid,
1247
+ {
1248
+ columns: 4,
1249
+ role: "grid",
1250
+ "aria-label": yearFormatter.format(navigationYear),
1251
+ children: Array.from({ length: ROWS$2 }, (_, row) => /* @__PURE__ */ jsx(StyledPeriodRow, { role: "row", children: Array.from({ length: COLUMNS$2 }, (_2, col) => {
1252
+ const month = row * COLUMNS$2 + col + 1;
1253
+ const date = new CalendarDate(navigationYear, month, 1).toDate(
1254
+ timeZone
1255
+ );
1256
+ const label = monthFormatter.format(date);
1257
+ const disabled = isMonthDisabled(month);
1258
+ const isCurrent = navigationYear === currentYear && month === currentMonth;
1259
+ const isSelected = navigationYear === selectedYear && month === selectedMonth;
1260
+ const cellProps = getCellProps(row, col);
1261
+ return /* @__PURE__ */ jsx(
1262
+ StyledPeriodGridCell,
1263
+ {
1264
+ role: "gridcell",
1265
+ "aria-selected": isSelected || void 0,
1266
+ children: /* @__PURE__ */ jsx(
1267
+ StyledPeriodCell,
1268
+ {
1269
+ ...cellProps,
1270
+ current: isCurrent,
1271
+ selected: isSelected,
1272
+ "aria-current": isCurrent ? "date" : void 0,
1273
+ "aria-disabled": disabled || void 0,
1274
+ "aria-label": "".concat(label, " ").concat(yearFormatter.format(
1275
+ navigationYear
1276
+ )),
1277
+ onPress: () => onMonthSelect(month),
1278
+ children: label
1279
+ }
1280
+ )
1281
+ },
1282
+ month
1283
+ );
1284
+ }) }, row))
1285
+ }
1286
+ );
1287
+ };
1288
+
1289
+ const ROWS$1 = 2;
1290
+ const COLUMNS$1 = 2;
1291
+ const QUARTER_MONTHS = [
1292
+ [1, 3],
1293
+ [4, 6],
1294
+ [7, 9],
1295
+ [10, 12]
1296
+ ];
1297
+ const QuarterGrid = ({
1298
+ navigationYear,
1299
+ timeZone,
1300
+ onQuarterSelect,
1301
+ currentQuarter,
1302
+ currentYear,
1303
+ selectedQuarter,
1304
+ selectedYear,
1305
+ minDate,
1306
+ maxDate,
1307
+ formatQuarterLabel,
1308
+ yearFormatter,
1309
+ focusOnMount
1310
+ }) => {
1311
+ const strings = useCalendarStrings();
1312
+ const monthFormatter = useDateFormatter({
1313
+ month: "short",
1314
+ timeZone
1315
+ });
1316
+ const { getCellProps } = useGridNavigation({
1317
+ rows: ROWS$1,
1318
+ columns: COLUMNS$1,
1319
+ selectedIndex: selectedYear === navigationYear && selectedQuarter != null ? selectedQuarter - 1 : -1,
1320
+ currentIndex: currentYear === navigationYear && currentQuarter != null ? currentQuarter - 1 : -1,
1321
+ resetKey: navigationYear,
1322
+ focusOnMount
1323
+ });
1324
+ const isQuarterDisabled = (quarter) => {
1325
+ const [startMonth, endMonth] = QUARTER_MONTHS[quarter - 1];
1326
+ if (minDate != null) {
1327
+ if (navigationYear < minDate.year || navigationYear === minDate.year && endMonth < minDate.month) {
1328
+ return true;
1329
+ }
1330
+ }
1331
+ if (maxDate != null) {
1332
+ if (navigationYear > maxDate.year || navigationYear === maxDate.year && startMonth > maxDate.month) {
1333
+ return true;
1334
+ }
1335
+ }
1336
+ return false;
1337
+ };
1338
+ const formatRange = (quarter) => {
1339
+ const [startMonth, endMonth] = QUARTER_MONTHS[quarter - 1];
1340
+ const start = monthFormatter.format(
1341
+ new CalendarDate(navigationYear, startMonth, 1).toDate(timeZone)
1342
+ );
1343
+ const end = monthFormatter.format(
1344
+ new CalendarDate(navigationYear, endMonth, 1).toDate(timeZone)
1345
+ );
1346
+ return "".concat(start, " \u2013 ").concat(end);
1347
+ };
1348
+ return /* @__PURE__ */ jsx(
1349
+ StyledPeriodGrid,
1350
+ {
1351
+ columns: 2,
1352
+ role: "grid",
1353
+ "aria-label": yearFormatter.format(navigationYear),
1354
+ children: Array.from({ length: ROWS$1 }, (_, row) => /* @__PURE__ */ jsx(StyledPeriodRow, { role: "row", children: Array.from({ length: COLUMNS$1 }, (_2, col) => {
1355
+ const quarter = row * COLUMNS$1 + col + 1;
1356
+ const disabled = isQuarterDisabled(quarter);
1357
+ const isCurrent = navigationYear === currentYear && quarter === currentQuarter;
1358
+ const isSelected = navigationYear === selectedYear && quarter === selectedQuarter;
1359
+ const cellProps = getCellProps(row, col);
1360
+ const rangeLabel = formatRange(quarter);
1361
+ const label = (formatQuarterLabel != null ? formatQuarterLabel : strings.quarter)(quarter);
1362
+ return /* @__PURE__ */ jsx(
1363
+ StyledPeriodGridCell,
1364
+ {
1365
+ role: "gridcell",
1366
+ "aria-selected": isSelected || void 0,
1367
+ children: /* @__PURE__ */ jsxs(
1368
+ StyledPeriodCell,
1369
+ {
1370
+ ...cellProps,
1371
+ current: isCurrent,
1372
+ selected: isSelected,
1373
+ tall: true,
1374
+ "aria-current": isCurrent ? "date" : void 0,
1375
+ "aria-disabled": disabled || void 0,
1376
+ "aria-label": "".concat(label, " ").concat(yearFormatter.format(
1377
+ navigationYear
1378
+ ), ", ").concat(rangeLabel),
1379
+ onPress: () => onQuarterSelect(quarter),
1380
+ children: [
1381
+ label,
1382
+ /* @__PURE__ */ jsx(StyledPeriodCellLabel, { children: rangeLabel })
1383
+ ]
1384
+ }
1385
+ )
1386
+ },
1387
+ quarter
1388
+ );
1389
+ }) }, row))
1390
+ }
1391
+ );
1392
+ };
1393
+
1394
+ const ROWS = 3;
1395
+ const COLUMNS = 4;
1396
+ const TOTAL_CELLS = ROWS * COLUMNS;
1397
+ const YearGrid = ({
1398
+ decadeStart,
1399
+ onYearSelect,
1400
+ currentYear,
1401
+ selectedYear,
1402
+ minDate,
1403
+ maxDate,
1404
+ yearFormatter,
1405
+ focusOnMount
1406
+ }) => {
1407
+ const { getCellProps } = useGridNavigation({
1408
+ rows: ROWS,
1409
+ columns: COLUMNS,
1410
+ selectedIndex: selectedYear != null ? selectedYear - decadeStart : -1,
1411
+ currentIndex: currentYear != null ? currentYear - decadeStart : -1,
1412
+ resetKey: decadeStart,
1413
+ focusOnMount
1414
+ });
1415
+ const isYearDisabled = (year) => {
1416
+ if (minDate != null && year < minDate.year) return true;
1417
+ if (maxDate != null && year > maxDate.year) return true;
1418
+ return false;
1419
+ };
1420
+ return /* @__PURE__ */ jsx(
1421
+ StyledPeriodGrid,
1422
+ {
1423
+ columns: 4,
1424
+ role: "grid",
1425
+ "aria-label": "".concat(yearFormatter.format(decadeStart), "\u2013").concat(yearFormatter.format(
1426
+ decadeStart + TOTAL_CELLS - 1
1427
+ )),
1428
+ children: Array.from({ length: ROWS }, (_, row) => /* @__PURE__ */ jsx(StyledPeriodRow, { role: "row", children: Array.from({ length: COLUMNS }, (_2, col) => {
1429
+ const year = decadeStart + row * COLUMNS + col;
1430
+ const disabled = isYearDisabled(year);
1431
+ const isCurrent = year === currentYear;
1432
+ const isSelected = year === selectedYear;
1433
+ const cellProps = getCellProps(row, col);
1434
+ return /* @__PURE__ */ jsx(
1435
+ StyledPeriodGridCell,
1436
+ {
1437
+ role: "gridcell",
1438
+ "aria-selected": isSelected || void 0,
1439
+ children: /* @__PURE__ */ jsx(
1440
+ StyledPeriodCell,
1441
+ {
1442
+ ...cellProps,
1443
+ current: isCurrent,
1444
+ selected: isSelected,
1445
+ "aria-current": isCurrent ? "date" : void 0,
1446
+ "aria-disabled": disabled || void 0,
1447
+ "aria-label": yearFormatter.format(year),
1448
+ onPress: () => onYearSelect(year),
1449
+ children: yearFormatter.format(year)
1450
+ }
1451
+ )
1452
+ },
1453
+ year
1454
+ );
1455
+ }) }, row))
1456
+ }
1457
+ );
1458
+ };
1459
+
459
1460
  const StyledPredefinedRanges = styled(Primitive.div, {
460
1461
  display: "flex",
461
1462
  flexDirection: "column",
@@ -500,9 +1501,31 @@ const PredefinedRanges = () => {
500
1501
  };
501
1502
 
502
1503
  const CONTENT_OFFSET = parseInt(theme.space[50]);
1504
+ const VIEW_MODE_UP = {
1505
+ days: "months",
1506
+ months: "years",
1507
+ quarters: "years",
1508
+ years: "years"
1509
+ };
503
1510
  const SharedContent = React.forwardRef(
504
1511
  ({ calendarProps, prevButtonProps, nextButtonProps, state }, forwardRef) => {
505
- const { visibleDuration } = useCalendarContext();
1512
+ const {
1513
+ visibleDuration,
1514
+ viewMode,
1515
+ setViewMode,
1516
+ navigationYear,
1517
+ setNavigationYear,
1518
+ navigation,
1519
+ period,
1520
+ picker,
1521
+ minDate,
1522
+ maxDate,
1523
+ onCalendarDateClick,
1524
+ formatQuarterLabel,
1525
+ goToCurrentLabel,
1526
+ value: selectedValue
1527
+ } = useCalendarContext();
1528
+ const { locale } = useLocale();
506
1529
  const monthDateFormatter = useDateFormatter({
507
1530
  month: "long",
508
1531
  year: "numeric",
@@ -514,8 +1537,185 @@ const SharedContent = React.forwardRef(
514
1537
  const nextAriaLabel = "".concat(nextButtonProps["aria-label"], ", ").concat(monthDateFormatter.format(
515
1538
  state.visibleRange.start.add({ months: visibleDuration.months === 2 ? 2 : 1 }).toDate(state.timeZone)
516
1539
  ));
1540
+ const todayDate = useMemo(() => today(getLocalTimeZone()), []);
1541
+ const currentYear = todayDate.year;
1542
+ const currentMonth = todayDate.month;
1543
+ const yearFormatter = useMemo(
1544
+ () => new Intl.NumberFormat(locale, { useGrouping: false }),
1545
+ [locale]
1546
+ );
1547
+ const handleZoomOut = useCallback(() => {
1548
+ if (viewMode === "days") {
1549
+ setNavigationYear(state.visibleRange.start.year);
1550
+ }
1551
+ setViewMode(VIEW_MODE_UP[viewMode]);
1552
+ }, [
1553
+ viewMode,
1554
+ setViewMode,
1555
+ setNavigationYear,
1556
+ state.visibleRange.start.year
1557
+ ]);
1558
+ const handleMonthSelect = useCallback(
1559
+ (month) => {
1560
+ if (period === "month") {
1561
+ const date = new CalendarDate(navigationYear, month, 1);
1562
+ onCalendarDateClick(date);
1563
+ } else {
1564
+ state.setFocusedDate(new CalendarDate(navigationYear, month, 1));
1565
+ state.setFocused(true);
1566
+ setViewMode("days");
1567
+ }
1568
+ },
1569
+ [navigationYear, state, setViewMode, period, onCalendarDateClick]
1570
+ );
1571
+ const handleQuarterSelect = useCallback(
1572
+ (quarter) => {
1573
+ const firstMonth = (quarter - 1) * 3 + 1;
1574
+ const date = new CalendarDate(navigationYear, firstMonth, 1);
1575
+ onCalendarDateClick(date);
1576
+ },
1577
+ [navigationYear, onCalendarDateClick]
1578
+ );
1579
+ const handleYearSelect = useCallback(
1580
+ (year) => {
1581
+ setNavigationYear(year);
1582
+ setViewMode(period === "quarter" ? "quarters" : "months");
1583
+ },
1584
+ [setNavigationYear, setViewMode, period]
1585
+ );
1586
+ const handlePrevOverride = useCallback(() => {
1587
+ if (viewMode === "years") {
1588
+ setNavigationYear((y) => y - DECADE_SIZE);
1589
+ } else {
1590
+ setNavigationYear((y) => y - 1);
1591
+ }
1592
+ }, [viewMode, setNavigationYear]);
1593
+ const handleNextOverride = useCallback(() => {
1594
+ if (viewMode === "years") {
1595
+ setNavigationYear((y) => y + DECADE_SIZE);
1596
+ } else {
1597
+ setNavigationYear((y) => y + 1);
1598
+ }
1599
+ }, [viewMode, setNavigationYear]);
1600
+ const decadeStart = Math.floor(navigationYear / DECADE_SIZE) * DECADE_SIZE;
1601
+ const [isPrevDisabled, isNextDisabled] = useMemo(() => {
1602
+ if (viewMode === "days") {
1603
+ return [void 0, void 0];
1604
+ }
1605
+ if (viewMode === "years") {
1606
+ return [
1607
+ minDate != null && decadeStart - 1 < minDate.year,
1608
+ maxDate != null && decadeStart + DECADE_SIZE > maxDate.year
1609
+ ];
1610
+ }
1611
+ return [
1612
+ minDate != null && navigationYear - 1 < minDate.year,
1613
+ maxDate != null && navigationYear + 1 > maxDate.year
1614
+ ];
1615
+ }, [viewMode, decadeStart, navigationYear, minDate, maxDate]);
1616
+ const currentQuarter = Math.ceil(currentMonth / 3);
1617
+ const handleGoToCurrent = useCallback(() => {
1618
+ const now = today(getLocalTimeZone());
1619
+ let date;
1620
+ switch (period) {
1621
+ case "week":
1622
+ date = startOfWeek(now, locale);
1623
+ break;
1624
+ case "month":
1625
+ date = new CalendarDate(currentYear, currentMonth, 1);
1626
+ break;
1627
+ case "quarter": {
1628
+ const firstMonth = (currentQuarter - 1) * 3 + 1;
1629
+ date = new CalendarDate(currentYear, firstMonth, 1);
1630
+ break;
1631
+ }
1632
+ default:
1633
+ date = now;
1634
+ break;
1635
+ }
1636
+ onCalendarDateClick(date);
1637
+ }, [
1638
+ period,
1639
+ locale,
1640
+ currentYear,
1641
+ currentMonth,
1642
+ currentQuarter,
1643
+ onCalendarDateClick
1644
+ ]);
1645
+ const prevViewModeRef = useRef(viewMode);
1646
+ const shouldAutoFocus = prevViewModeRef.current !== viewMode && viewMode !== "days";
1647
+ useEffect(() => {
1648
+ prevViewModeRef.current = viewMode;
1649
+ }, [viewMode]);
1650
+ const selectedDate = selectedValue != null && !("start" in selectedValue) ? selectedValue : null;
1651
+ const selectedYear = selectedDate == null ? void 0 : selectedDate.year;
1652
+ const selectedMonth = selectedDate == null ? void 0 : selectedDate.month;
1653
+ const selectedQuarter = selectedMonth != null ? Math.ceil(selectedMonth / 3) : void 0;
1654
+ const isSelectionInCurrentPeriod = useMemo(
1655
+ () => selectedDate != null && isCurrentPeriod(selectedDate, period, locale),
1656
+ [selectedDate, period, locale]
1657
+ );
1658
+ const showGoToCurrent = goToCurrentLabel != null && picker === "single" && !isSelectionInCurrentPeriod;
1659
+ const renderGrid = () => {
1660
+ switch (viewMode) {
1661
+ case "months":
1662
+ return /* @__PURE__ */ jsx(
1663
+ MonthGrid,
1664
+ {
1665
+ navigationYear,
1666
+ timeZone: state.timeZone,
1667
+ onMonthSelect: handleMonthSelect,
1668
+ currentMonth,
1669
+ currentYear,
1670
+ selectedMonth,
1671
+ selectedYear,
1672
+ minDate,
1673
+ maxDate,
1674
+ yearFormatter,
1675
+ focusOnMount: shouldAutoFocus
1676
+ }
1677
+ );
1678
+ case "quarters":
1679
+ return /* @__PURE__ */ jsx(
1680
+ QuarterGrid,
1681
+ {
1682
+ navigationYear,
1683
+ timeZone: state.timeZone,
1684
+ onQuarterSelect: handleQuarterSelect,
1685
+ currentQuarter,
1686
+ currentYear,
1687
+ selectedQuarter,
1688
+ selectedYear,
1689
+ minDate,
1690
+ maxDate,
1691
+ formatQuarterLabel,
1692
+ yearFormatter,
1693
+ focusOnMount: shouldAutoFocus
1694
+ }
1695
+ );
1696
+ case "years":
1697
+ return /* @__PURE__ */ jsx(
1698
+ YearGrid,
1699
+ {
1700
+ decadeStart,
1701
+ onYearSelect: handleYearSelect,
1702
+ currentYear,
1703
+ selectedYear,
1704
+ minDate,
1705
+ maxDate,
1706
+ yearFormatter,
1707
+ focusOnMount: shouldAutoFocus
1708
+ }
1709
+ );
1710
+ default:
1711
+ return /* @__PURE__ */ jsxs(StyledGridContent, { children: [
1712
+ /* @__PURE__ */ jsx(StyledBodyContentLeft, { children: /* @__PURE__ */ jsx(Grid, { state }) }),
1713
+ visibleDuration.months === 2 && /* @__PURE__ */ jsx(StyledBodyContentRight, { children: /* @__PURE__ */ jsx(Grid, { state, offset: { months: 1 } }) })
1714
+ ] });
1715
+ }
1716
+ };
517
1717
  return /* @__PURE__ */ jsxs(StyledCalendarBody, { ...calendarProps, ref: forwardRef, children: [
518
- /* @__PURE__ */ jsx(PredefinedRanges, {}),
1718
+ viewMode === "days" && /* @__PURE__ */ jsx(PredefinedRanges, {}),
519
1719
  /* @__PURE__ */ jsxs(Flex, { direction: "column", children: [
520
1720
  /* @__PURE__ */ jsx(
521
1721
  Header,
@@ -529,12 +1729,19 @@ const SharedContent = React.forwardRef(
529
1729
  ...nextButtonProps,
530
1730
  "aria-label": nextAriaLabel
531
1731
  },
532
- visibleMonths: visibleDuration.months
1732
+ visibleMonths: visibleDuration.months,
1733
+ yearFormatter,
1734
+ onPrevOverride: handlePrevOverride,
1735
+ onNextOverride: handleNextOverride,
1736
+ isPrevDisabled,
1737
+ isNextDisabled,
1738
+ onZoomOut: navigation === true ? handleZoomOut : void 0
533
1739
  }
534
1740
  ),
535
- /* @__PURE__ */ jsxs(StyledGridContent, { children: [
536
- /* @__PURE__ */ jsx(StyledBodyContentLeft, { children: /* @__PURE__ */ jsx(Grid, { state }) }),
537
- visibleDuration.months === 2 && /* @__PURE__ */ jsx(StyledBodyContentRight, { children: /* @__PURE__ */ jsx(Grid, { state, offset: { months: 1 } }) })
1741
+ renderGrid(),
1742
+ showGoToCurrent && /* @__PURE__ */ jsxs(StyledGoToCurrent, { onPress: handleGoToCurrent, children: [
1743
+ /* @__PURE__ */ jsx(IconClock, { "aria-hidden": "true" }),
1744
+ goToCurrentLabel
538
1745
  ] })
539
1746
  ] })
540
1747
  ] });
@@ -547,15 +1754,31 @@ const DatePickerContent = () => {
547
1754
  visibleDuration,
548
1755
  minDate,
549
1756
  maxDate,
550
- onCalendarDateClick
1757
+ onCalendarDateClick,
1758
+ period
551
1759
  } = useCalendarContext();
1760
+ const handleChange = useCallback(
1761
+ (date) => {
1762
+ if (period === "week") {
1763
+ const weekStart = startOfWeek(date, locale);
1764
+ const weekEnd = weekStart.add({ days: 6 });
1765
+ if (minDate != null && weekStart.compare(minDate) < 0 || maxDate != null && weekEnd.compare(maxDate) > 0) {
1766
+ return;
1767
+ }
1768
+ onCalendarDateClick(weekStart);
1769
+ } else {
1770
+ onCalendarDateClick(date);
1771
+ }
1772
+ },
1773
+ [period, locale, minDate, maxDate, onCalendarDateClick]
1774
+ );
552
1775
  const state = useCalendarState({
553
1776
  value: calendarValue,
554
1777
  visibleDuration,
555
1778
  minValue: minDate,
556
1779
  maxValue: maxDate,
557
1780
  autoFocus: true,
558
- onChange: onCalendarDateClick,
1781
+ onChange: handleChange,
559
1782
  locale,
560
1783
  createCalendar
561
1784
  });
@@ -652,44 +1875,6 @@ const Content = React.forwardRef(
652
1875
  }
653
1876
  );
654
1877
 
655
- const StyledValue = styled(Primitive.span, {});
656
-
657
- const DEFAULT_FORMAT = {
658
- day: "2-digit",
659
- month: "short",
660
- year: "numeric"
661
- };
662
- function formatDate(date, formatter, timeZone) {
663
- return formatter.format(date.toDate(timeZone));
664
- }
665
- function formatValue(value, formatter, timeZone) {
666
- if (value == null || value === void 0) {
667
- return void 0;
668
- }
669
- if ("start" in value) {
670
- if (value.start == null || value.end == null) {
671
- return void 0;
672
- }
673
- return "".concat(formatDate(value.start, formatter, timeZone), " - ").concat(formatDate(
674
- value.end,
675
- formatter,
676
- timeZone
677
- ));
678
- }
679
- return formatDate(value, formatter, timeZone);
680
- }
681
- const Value = React.forwardRef(({ format = DEFAULT_FORMAT, ...restProps }, forwardRef) => {
682
- const { value } = useCalendarContext();
683
- const timeZone = useMemo(getLocalTimeZone, [format]);
684
- const formatWithTimeZone = useMemo(
685
- () => ({ ...format, timeZone }),
686
- [format, timeZone]
687
- );
688
- const dateFormatter = useDateFormatter(formatWithTimeZone);
689
- const valueToDisplay = formatValue(value, dateFormatter, timeZone);
690
- return /* @__PURE__ */ jsx(StyledValue, { ref: forwardRef, ...restProps, children: valueToDisplay });
691
- });
692
-
693
1878
  const ClearButton = React.forwardRef(({ onClear, children, ...restProps }, forwardRef) => {
694
1879
  const { value, setValue } = useCalendarContext();
695
1880
  const onClearHandler = () => {
@@ -780,6 +1965,11 @@ const Calendar = React.forwardRef(
780
1965
  "aria-disabled": ariaDisabled,
781
1966
  children,
782
1967
  predefinedValues,
1968
+ navigation,
1969
+ period,
1970
+ formatQuarterLabel,
1971
+ formatSwitchViewLabel,
1972
+ goToCurrentLabel,
783
1973
  ...props
784
1974
  }, forwardRef) => {
785
1975
  const providerProps = {
@@ -796,7 +1986,12 @@ const Calendar = React.forwardRef(
796
1986
  onOpen,
797
1987
  disabled,
798
1988
  ariaDisabled,
799
- predefinedValues
1989
+ predefinedValues,
1990
+ navigation,
1991
+ period,
1992
+ formatQuarterLabel,
1993
+ formatSwitchViewLabel,
1994
+ goToCurrentLabel
800
1995
  };
801
1996
  const validatedLocale = getValidLocale(locale);
802
1997
  return /* @__PURE__ */ jsx(I18nProvider, { locale: validatedLocale, children: /* @__PURE__ */ jsx(CalendarProvider, { ...providerProps, children: /* @__PURE__ */ jsx(Root, { ...props, ref: forwardRef, children }) }) });
@@ -811,7 +2006,12 @@ const CalendarGrid = ({
811
2006
  visibleMonths,
812
2007
  minDate,
813
2008
  maxDate,
814
- predefinedValues
2009
+ predefinedValues,
2010
+ navigation,
2011
+ period,
2012
+ formatQuarterLabel,
2013
+ formatSwitchViewLabel,
2014
+ goToCurrentLabel
815
2015
  }) => {
816
2016
  const providerProps = {
817
2017
  picker,
@@ -821,7 +2021,12 @@ const CalendarGrid = ({
821
2021
  visibleMonths,
822
2022
  minDate,
823
2023
  maxDate,
824
- predefinedValues
2024
+ predefinedValues,
2025
+ navigation,
2026
+ period,
2027
+ formatQuarterLabel,
2028
+ formatSwitchViewLabel,
2029
+ goToCurrentLabel
825
2030
  };
826
2031
  const validatedLocale = getValidLocale(locale);
827
2032
  return /* @__PURE__ */ jsx(I18nProvider, { locale: validatedLocale, children: /* @__PURE__ */ jsx(CalendarProvider, { ...providerProps, children: /* @__PURE__ */ jsx(CalendarGridContent, {}) }) });