@eifi1/ui-kit 0.4.1 → 0.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/LICENSE +32 -0
- package/README.md +498 -57
- package/dist/chart.d.ts +10 -0
- package/dist/chart.js +10 -0
- package/dist/chart.js.map +1 -0
- package/dist/components/account-settings.d.ts +11 -0
- package/dist/components/account-settings.js +19 -12
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/alert-banner.d.ts +10 -6
- package/dist/components/alert-banner.js +20 -16
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +30 -4
- package/dist/components/amount-input.js +36 -14
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/calculator.d.ts +24 -7
- package/dist/components/calculator.js +37 -20
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.d.ts +156 -0
- package/dist/components/chart-zoom.js +262 -0
- package/dist/components/chart-zoom.js.map +1 -0
- package/dist/components/chart.d.ts +28 -3
- package/dist/components/chart.js +71 -32
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox.d.ts +51 -0
- package/dist/components/checkbox.js +123 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/chip.d.ts +128 -0
- package/dist/components/chip.js +357 -0
- package/dist/components/chip.js.map +1 -0
- package/dist/components/choice-parts.d.ts +24 -0
- package/dist/components/choice-parts.js +18 -0
- package/dist/components/choice-parts.js.map +1 -0
- package/dist/components/combobox-core.d.ts +52 -9
- package/dist/components/combobox-core.js +89 -19
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +43 -33
- package/dist/components/combobox.js +409 -340
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.d.ts +48 -8
- package/dist/components/currency-select.js +107 -65
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +15 -3
- package/dist/components/data-table-filter-popover.js +23 -17
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +3 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +53 -2
- package/dist/components/data-table-labels.js +31 -2
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table-pagination.js +16 -10
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-sort.js +1 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +201 -85
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +25 -9
- package/dist/components/date-picker.js +232 -94
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dropdown.d.ts +74 -19
- package/dist/components/dropdown.js +72 -22
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +5 -4
- package/dist/components/entity-combobox.js +107 -76
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +36 -0
- package/dist/components/facing-pair.js +40 -0
- package/dist/components/facing-pair.js.map +1 -0
- package/dist/components/field-sync.d.ts +164 -0
- package/dist/components/field-sync.js +264 -0
- package/dist/components/field-sync.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +21 -7
- package/dist/components/file-dropzone.js +28 -7
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +37 -26
- package/dist/components/full-bleed-dialog.js +20 -5
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/grouped-picker.d.ts +21 -10
- package/dist/components/grouped-picker.js +66 -52
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +20 -4
- package/dist/components/hover-menu.js +15 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +102 -14
- package/dist/components/mini-calendar.js +256 -70
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +9 -3
- package/dist/components/modal.js +34 -45
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +79 -0
- package/dist/components/month-picker.js +342 -0
- package/dist/components/month-picker.js.map +1 -0
- package/dist/components/multi-entity-combobox.d.ts +7 -6
- package/dist/components/multi-entity-combobox.js +112 -72
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +13 -11
- package/dist/components/multi-select.js +161 -84
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +92 -0
- package/dist/components/number-field.js +111 -0
- package/dist/components/number-field.js.map +1 -0
- package/dist/components/number-input.d.ts +21 -2
- package/dist/components/number-input.js +2 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +25 -4
- package/dist/components/numpad-sheet.js +30 -13
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-contents.d.ts +90 -0
- package/dist/components/page-contents.js +170 -0
- package/dist/components/page-contents.js.map +1 -0
- package/dist/components/password-strength.d.ts +106 -0
- package/dist/components/password-strength.js +132 -0
- package/dist/components/password-strength.js.map +1 -0
- package/dist/components/picker-sheet.d.ts +23 -15
- package/dist/components/picker-sheet.js +34 -14
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.d.ts +62 -4
- package/dist/components/popover.js +52 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/search-field.d.ts +25 -7
- package/dist/components/search-field.js +7 -5
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +14 -0
- package/dist/components/series-chart-labels.js +11 -0
- package/dist/components/series-chart-labels.js.map +1 -0
- package/dist/components/series-chart.d.ts +207 -0
- package/dist/components/series-chart.js +223 -0
- package/dist/components/series-chart.js.map +1 -0
- package/dist/components/settings-fields.d.ts +14 -4
- package/dist/components/settings-fields.js +19 -8
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/signature-pad.d.ts +119 -0
- package/dist/components/signature-pad.js +405 -0
- package/dist/components/signature-pad.js.map +1 -0
- package/dist/components/slider.d.ts +80 -0
- package/dist/components/slider.js +155 -0
- package/dist/components/slider.js.map +1 -0
- package/dist/components/sparkline.d.ts +93 -0
- package/dist/components/sparkline.js +205 -0
- package/dist/components/sparkline.js.map +1 -0
- package/dist/components/stat-tile.d.ts +151 -0
- package/dist/components/stat-tile.js +236 -0
- package/dist/components/stat-tile.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +25 -2
- package/dist/components/swipeable-row.js +31 -0
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +60 -0
- package/dist/components/switch.js +81 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/time-input.d.ts +58 -0
- package/dist/components/time-input.js +50 -0
- package/dist/components/time-input.js.map +1 -0
- package/dist/components/toggle-group.d.ts +18 -3
- package/dist/components/toggle-group.js +10 -6
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +115 -0
- package/dist/components/toggle-legend.js +114 -0
- package/dist/components/toggle-legend.js.map +1 -0
- package/dist/components/tooltip.d.ts +40 -11
- package/dist/components/tooltip.js +77 -20
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/treemap.d.ts +132 -0
- package/dist/components/treemap.js +179 -0
- package/dist/components/treemap.js.map +1 -0
- package/dist/components/ui.d.ts +159 -46
- package/dist/components/ui.js +220 -115
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-mobile-reveal.js +1 -0
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/use-table-state.js +38 -17
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.d.ts +5 -3
- package/dist/components/user-avatar.js +4 -2
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/components/wizard-stepper.js +3 -2
- package/dist/components/wizard-stepper.js.map +1 -1
- package/dist/{data-table-filters-aXyf0Xub.d.ts → data-table-filters-DM1auUfH.d.ts} +27 -5
- package/dist/data-table.d.ts +7 -0
- package/dist/data-table.js +12 -0
- package/dist/data-table.js.map +1 -0
- package/dist/feedback/feedback-attachment.js +12 -11
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.js +2 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +33 -7
- package/dist/feedback/feedback-inbox.js +49 -37
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback.d.ts +5 -0
- package/dist/feedback.js +5 -0
- package/dist/feedback.js.map +1 -0
- package/dist/hooks/use-anchored-panel.js +1 -0
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js +1 -0
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-announce.d.ts +46 -0
- package/dist/hooks/use-announce.js +37 -0
- package/dist/hooks/use-announce.js.map +1 -0
- package/dist/hooks/use-body-scroll-lock.js +1 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -1
- package/dist/hooks/use-close-transition.js +1 -0
- package/dist/hooks/use-close-transition.js.map +1 -1
- package/dist/hooks/use-dismiss.js +1 -0
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +16 -0
- package/dist/hooks/use-focus-trap.js +67 -0
- package/dist/hooks/use-focus-trap.js.map +1 -0
- package/dist/hooks/use-media-query.js +1 -0
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +27 -1
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.js +1 -0
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.d.ts +31 -0
- package/dist/i18n/defaults.js +63 -0
- package/dist/i18n/defaults.js.map +1 -0
- package/dist/i18n/kit-labels.d.ts +250 -0
- package/dist/i18n/kit-labels.js +184 -0
- package/dist/i18n/kit-labels.js.map +1 -0
- package/dist/index.d.ts +47 -22
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/calc.js +1 -0
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/cn.js +1 -0
- package/dist/lib/cn.js.map +1 -1
- package/dist/lib/dates.js +1 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/logger.js +1 -0
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/safe-storage.js +1 -0
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/search/command-palette.d.ts +11 -1
- package/dist/search/command-palette.js +21 -15
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search.d.ts +2 -0
- package/dist/search.js +3 -0
- package/dist/search.js.map +1 -0
- package/dist/shell/app-shell.d.ts +33 -4
- package/dist/shell/app-shell.js +275 -78
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/option-switcher-menu.js +3 -2
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +6 -0
- package/dist/shell/role-switcher.js +7 -4
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar.d.ts +15 -7
- package/dist/shell/top-bar.js +3 -5
- package/dist/shell/top-bar.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +3 -2
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +5 -2
- package/dist/shell/topbar-controls.js +20 -15
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell.d.ts +13 -0
- package/dist/shell.js +8 -0
- package/dist/shell.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +19 -12
- package/dist/theme/chart-palette.js +24 -30
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/color.d.ts +78 -0
- package/dist/theme/color.js +148 -0
- package/dist/theme/color.js.map +1 -0
- package/dist/theme/palette-derive.d.ts +140 -0
- package/dist/theme/palette-derive.js +376 -0
- package/dist/theme/palette-derive.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +14 -1
- package/dist/theme/palette-presets.js +231 -3
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js +1 -0
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/theme-store.js +1 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour.d.ts +2 -1
- package/dist/tour/tour.js +15 -9
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +3 -0
- package/dist/tour.js.map +1 -0
- package/dist/wizard/stepper-nav.js +10 -8
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +18 -2
- package/dist/wizard/types.js +4 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +12 -4
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js +1 -0
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js +1 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.js +1 -0
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js +5 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +8 -0
- package/dist/wizard.js +9 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +66 -13
- package/src/chart.ts +22 -0
- package/src/components/account-settings.tsx +275 -0
- package/src/components/alert-banner.tsx +75 -0
- package/src/components/amount-input.tsx +471 -0
- package/src/components/calculator.tsx +252 -0
- package/src/components/chart-zoom.tsx +499 -0
- package/src/components/chart.tsx +363 -0
- package/src/components/checkbox.tsx +222 -0
- package/src/components/chip.tsx +570 -0
- package/src/components/choice-parts.ts +33 -0
- package/src/components/combobox-core.tsx +540 -0
- package/src/components/combobox.tsx +1026 -0
- package/src/components/currency-select.tsx +269 -0
- package/src/components/data-table-filter-popover.tsx +252 -0
- package/src/components/data-table-filters.ts +176 -0
- package/src/components/data-table-labels.ts +190 -0
- package/src/components/data-table-pagination.tsx +126 -0
- package/src/components/data-table-sort.ts +70 -0
- package/src/components/data-table.tsx +1740 -0
- package/src/components/date-picker.tsx +631 -0
- package/src/components/dropdown.tsx +341 -0
- package/src/components/entity-combobox.tsx +215 -0
- package/src/components/facing-pair.ts +86 -0
- package/src/components/field-sync.tsx +472 -0
- package/src/components/file-dropzone.tsx +181 -0
- package/src/components/full-bleed-dialog.tsx +179 -0
- package/src/components/grouped-picker.tsx +157 -0
- package/src/components/hover-menu.tsx +183 -0
- package/src/components/mini-calendar.tsx +554 -0
- package/src/components/modal.tsx +287 -0
- package/src/components/month-picker.tsx +549 -0
- package/src/components/multi-entity-combobox.tsx +226 -0
- package/src/components/multi-select.tsx +303 -0
- package/src/components/number-field.tsx +225 -0
- package/src/components/number-input.tsx +211 -0
- package/src/components/numpad-sheet.tsx +264 -0
- package/src/components/page-contents.tsx +289 -0
- package/src/components/password-strength.tsx +258 -0
- package/src/components/picker-sheet.tsx +251 -0
- package/src/components/popover.tsx +195 -0
- package/src/components/search-field.tsx +146 -0
- package/src/components/series-chart-labels.ts +26 -0
- package/src/components/series-chart.tsx +481 -0
- package/src/components/settings-fields.tsx +68 -0
- package/src/components/signature-pad.tsx +630 -0
- package/src/components/slider.tsx +313 -0
- package/src/components/sparkline.tsx +329 -0
- package/src/components/stat-tile.tsx +448 -0
- package/src/components/swipeable-row.tsx +271 -0
- package/src/components/switch.tsx +170 -0
- package/src/components/time-input.tsx +107 -0
- package/src/components/toggle-group.tsx +153 -0
- package/src/components/toggle-legend.tsx +229 -0
- package/src/components/tooltip.tsx +477 -0
- package/src/components/treemap.tsx +390 -0
- package/src/components/ui.tsx +1284 -0
- package/src/components/use-mobile-reveal.ts +54 -0
- package/src/components/use-table-state.ts +295 -0
- package/src/components/user-avatar.tsx +61 -0
- package/src/components/wizard-stepper.tsx +37 -0
- package/src/data-table.ts +19 -0
- package/src/feedback/feedback-attachment.tsx +246 -0
- package/src/feedback/feedback-dialog.tsx +209 -0
- package/src/feedback/feedback-inbox.tsx +615 -0
- package/src/feedback.ts +8 -0
- package/src/hooks/use-anchored-panel.ts +169 -0
- package/src/hooks/use-anchored-rect.ts +78 -0
- package/src/hooks/use-announce.ts +85 -0
- package/src/hooks/use-body-scroll-lock.ts +94 -0
- package/src/hooks/use-close-transition.ts +99 -0
- package/src/hooks/use-dismiss.ts +77 -0
- package/src/hooks/use-focus-trap.ts +160 -0
- package/src/hooks/use-media-query.ts +23 -0
- package/src/hooks/use-overlay-history.ts +345 -0
- package/src/hooks/use-row-swipe.ts +212 -0
- package/src/i18n/defaults.ts +68 -0
- package/src/i18n/kit-labels.tsx +447 -0
- package/src/index.ts +169 -0
- package/src/lib/calc.ts +309 -0
- package/src/lib/cn.ts +5 -0
- package/src/lib/dates.ts +187 -0
- package/src/lib/logger.ts +73 -0
- package/src/lib/safe-storage.ts +37 -0
- package/src/search/command-palette.tsx +342 -0
- package/src/search.ts +3 -0
- package/src/shell/app-shell.tsx +772 -0
- package/src/shell/option-switcher-menu.tsx +72 -0
- package/src/shell/role-switcher.tsx +46 -0
- package/src/shell/top-bar.tsx +39 -0
- package/src/shell/topbar-action-menu.tsx +91 -0
- package/src/shell/topbar-controls.tsx +229 -0
- package/src/shell.ts +10 -0
- package/src/theme/chart-palette.ts +145 -0
- package/src/theme/color.ts +266 -0
- package/src/theme/palette-derive.ts +681 -0
- package/src/theme/palette-presets.ts +760 -0
- package/src/theme/palette-store.ts +94 -0
- package/src/theme/theme-store.ts +145 -0
- package/src/tour/tour.tsx +586 -0
- package/src/tour.ts +3 -0
- package/src/vite-env.d.ts +26 -0
- package/src/wizard/stepper-nav.tsx +221 -0
- package/src/wizard/types.ts +178 -0
- package/src/wizard/use-wizard.ts +345 -0
- package/src/wizard/validation.ts +37 -0
- package/src/wizard/wizard-context.tsx +29 -0
- package/src/wizard/wizard-step.tsx +14 -0
- package/src/wizard/wizard-summary.tsx +69 -0
- package/src/wizard.ts +14 -0
- package/tokens.css +222 -20
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from "react";
|
|
3
|
+
import { Calendar, ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { monthKey, pad } from "../lib/dates";
|
|
6
|
+
import { FieldLabel, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER } from "./ui";
|
|
7
|
+
import { Popover } from "./popover";
|
|
8
|
+
import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Every string the month picker can speak. The month and year names are NOT here —
|
|
12
|
+
* they come from `Intl` in the `locale` prop, so they are already in the user's
|
|
13
|
+
* language. What is left is the two icon-only year arrows, whose `aria-label` is the
|
|
14
|
+
* only name they have, the panel's name, and a month cell's name.
|
|
15
|
+
*/
|
|
16
|
+
export interface MonthPickerLabels {
|
|
17
|
+
previousYear: string;
|
|
18
|
+
nextYear: string;
|
|
19
|
+
/** The popover's accessible name (`role="dialog"`). */
|
|
20
|
+
panel: string;
|
|
21
|
+
/**
|
|
22
|
+
* Accessible name of one month cell, given that month and its year formatted in
|
|
23
|
+
* `locale` ("August 2026"). The cell's visible text is the short month alone, and
|
|
24
|
+
* "Aug" read out on its own does not say which year the grid is on.
|
|
25
|
+
*/
|
|
26
|
+
month: (monthYear: string) => string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The English starting point every override is merged onto. Exported (unlike the
|
|
30
|
+
* calendar's) because it is asked for by name: a host building its own translation
|
|
31
|
+
* table can spread it rather than re-typing four keys to change one. */
|
|
32
|
+
export const DEFAULT_MONTH_PICKER_LABELS: MonthPickerLabels = {
|
|
33
|
+
previousYear: "Previous year",
|
|
34
|
+
nextYear: "Next year",
|
|
35
|
+
panel: "Choose a month",
|
|
36
|
+
month: (monthYear) => monthYear,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Merge caller overrides onto the English defaults. */
|
|
40
|
+
function useMonthPickerLabels(partial?: Partial<MonthPickerLabels>): MonthPickerLabels {
|
|
41
|
+
// Prop > <UiKitProvider> > English, like every labelled component in the kit.
|
|
42
|
+
return useKitLabels("monthPicker", DEFAULT_MONTH_PICKER_LABELS, partial);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `onChange` is omitted from the `<div>` attributes: the DOM event of that name carries
|
|
47
|
+
* a `FormEvent`, and this one answers with a month key. Everything else reaches the
|
|
48
|
+
* field's root, so a test id or a `data-tour` anchor can find it.
|
|
49
|
+
*/
|
|
50
|
+
export interface MonthPickerProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
|
|
51
|
+
/**
|
|
52
|
+
* The selected month as a `"YYYY-MM"` key, or `""` for none.
|
|
53
|
+
*
|
|
54
|
+
* A string rather than `{ year, month }` or a `Date`, for the reason every date in
|
|
55
|
+
* this kit is one: it is what `monthKey()` in `lib/dates` produces, it sorts
|
|
56
|
+
* chronologically so the bounds are plain `<` / `>`, and it crosses a URL or an API
|
|
57
|
+
* without a serialiser. A `{ year, month }` caller converts with
|
|
58
|
+
* `` `${year}-${pad(month)}` `` (month 1–12) and back with `key.split("-").map(Number)`.
|
|
59
|
+
*/
|
|
60
|
+
value: string;
|
|
61
|
+
/** Called with the picked month as `"YYYY-MM"`. */
|
|
62
|
+
onChange: (month: string) => void;
|
|
63
|
+
/** Drives the month and year names (`Intl.DateTimeFormat`). Defaults to the
|
|
64
|
+
* `<UiKitProvider>` locale, then the runtime's. */
|
|
65
|
+
locale?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Inclusive bounds, as `"YYYY-MM"`. A full ISO date (`"YYYY-MM-DD"`) is accepted and
|
|
68
|
+
* read as its month, so a host holding "the first day we have data for" can pass it
|
|
69
|
+
* straight in. Months outside are shown but refuse selection; a year arrow is
|
|
70
|
+
* disabled once the whole year beyond it is out of bounds.
|
|
71
|
+
*/
|
|
72
|
+
min?: string;
|
|
73
|
+
max?: string;
|
|
74
|
+
/** Embedded top-boundary label, matching the other field-shaped controls. */
|
|
75
|
+
label?: ReactNode;
|
|
76
|
+
/** Trigger text when `value` is `""`. */
|
|
77
|
+
placeholder?: string;
|
|
78
|
+
disabled?: boolean;
|
|
79
|
+
/** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
|
|
80
|
+
invalid?: boolean;
|
|
81
|
+
/** Intl options for the trigger's text (default: long month + numeric year). */
|
|
82
|
+
formatOptions?: Intl.DateTimeFormatOptions;
|
|
83
|
+
/**
|
|
84
|
+
* The month to ring as "now", as `"YYYY-MM"`. Defaults to the local calendar's
|
|
85
|
+
* current month; pass one to pin it (a test, a host with its own notion of today).
|
|
86
|
+
*/
|
|
87
|
+
currentMonth?: string;
|
|
88
|
+
/** User-facing strings; see {@link MonthPickerLabels}. */
|
|
89
|
+
labels?: Partial<MonthPickerLabels>;
|
|
90
|
+
/** Extra classes for the field's root. */
|
|
91
|
+
className?: string;
|
|
92
|
+
/** Extra classes for the trigger button — e.g. a compact `h-9 w-auto` trigger
|
|
93
|
+
* sitting between two icon buttons in a toolbar. */
|
|
94
|
+
triggerClassName?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Month keys ────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
interface YearMonth {
|
|
100
|
+
year: number;
|
|
101
|
+
/** 1–12, as in the key. */
|
|
102
|
+
month: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const KEY = /^(\d{4})-(\d{2})/;
|
|
106
|
+
|
|
107
|
+
function parseMonth(key: string | undefined): YearMonth | null {
|
|
108
|
+
const m = key ? KEY.exec(key) : null;
|
|
109
|
+
if (!m) return null;
|
|
110
|
+
const month = Number(m[2]);
|
|
111
|
+
return month >= 1 && month <= 12 ? { year: Number(m[1]), month } : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function toKey({ year, month }: YearMonth): string {
|
|
115
|
+
return `${year}-${pad(month)}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Move a month key by whole months, carrying into the year. */
|
|
119
|
+
function shiftMonth(ym: YearMonth, delta: number): YearMonth {
|
|
120
|
+
const index = ym.year * 12 + (ym.month - 1) + delta;
|
|
121
|
+
return { year: Math.floor(index / 12), month: (index % 12) + 1 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A bound, normalised to its month key — so `"2024-03-15"` compares as `"2024-03"`. */
|
|
125
|
+
function boundKey(bound: string | undefined): string | null {
|
|
126
|
+
const ym = parseMonth(bound);
|
|
127
|
+
return ym ? toKey(ym) : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Local-calendar Date for the 1st of a month, for `Intl` to format. */
|
|
131
|
+
function firstOf({ year, month }: YearMonth): Date {
|
|
132
|
+
return new Date(year, month - 1, 1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const DEFAULT_TRIGGER_FORMAT: Intl.DateTimeFormatOptions = { month: "long", year: "numeric" };
|
|
136
|
+
const MONTH_NAME_FORMAT: Intl.DateTimeFormatOptions = { month: "long", year: "numeric" };
|
|
137
|
+
|
|
138
|
+
// ── Trigger ───────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The field-shaped button that opens the grid. The same shape as `DatePicker`'s
|
|
142
|
+
* trigger, for the same reasons written down there: `role="combobox"` so it may carry
|
|
143
|
+
* `aria-expanded` / `aria-invalid`, a name made of the label AND the value, and focus
|
|
144
|
+
* handed back here when the panel closes with focus still inside it.
|
|
145
|
+
*/
|
|
146
|
+
function MonthFieldTrigger({
|
|
147
|
+
open,
|
|
148
|
+
toggle,
|
|
149
|
+
triggerRef,
|
|
150
|
+
triggerText,
|
|
151
|
+
hasValue,
|
|
152
|
+
labelledBy,
|
|
153
|
+
valueId,
|
|
154
|
+
panelId,
|
|
155
|
+
padded,
|
|
156
|
+
disabled,
|
|
157
|
+
invalid,
|
|
158
|
+
className,
|
|
159
|
+
}: {
|
|
160
|
+
open: boolean;
|
|
161
|
+
toggle: () => void;
|
|
162
|
+
triggerRef: RefObject<HTMLButtonElement | null>;
|
|
163
|
+
triggerText: string;
|
|
164
|
+
hasValue: boolean;
|
|
165
|
+
labelledBy: string;
|
|
166
|
+
valueId: string;
|
|
167
|
+
panelId: string;
|
|
168
|
+
padded: boolean;
|
|
169
|
+
disabled?: boolean;
|
|
170
|
+
invalid?: boolean;
|
|
171
|
+
className?: string;
|
|
172
|
+
}) {
|
|
173
|
+
const wasOpen = useRef(false);
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const justClosed = wasOpen.current && !open;
|
|
176
|
+
wasOpen.current = open;
|
|
177
|
+
if (!justClosed) return;
|
|
178
|
+
// Only when focus went down with the panel. An outside click has already put it
|
|
179
|
+
// somewhere real, and dragging it back here would be worse than leaving it.
|
|
180
|
+
const active = document.activeElement;
|
|
181
|
+
if (!active || active === document.body) triggerRef.current?.focus();
|
|
182
|
+
}, [open, triggerRef]);
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<button
|
|
186
|
+
ref={triggerRef}
|
|
187
|
+
type="button"
|
|
188
|
+
disabled={disabled}
|
|
189
|
+
onClick={toggle}
|
|
190
|
+
role="combobox"
|
|
191
|
+
aria-haspopup="dialog"
|
|
192
|
+
aria-controls={panelId}
|
|
193
|
+
aria-expanded={open}
|
|
194
|
+
aria-invalid={invalid || undefined}
|
|
195
|
+
aria-labelledby={labelledBy}
|
|
196
|
+
className={cn(
|
|
197
|
+
FIELD_TRIGGER,
|
|
198
|
+
"pe-9",
|
|
199
|
+
padded && FIELD_FLOATING_PAD,
|
|
200
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
201
|
+
invalid && FIELD_INVALID,
|
|
202
|
+
className,
|
|
203
|
+
)}
|
|
204
|
+
>
|
|
205
|
+
{/* The U+00A0 fallback keeps the line box when there is no text — see
|
|
206
|
+
DatePicker. The value arrives capitalised (see `upperFirst`); this span does
|
|
207
|
+
NOT use `capitalize`, which upper-cases every WORD — "Choose A Month",
|
|
208
|
+
"Septiembre De 2026". */}
|
|
209
|
+
<span
|
|
210
|
+
id={valueId}
|
|
211
|
+
className={cn("truncate", !hasValue && "text-[var(--text-placeholder)]")}
|
|
212
|
+
>
|
|
213
|
+
{triggerText || " "}
|
|
214
|
+
</span>
|
|
215
|
+
</button>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Grid ──────────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
const COLUMNS = 3;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The panel: a year caption between ‹ › arrows, over a 4×3 grid of months.
|
|
225
|
+
*
|
|
226
|
+
* Mounted only while the popover is open, so every opening starts from the current
|
|
227
|
+
* value's year — a value moved from outside (a prev/next-month button beside the
|
|
228
|
+
* field) is where the grid lands, with no effect needed to snap it back.
|
|
229
|
+
*
|
|
230
|
+
* A grid in the APG sense, like `MiniCalendar`: one tab stop (the roving tabindex),
|
|
231
|
+
* the arrows walk the months — left/right by one, up/down by a row of three — and
|
|
232
|
+
* walking off either end of the year MOVES the year rather than stopping. PageUp /
|
|
233
|
+
* PageDown step a whole year. Out-of-bounds months are `aria-disabled`, not
|
|
234
|
+
* `disabled`, so the roving tab stop never falls into a hole.
|
|
235
|
+
*/
|
|
236
|
+
function MonthGrid({
|
|
237
|
+
value,
|
|
238
|
+
onPick,
|
|
239
|
+
locale,
|
|
240
|
+
minKey,
|
|
241
|
+
maxKey,
|
|
242
|
+
currentKey,
|
|
243
|
+
labels,
|
|
244
|
+
}: {
|
|
245
|
+
value: YearMonth | null;
|
|
246
|
+
onPick: (key: string) => void;
|
|
247
|
+
locale: string | undefined;
|
|
248
|
+
minKey: string | null;
|
|
249
|
+
maxKey: string | null;
|
|
250
|
+
currentKey: string;
|
|
251
|
+
labels: MonthPickerLabels;
|
|
252
|
+
}) {
|
|
253
|
+
const outOfBounds = (key: string) =>
|
|
254
|
+
Boolean((minKey && key < minKey) || (maxKey && key > maxKey));
|
|
255
|
+
|
|
256
|
+
// The month holding the tab order, and with it the year on screen — one piece of
|
|
257
|
+
// state, as in MiniCalendar, so the two cannot disagree. Opens on the value, or on
|
|
258
|
+
// the current month, pulled inside the bounds.
|
|
259
|
+
const [active, setActive] = useState<YearMonth>(() => {
|
|
260
|
+
const start = value ? toKey(value) : currentKey;
|
|
261
|
+
if (minKey && start < minKey) return parseMonth(minKey)!;
|
|
262
|
+
if (maxKey && start > maxKey) return parseMonth(maxKey)!;
|
|
263
|
+
return parseMonth(start)!;
|
|
264
|
+
});
|
|
265
|
+
const year = active.year;
|
|
266
|
+
|
|
267
|
+
const gridRef = useRef<HTMLDivElement>(null);
|
|
268
|
+
const captionId = `${useId()}-year`;
|
|
269
|
+
|
|
270
|
+
// Short names for the cells, long "Month YYYY" for their accessible names.
|
|
271
|
+
const shortNames = useMemo(() => {
|
|
272
|
+
const fmt = new Intl.DateTimeFormat(locale, { month: "short" });
|
|
273
|
+
return Array.from({ length: 12 }, (_, i) => fmt.format(new Date(2000, i, 1)));
|
|
274
|
+
}, [locale]);
|
|
275
|
+
const longName = useMemo(() => new Intl.DateTimeFormat(locale, MONTH_NAME_FORMAT), [locale]);
|
|
276
|
+
const yearCaption = new Intl.DateTimeFormat(locale, { year: "numeric" }).format(
|
|
277
|
+
firstOf({ year, month: 1 }),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// A month whose cell must take focus after the next commit. Arrowing across a year
|
|
281
|
+
// boundary re-labels the same twelve buttons, so the one to focus is a DIFFERENT
|
|
282
|
+
// button from the one the key landed on.
|
|
283
|
+
const pendingFocus = useRef<string | null>(null);
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
const key = pendingFocus.current;
|
|
286
|
+
if (!key) return;
|
|
287
|
+
pendingFocus.current = null;
|
|
288
|
+
gridRef.current?.querySelector<HTMLElement>(`[data-month="${key}"]`)?.focus();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Opened because the user asked to pick a month: land them on it. Runs before the
|
|
292
|
+
// popover's focus trap (a child's effect fires first), which then keeps this focus.
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
gridRef.current?.querySelector<HTMLElement>('[tabindex="0"]')?.focus();
|
|
295
|
+
}, []);
|
|
296
|
+
|
|
297
|
+
const moveTo = (next: YearMonth, focus: boolean) => {
|
|
298
|
+
if (focus) pendingFocus.current = toKey(next);
|
|
299
|
+
setActive(next);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const onCellKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
|
|
303
|
+
// Left and right are VISUAL directions: in a right-to-left page the grid runs the
|
|
304
|
+
// other way, so "right" is the previous month there. Read from the nearest `dir`
|
|
305
|
+
// up the tree, which for this portalled panel is in practice <html>'s.
|
|
306
|
+
const rtl = e.currentTarget.closest("[dir]")?.getAttribute("dir") === "rtl";
|
|
307
|
+
const col = (active.month - 1) % COLUMNS;
|
|
308
|
+
let delta: number;
|
|
309
|
+
switch (e.key) {
|
|
310
|
+
case "ArrowLeft":
|
|
311
|
+
delta = rtl ? 1 : -1;
|
|
312
|
+
break;
|
|
313
|
+
case "ArrowRight":
|
|
314
|
+
delta = rtl ? -1 : 1;
|
|
315
|
+
break;
|
|
316
|
+
case "ArrowUp":
|
|
317
|
+
delta = -COLUMNS;
|
|
318
|
+
break;
|
|
319
|
+
case "ArrowDown":
|
|
320
|
+
delta = COLUMNS;
|
|
321
|
+
break;
|
|
322
|
+
case "Home":
|
|
323
|
+
delta = -col;
|
|
324
|
+
break;
|
|
325
|
+
case "End":
|
|
326
|
+
delta = COLUMNS - 1 - col;
|
|
327
|
+
break;
|
|
328
|
+
case "PageUp":
|
|
329
|
+
delta = -12;
|
|
330
|
+
break;
|
|
331
|
+
case "PageDown":
|
|
332
|
+
delta = 12;
|
|
333
|
+
break;
|
|
334
|
+
default:
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Before any no-op check: these keys also scroll the panel, and a month that
|
|
338
|
+
// cannot move still must not move the page.
|
|
339
|
+
e.preventDefault();
|
|
340
|
+
if (delta !== 0) moveTo(shiftMonth(active, delta), true);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// An arrow is dead once EVERY month of the year it leads to is out of bounds.
|
|
344
|
+
const prevBlocked = Boolean(minKey && toKey({ year: year - 1, month: 12 }) < minKey);
|
|
345
|
+
const nextBlocked = Boolean(maxKey && toKey({ year: year + 1, month: 1 }) > maxKey);
|
|
346
|
+
const selectedKey = value ? toKey(value) : null;
|
|
347
|
+
|
|
348
|
+
const arrow =
|
|
349
|
+
"flex size-7 items-center justify-center rounded text-[var(--text-muted)] hover:bg-[var(--bg-hover)] disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent";
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<div className="select-none">
|
|
353
|
+
<div className="mb-1 flex items-center justify-between">
|
|
354
|
+
<button
|
|
355
|
+
type="button"
|
|
356
|
+
aria-label={labels.previousYear}
|
|
357
|
+
disabled={prevBlocked}
|
|
358
|
+
// Focus stays on the arrow being pressed — only the grid moves.
|
|
359
|
+
onClick={() => moveTo(shiftMonth(active, -12), false)}
|
|
360
|
+
className={arrow}
|
|
361
|
+
>
|
|
362
|
+
{/* Mirrored in RTL, where "previous" points the other way. */}
|
|
363
|
+
<ChevronLeft className="size-4 rtl:-scale-x-100" />
|
|
364
|
+
</button>
|
|
365
|
+
{/* Names the grid, and `aria-live` so stepping a year says which one you are
|
|
366
|
+
on — the user pressing the arrow is not inside the grid to hear it change. */}
|
|
367
|
+
<div
|
|
368
|
+
id={captionId}
|
|
369
|
+
aria-live="polite"
|
|
370
|
+
className="text-sm font-semibold tabular-nums text-[var(--text-primary)]"
|
|
371
|
+
>
|
|
372
|
+
{yearCaption}
|
|
373
|
+
</div>
|
|
374
|
+
<button
|
|
375
|
+
type="button"
|
|
376
|
+
aria-label={labels.nextYear}
|
|
377
|
+
disabled={nextBlocked}
|
|
378
|
+
onClick={() => moveTo(shiftMonth(active, 12), false)}
|
|
379
|
+
className={arrow}
|
|
380
|
+
>
|
|
381
|
+
<ChevronRight className="size-4 rtl:-scale-x-100" />
|
|
382
|
+
</button>
|
|
383
|
+
</div>
|
|
384
|
+
<div ref={gridRef} role="grid" aria-labelledby={captionId} className="grid gap-1">
|
|
385
|
+
{Array.from({ length: 12 / COLUMNS }, (_, row) => (
|
|
386
|
+
<div key={row} role="row" className="grid grid-cols-3 gap-1">
|
|
387
|
+
{Array.from({ length: COLUMNS }, (_, c) => {
|
|
388
|
+
const ym = { year, month: row * COLUMNS + c + 1 };
|
|
389
|
+
const key = toKey(ym);
|
|
390
|
+
const disabled = outOfBounds(key);
|
|
391
|
+
const selected = key === selectedKey;
|
|
392
|
+
const current = key === currentKey;
|
|
393
|
+
return (
|
|
394
|
+
<button
|
|
395
|
+
key={c}
|
|
396
|
+
type="button"
|
|
397
|
+
role="gridcell"
|
|
398
|
+
data-month={key}
|
|
399
|
+
tabIndex={ym.month === active.month ? 0 : -1}
|
|
400
|
+
aria-label={labels.month(longName.format(firstOf(ym)))}
|
|
401
|
+
aria-selected={selected}
|
|
402
|
+
aria-disabled={disabled || undefined}
|
|
403
|
+
aria-current={current ? "date" : undefined}
|
|
404
|
+
onClick={() => {
|
|
405
|
+
// The guard `disabled` would have been; see the component note.
|
|
406
|
+
if (disabled) return;
|
|
407
|
+
setActive(ym);
|
|
408
|
+
onPick(key);
|
|
409
|
+
}}
|
|
410
|
+
onKeyDown={onCellKeyDown}
|
|
411
|
+
className={cn(
|
|
412
|
+
"rounded px-2 py-1.5 text-sm capitalize transition-colors",
|
|
413
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset",
|
|
414
|
+
// A --brand ring on a --brand fill exists only in the DOM.
|
|
415
|
+
selected && !disabled
|
|
416
|
+
? "focus-visible:ring-[var(--brand-contrast)]"
|
|
417
|
+
: "focus-visible:ring-[var(--brand)]",
|
|
418
|
+
disabled
|
|
419
|
+
? "cursor-not-allowed text-[var(--text-placeholder)]"
|
|
420
|
+
: selected
|
|
421
|
+
? "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]"
|
|
422
|
+
: "text-[var(--text-primary)] hover:bg-[var(--bg-hover)]",
|
|
423
|
+
current && !selected && !disabled && "ring-1 ring-inset ring-[var(--border-strong)]",
|
|
424
|
+
)}
|
|
425
|
+
>
|
|
426
|
+
{shortNames[ym.month - 1]}
|
|
427
|
+
</button>
|
|
428
|
+
);
|
|
429
|
+
})}
|
|
430
|
+
</div>
|
|
431
|
+
))}
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── Field ─────────────────────────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Month picker: a field showing the selected month ("August 2026"), opening a 12-month
|
|
441
|
+
* grid with year arrows. For a value that IS a month — a budget period, a calendar
|
|
442
|
+
* view, a statement — where a day picker would ask for a day nobody means.
|
|
443
|
+
*
|
|
444
|
+
* One component for the two app-local pickers it replaces: keksdose's budget-header
|
|
445
|
+
* popover (grid + year steppers + a floor) and kastlan's calendar header (a month list
|
|
446
|
+
* and a year list in two popovers). The grid does both jobs — the year arrows and
|
|
447
|
+
* PageUp/PageDown cover what the year list did, without a fixed ±5-year window.
|
|
448
|
+
*/
|
|
449
|
+
/** Upper-case the first letter only, in `locale`. Several locales (fr, es, it, pt, …)
|
|
450
|
+
* write month names in lower case, which at the start of a field reads as a typo —
|
|
451
|
+
* but only the FIRST letter is the field's business: "septiembre de 2026" becomes
|
|
452
|
+
* "Septiembre de 2026", not "Septiembre De 2026". */
|
|
453
|
+
function upperFirst(text: string, locale: string | undefined): string {
|
|
454
|
+
return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function MonthPicker({
|
|
458
|
+
value,
|
|
459
|
+
onChange,
|
|
460
|
+
locale: localeProp,
|
|
461
|
+
min,
|
|
462
|
+
max,
|
|
463
|
+
label,
|
|
464
|
+
placeholder,
|
|
465
|
+
disabled,
|
|
466
|
+
invalid,
|
|
467
|
+
formatOptions,
|
|
468
|
+
currentMonth,
|
|
469
|
+
labels: labelsProp,
|
|
470
|
+
className,
|
|
471
|
+
triggerClassName,
|
|
472
|
+
...rest
|
|
473
|
+
}: MonthPickerProps) {
|
|
474
|
+
const labels = useMonthPickerLabels(labelsProp);
|
|
475
|
+
const locale = useKitLocale(localeProp);
|
|
476
|
+
const selected = parseMonth(value);
|
|
477
|
+
const minKey = boundKey(min);
|
|
478
|
+
const maxKey = boundKey(max);
|
|
479
|
+
const currentKey = boundKey(currentMonth) ?? monthKey(new Date());
|
|
480
|
+
|
|
481
|
+
const triggerText = selected
|
|
482
|
+
? upperFirst(
|
|
483
|
+
new Intl.DateTimeFormat(locale, formatOptions ?? DEFAULT_TRIGGER_FORMAT).format(
|
|
484
|
+
firstOf(selected),
|
|
485
|
+
),
|
|
486
|
+
locale,
|
|
487
|
+
)
|
|
488
|
+
: (placeholder ?? "");
|
|
489
|
+
|
|
490
|
+
const id = useId();
|
|
491
|
+
const labelId = `${id}-label`;
|
|
492
|
+
const valueId = `${id}-value`;
|
|
493
|
+
const panelId = `${id}-panel`;
|
|
494
|
+
// Only a STRING label can double as a name; a ReactNode may hold its own controls.
|
|
495
|
+
const named = typeof label === "string";
|
|
496
|
+
|
|
497
|
+
return (
|
|
498
|
+
<div {...rest} className={cn("relative", className)}>
|
|
499
|
+
{label !== undefined && <FieldLabel>{label}</FieldLabel>}
|
|
500
|
+
{/* The hidden twin the trigger is named by — see DateField. `sr-only-fixed`,
|
|
501
|
+
and inside this `relative` root either way. */}
|
|
502
|
+
{named && (
|
|
503
|
+
<span id={labelId} aria-hidden className="sr-only-fixed">
|
|
504
|
+
{label}
|
|
505
|
+
</span>
|
|
506
|
+
)}
|
|
507
|
+
<Popover
|
|
508
|
+
width={256}
|
|
509
|
+
panelId={panelId}
|
|
510
|
+
labels={{ panel: labels.panel }}
|
|
511
|
+
trigger={({ open, toggle, ref }) => (
|
|
512
|
+
<MonthFieldTrigger
|
|
513
|
+
open={open}
|
|
514
|
+
toggle={toggle}
|
|
515
|
+
triggerRef={ref}
|
|
516
|
+
triggerText={triggerText}
|
|
517
|
+
hasValue={selected != null}
|
|
518
|
+
labelledBy={named ? `${labelId} ${valueId}` : valueId}
|
|
519
|
+
valueId={valueId}
|
|
520
|
+
panelId={panelId}
|
|
521
|
+
padded={label !== undefined}
|
|
522
|
+
disabled={disabled}
|
|
523
|
+
invalid={invalid}
|
|
524
|
+
className={triggerClassName}
|
|
525
|
+
/>
|
|
526
|
+
)}
|
|
527
|
+
>
|
|
528
|
+
{(close) => (
|
|
529
|
+
<MonthGrid
|
|
530
|
+
value={selected}
|
|
531
|
+
locale={locale}
|
|
532
|
+
minKey={minKey}
|
|
533
|
+
maxKey={maxKey}
|
|
534
|
+
currentKey={currentKey}
|
|
535
|
+
labels={labels}
|
|
536
|
+
onPick={(key) => {
|
|
537
|
+
onChange(key);
|
|
538
|
+
close();
|
|
539
|
+
}}
|
|
540
|
+
/>
|
|
541
|
+
)}
|
|
542
|
+
</Popover>
|
|
543
|
+
<Calendar
|
|
544
|
+
aria-hidden
|
|
545
|
+
className="pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]"
|
|
546
|
+
/>
|
|
547
|
+
</div>
|
|
548
|
+
);
|
|
549
|
+
}
|