@allxsmith/bestax-bulma 5.26.6 → 5.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bestax.css +1 -1
- package/dist/bestax.css.map +1 -1
- package/dist/esm/columns/Column.js +48 -0
- package/dist/esm/columns/Column.js.map +1 -0
- package/dist/esm/columns/Columns.js +53 -0
- package/dist/esm/columns/Columns.js.map +1 -0
- package/dist/esm/components/Avatar.js +299 -0
- package/dist/esm/components/Avatar.js.map +1 -0
- package/dist/esm/components/Avatars.js +81 -0
- package/dist/esm/components/Avatars.js.map +1 -0
- package/dist/esm/components/Badge.js +105 -0
- package/dist/esm/components/Badge.js.map +1 -0
- package/dist/esm/components/Breadcrumb.js +36 -0
- package/dist/esm/components/Breadcrumb.js.map +1 -0
- package/dist/esm/components/Card.js +252 -0
- package/dist/esm/components/Card.js.map +1 -0
- package/dist/esm/components/Carousel.js +351 -0
- package/dist/esm/components/Carousel.js.map +1 -0
- package/dist/esm/components/Collapse.js +193 -0
- package/dist/esm/components/Collapse.js.map +1 -0
- package/dist/esm/components/Collapses.js +99 -0
- package/dist/esm/components/Collapses.js.map +1 -0
- package/dist/esm/components/Dialog.js +230 -0
- package/dist/esm/components/Dialog.js.map +1 -0
- package/dist/esm/components/Dropdown.js +362 -0
- package/dist/esm/components/Dropdown.js.map +1 -0
- package/dist/esm/components/Loading.js +87 -0
- package/dist/esm/components/Loading.js.map +1 -0
- package/dist/esm/components/Menu.js +117 -0
- package/dist/esm/components/Menu.js.map +1 -0
- package/dist/esm/components/Message.js +56 -0
- package/dist/esm/components/Message.js.map +1 -0
- package/dist/esm/components/Modal.js +354 -0
- package/dist/esm/components/Modal.js.map +1 -0
- package/dist/esm/components/Navbar.js +287 -0
- package/dist/esm/components/Navbar.js.map +1 -0
- package/dist/esm/components/Pagination.js +191 -0
- package/dist/esm/components/Pagination.js.map +1 -0
- package/dist/esm/components/Panel.js +100 -0
- package/dist/esm/components/Panel.js.map +1 -0
- package/dist/esm/components/Popover.js +298 -0
- package/dist/esm/components/Popover.js.map +1 -0
- package/dist/esm/components/Reveal.js +157 -0
- package/dist/esm/components/Reveal.js.map +1 -0
- package/dist/esm/components/Sidebar.js +209 -0
- package/dist/esm/components/Sidebar.js.map +1 -0
- package/dist/esm/components/Steps.js +128 -0
- package/dist/esm/components/Steps.js.map +1 -0
- package/dist/esm/components/Tabs.js +335 -0
- package/dist/esm/components/Tabs.js.map +1 -0
- package/dist/esm/components/Toast.js +322 -0
- package/dist/esm/components/Toast.js.map +1 -0
- package/dist/esm/components/Tooltip.js +202 -0
- package/dist/esm/components/Tooltip.js.map +1 -0
- package/dist/esm/components/collapsesContext.js +12 -0
- package/dist/esm/components/collapsesContext.js.map +1 -0
- package/dist/esm/elements/Block.js +29 -0
- package/dist/esm/elements/Block.js.map +1 -0
- package/dist/esm/elements/Box.js +32 -0
- package/dist/esm/elements/Box.js.map +1 -0
- package/dist/esm/elements/Button.js +139 -0
- package/dist/esm/elements/Button.js.map +1 -0
- package/dist/esm/elements/Buttons.js +42 -0
- package/dist/esm/elements/Buttons.js.map +1 -0
- package/dist/esm/elements/Code.js +25 -0
- package/dist/esm/elements/Code.js.map +1 -0
- package/dist/esm/elements/Content.js +33 -0
- package/dist/esm/elements/Content.js.map +1 -0
- package/dist/esm/elements/Delete.js +35 -0
- package/dist/esm/elements/Delete.js.map +1 -0
- package/dist/esm/elements/Divider.js +27 -0
- package/dist/esm/elements/Divider.js.map +1 -0
- package/dist/esm/elements/Emphasis.js +25 -0
- package/dist/esm/elements/Emphasis.js.map +1 -0
- package/dist/esm/elements/Figure.js +58 -0
- package/dist/esm/elements/Figure.js.map +1 -0
- package/dist/esm/elements/Icon.js +256 -0
- package/dist/esm/elements/Icon.js.map +1 -0
- package/dist/esm/elements/IconText.js +36 -0
- package/dist/esm/elements/IconText.js.map +1 -0
- package/dist/esm/elements/Image.js +46 -0
- package/dist/esm/elements/Image.js.map +1 -0
- package/dist/esm/elements/Link.js +36 -0
- package/dist/esm/elements/Link.js.map +1 -0
- package/dist/esm/elements/LinkButton.js +37 -0
- package/dist/esm/elements/LinkButton.js.map +1 -0
- package/dist/esm/elements/ListItem.js +28 -0
- package/dist/esm/elements/ListItem.js.map +1 -0
- package/dist/esm/elements/Loader.js +37 -0
- package/dist/esm/elements/Loader.js.map +1 -0
- package/dist/esm/elements/Notification.js +316 -0
- package/dist/esm/elements/Notification.js.map +1 -0
- package/dist/esm/elements/OrderedList.js +31 -0
- package/dist/esm/elements/OrderedList.js.map +1 -0
- package/dist/esm/elements/Paragraph.js +28 -0
- package/dist/esm/elements/Paragraph.js.map +1 -0
- package/dist/esm/elements/Pre.js +25 -0
- package/dist/esm/elements/Pre.js.map +1 -0
- package/dist/esm/elements/Progress.js +33 -0
- package/dist/esm/elements/Progress.js.map +1 -0
- package/dist/esm/elements/Skeleton.js +30 -0
- package/dist/esm/elements/Skeleton.js.map +1 -0
- package/dist/esm/elements/Span.js +28 -0
- package/dist/esm/elements/Span.js.map +1 -0
- package/dist/esm/elements/Strong.js +25 -0
- package/dist/esm/elements/Strong.js.map +1 -0
- package/dist/esm/elements/SubTitle.js +51 -0
- package/dist/esm/elements/SubTitle.js.map +1 -0
- package/dist/esm/elements/Table.js +51 -0
- package/dist/esm/elements/Table.js.map +1 -0
- package/dist/esm/elements/Tag.js +49 -0
- package/dist/esm/elements/Tag.js.map +1 -0
- package/dist/esm/elements/Tags.js +34 -0
- package/dist/esm/elements/Tags.js.map +1 -0
- package/dist/esm/elements/Tbody.js +26 -0
- package/dist/esm/elements/Tbody.js.map +1 -0
- package/dist/esm/elements/Td.js +61 -0
- package/dist/esm/elements/Td.js.map +1 -0
- package/dist/esm/elements/Tfoot.js +26 -0
- package/dist/esm/elements/Tfoot.js.map +1 -0
- package/dist/esm/elements/Th.js +34 -0
- package/dist/esm/elements/Th.js.map +1 -0
- package/dist/esm/elements/Thead.js +26 -0
- package/dist/esm/elements/Thead.js.map +1 -0
- package/dist/esm/elements/Title.js +44 -0
- package/dist/esm/elements/Title.js.map +1 -0
- package/dist/esm/elements/Tr.js +31 -0
- package/dist/esm/elements/Tr.js.map +1 -0
- package/dist/esm/elements/UnorderedList.js +31 -0
- package/dist/esm/elements/UnorderedList.js.map +1 -0
- package/dist/esm/elements/iconProps.js +49 -0
- package/dist/esm/elements/iconProps.js.map +1 -0
- package/dist/esm/form/Autocomplete.js +279 -0
- package/dist/esm/form/Autocomplete.js.map +1 -0
- package/dist/esm/form/Checkbox.js +111 -0
- package/dist/esm/form/Checkbox.js.map +1 -0
- package/dist/esm/form/Checkboxes.js +89 -0
- package/dist/esm/form/Checkboxes.js.map +1 -0
- package/dist/esm/form/Control.js +79 -0
- package/dist/esm/form/Control.js.map +1 -0
- package/dist/esm/form/DateInput.js +87 -0
- package/dist/esm/form/DateInput.js.map +1 -0
- package/dist/esm/form/DateInputBase.js +263 -0
- package/dist/esm/form/DateInputBase.js.map +1 -0
- package/dist/esm/form/DateRangeInput.js +76 -0
- package/dist/esm/form/DateRangeInput.js.map +1 -0
- package/dist/esm/form/DateRangeInputBase.js +301 -0
- package/dist/esm/form/DateRangeInputBase.js.map +1 -0
- package/dist/esm/form/DateTimeInput.js +87 -0
- package/dist/esm/form/DateTimeInput.js.map +1 -0
- package/dist/esm/form/DateTimeInputBase.js +333 -0
- package/dist/esm/form/DateTimeInputBase.js.map +1 -0
- package/dist/esm/form/Field.js +153 -0
- package/dist/esm/form/Field.js.map +1 -0
- package/dist/esm/form/File.js +109 -0
- package/dist/esm/form/File.js.map +1 -0
- package/dist/esm/form/FormContext.js +170 -0
- package/dist/esm/form/FormContext.js.map +1 -0
- package/dist/esm/form/Input.js +88 -0
- package/dist/esm/form/Input.js.map +1 -0
- package/dist/esm/form/InputBase.js +40 -0
- package/dist/esm/form/InputBase.js.map +1 -0
- package/dist/esm/form/Numberinput.js +199 -0
- package/dist/esm/form/Numberinput.js.map +1 -0
- package/dist/esm/form/Radio.js +108 -0
- package/dist/esm/form/Radio.js.map +1 -0
- package/dist/esm/form/Radios.js +91 -0
- package/dist/esm/form/Radios.js.map +1 -0
- package/dist/esm/form/Rate.js +279 -0
- package/dist/esm/form/Rate.js.map +1 -0
- package/dist/esm/form/Select.js +81 -0
- package/dist/esm/form/Select.js.map +1 -0
- package/dist/esm/form/SelectBase.js +54 -0
- package/dist/esm/form/SelectBase.js.map +1 -0
- package/dist/esm/form/Slider.js +459 -0
- package/dist/esm/form/Slider.js.map +1 -0
- package/dist/esm/form/Switch.js +96 -0
- package/dist/esm/form/Switch.js.map +1 -0
- package/dist/esm/form/Taginput.js +352 -0
- package/dist/esm/form/Taginput.js.map +1 -0
- package/dist/esm/form/TextArea.js +77 -0
- package/dist/esm/form/TextArea.js.map +1 -0
- package/dist/esm/form/TextAreaBase.js +45 -0
- package/dist/esm/form/TextAreaBase.js.map +1 -0
- package/dist/esm/form/TimeInput.js +83 -0
- package/dist/esm/form/TimeInput.js.map +1 -0
- package/dist/esm/form/TimeInputBase.js +246 -0
- package/dist/esm/form/TimeInputBase.js.map +1 -0
- package/dist/esm/form/_pickerInternals/Calendar.js +760 -0
- package/dist/esm/form/_pickerInternals/Calendar.js.map +1 -0
- package/dist/esm/form/_pickerInternals/PickerPopover.js +95 -0
- package/dist/esm/form/_pickerInternals/PickerPopover.js.map +1 -0
- package/dist/esm/form/_pickerInternals/TimeWheels.js +721 -0
- package/dist/esm/form/_pickerInternals/TimeWheels.js.map +1 -0
- package/dist/esm/form/_pickerInternals/audioTick.js +109 -0
- package/dist/esm/form/_pickerInternals/audioTick.js.map +1 -0
- package/dist/esm/form/_pickerInternals/dateUtils.js +267 -0
- package/dist/esm/form/_pickerInternals/dateUtils.js.map +1 -0
- package/dist/esm/form/_pickerInternals/formatters.js +322 -0
- package/dist/esm/form/_pickerInternals/formatters.js.map +1 -0
- package/dist/esm/form/_pickerInternals/haptics.js +38 -0
- package/dist/esm/form/_pickerInternals/haptics.js.map +1 -0
- package/dist/esm/form/_pickerInternals/nativeInputSupport.js +25 -0
- package/dist/esm/form/_pickerInternals/nativeInputSupport.js.map +1 -0
- package/dist/esm/form/_pickerInternals/pickerTypes.js +50 -0
- package/dist/esm/form/_pickerInternals/pickerTypes.js.map +1 -0
- package/dist/esm/form/_pickerInternals/segmentMap.js +315 -0
- package/dist/esm/form/_pickerInternals/segmentMap.js.map +1 -0
- package/dist/esm/form/_pickerInternals/useNativeMobilePicker.js +49 -0
- package/dist/esm/form/_pickerInternals/useNativeMobilePicker.js.map +1 -0
- package/dist/esm/form/_pickerInternals/useSegmentedEntry.js +427 -0
- package/dist/esm/form/_pickerInternals/useSegmentedEntry.js.map +1 -0
- package/dist/esm/form/controlLoading.js +18 -0
- package/dist/esm/form/controlLoading.js.map +1 -0
- package/dist/esm/form/useAutoLabelId.js +54 -0
- package/dist/esm/form/useAutoLabelId.js.map +1 -0
- package/dist/esm/grid/Cell.js +35 -0
- package/dist/esm/grid/Cell.js.map +1 -0
- package/dist/esm/grid/Grid.js +51 -0
- package/dist/esm/grid/Grid.js.map +1 -0
- package/dist/esm/helpers/ClientOnly.js +33 -0
- package/dist/esm/helpers/ClientOnly.js.map +1 -0
- package/dist/esm/helpers/Config.js +60 -0
- package/dist/esm/helpers/Config.js.map +1 -0
- package/dist/esm/helpers/Theme.js +947 -0
- package/dist/esm/helpers/Theme.js.map +1 -0
- package/dist/esm/helpers/anchorAttrs.js +72 -0
- package/dist/esm/helpers/anchorAttrs.js.map +1 -0
- package/dist/esm/helpers/bulmaClassHelpers.js +415 -0
- package/dist/esm/helpers/bulmaClassHelpers.js.map +1 -0
- package/dist/esm/helpers/buttonType.js +24 -0
- package/dist/esm/helpers/buttonType.js.map +1 -0
- package/dist/esm/helpers/classNames.js +137 -0
- package/dist/esm/helpers/classNames.js.map +1 -0
- package/dist/esm/helpers/colorDeprecations.js +50 -0
- package/dist/esm/helpers/colorDeprecations.js.map +1 -0
- package/dist/esm/helpers/devWarnings.js +27 -0
- package/dist/esm/helpers/devWarnings.js.map +1 -0
- package/dist/esm/helpers/inertProps.js +27 -0
- package/dist/esm/helpers/inertProps.js.map +1 -0
- package/dist/esm/helpers/mergeBulmaStyles.js +20 -0
- package/dist/esm/helpers/mergeBulmaStyles.js.map +1 -0
- package/dist/esm/helpers/polymorphic.js +18 -0
- package/dist/esm/helpers/polymorphic.js.map +1 -0
- package/dist/esm/helpers/portal.js +76 -0
- package/dist/esm/helpers/portal.js.map +1 -0
- package/dist/esm/helpers/positionStacks.js +45 -0
- package/dist/esm/helpers/positionStacks.js.map +1 -0
- package/dist/esm/helpers/scrollLock.js +38 -0
- package/dist/esm/helpers/scrollLock.js.map +1 -0
- package/dist/esm/helpers/shadowDom.js +71 -0
- package/dist/esm/helpers/shadowDom.js.map +1 -0
- package/dist/esm/helpers/statusRegion.js +282 -0
- package/dist/esm/helpers/statusRegion.js.map +1 -0
- package/dist/esm/helpers/useAnchoredPosition.js +86 -0
- package/dist/esm/helpers/useAnchoredPosition.js.map +1 -0
- package/dist/esm/helpers/useBulmaClasses.js +158 -0
- package/dist/esm/helpers/useBulmaClasses.js.map +1 -0
- package/dist/esm/helpers/useClientLayoutEffect.js +13 -0
- package/dist/esm/helpers/useClientLayoutEffect.js.map +1 -0
- package/dist/esm/helpers/useColorClasses.js +84 -0
- package/dist/esm/helpers/useColorClasses.js.map +1 -0
- package/dist/esm/helpers/useFlexboxClasses.js +92 -0
- package/dist/esm/helpers/useFlexboxClasses.js.map +1 -0
- package/dist/esm/helpers/useFocusTrap.js +470 -0
- package/dist/esm/helpers/useFocusTrap.js.map +1 -0
- package/dist/esm/helpers/useIsHydrated.js +31 -0
- package/dist/esm/helpers/useIsHydrated.js.map +1 -0
- package/dist/esm/helpers/useOtherClasses.js +115 -0
- package/dist/esm/helpers/useOtherClasses.js.map +1 -0
- package/dist/esm/helpers/useSpacingClasses.js +82 -0
- package/dist/esm/helpers/useSpacingClasses.js.map +1 -0
- package/dist/esm/helpers/useTypographyClasses.js +93 -0
- package/dist/esm/helpers/useTypographyClasses.js.map +1 -0
- package/dist/esm/helpers/useVisibilityClasses.js +134 -0
- package/dist/esm/helpers/useVisibilityClasses.js.map +1 -0
- package/dist/esm/helpers/withSubComponents.js +24 -0
- package/dist/esm/helpers/withSubComponents.js.map +1 -0
- package/dist/esm/layout/Container.js +48 -0
- package/dist/esm/layout/Container.js.map +1 -0
- package/dist/esm/layout/Footer.js +33 -0
- package/dist/esm/layout/Footer.js.map +1 -0
- package/dist/esm/layout/Hero.js +90 -0
- package/dist/esm/layout/Hero.js.map +1 -0
- package/dist/esm/layout/Level.js +110 -0
- package/dist/esm/layout/Level.js.map +1 -0
- package/dist/esm/layout/Media.js +86 -0
- package/dist/esm/layout/Media.js.map +1 -0
- package/dist/esm/layout/Section.js +30 -0
- package/dist/esm/layout/Section.js.map +1 -0
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +821 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +115 -16845
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/Tabs.d.ts +25 -3
- package/dist/types/elements/Icon.d.ts +10 -7
- package/dist/types/form/DateInputBase.d.ts +8 -6
- package/dist/types/form/DateRangeInput.d.ts +91 -0
- package/dist/types/form/DateRangeInputBase.d.ts +172 -0
- package/dist/types/form/DateTimeInputBase.d.ts +10 -6
- package/dist/types/form/File.d.ts +18 -5
- package/dist/types/form/FormContext.d.ts +11 -0
- package/dist/types/form/TimeInputBase.d.ts +1 -1
- package/dist/types/form/_pickerInternals/PickerPopover.d.ts +1 -0
- package/dist/types/form/_pickerInternals/dateUtils.d.ts +7 -0
- package/dist/types/form/_pickerInternals/formatters.d.ts +14 -0
- package/dist/types/form/_pickerInternals/pickerTypes.d.ts +13 -0
- package/dist/types/form/_pickerInternals/useSegmentedEntry.d.ts +12 -3
- package/dist/types/index.d.ts +4 -0
- package/dist/types-cjs/components/Tabs.d.ts +25 -3
- package/dist/types-cjs/elements/Icon.d.ts +10 -7
- package/dist/types-cjs/form/DateInputBase.d.ts +8 -6
- package/dist/types-cjs/form/DateRangeInput.d.ts +91 -0
- package/dist/types-cjs/form/DateRangeInputBase.d.ts +172 -0
- package/dist/types-cjs/form/DateTimeInputBase.d.ts +10 -6
- package/dist/types-cjs/form/File.d.ts +18 -5
- package/dist/types-cjs/form/FormContext.d.ts +11 -0
- package/dist/types-cjs/form/TimeInputBase.d.ts +1 -1
- package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +1 -0
- package/dist/types-cjs/form/_pickerInternals/dateUtils.d.ts +7 -0
- package/dist/types-cjs/form/_pickerInternals/formatters.d.ts +14 -0
- package/dist/types-cjs/form/_pickerInternals/pickerTypes.d.ts +13 -0
- package/dist/types-cjs/form/_pickerInternals/useSegmentedEntry.d.ts +12 -3
- package/dist/types-cjs/index.d.ts +4 -0
- package/dist/versions/bestax-no-dark-mode.css +1 -1
- package/dist/versions/bestax-no-dark-mode.css.map +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
- package/dist/versions/bestax-no-helpers.css +1 -1
- package/dist/versions/bestax-no-helpers.css.map +1 -1
- package/dist/versions/bestax-prefixed.css +1 -1
- package/dist/versions/bestax-prefixed.css.map +1 -1
- package/package.json +1 -1
- package/src/scss/components/_tabs.scss +44 -0
- package/src/scss/form/_dateinput.scss +87 -8
- package/src/scss/form/_daterangeinput.scss +63 -0
- package/src/scss/form/_datetimeinput.scss +7 -2
- package/src/scss/form/_file.scss +37 -0
- package/src/scss/form/_index.scss +2 -0
- package/src/scss/form/_picker-popover.scss +1 -4
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
import { useRef, useState, useMemo, useCallback, useId, useEffect } from 'react';
|
|
4
|
+
import { usePrefixedClassNames, prefixedClassNames, classNames } from '../../helpers/classNames.js';
|
|
5
|
+
import { useConfig } from '../../helpers/Config.js';
|
|
6
|
+
import { getActiveElementInTree } from '../../helpers/shadowDom.js';
|
|
7
|
+
import { visuallyHidden } from '../../helpers/statusRegion.js';
|
|
8
|
+
import { mergeLabels, isPopoverToggleKey } from './pickerTypes.js';
|
|
9
|
+
import { floorMin, buildMonthGrid, isDayUnselectable, isPeriodUnselectable, makeDate, canCloseRange, startOfDay, addDays, isSameDay, addYears, addMonths, startOfMonth, clampDate, FIRST_YEAR, isSameMonth } from './dateUtils.js';
|
|
10
|
+
import { getDayNames, getMonthNames, formatDate } from './formatters.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Selects the calendar's focused cell, the one tab stop of whichever grid or
|
|
14
|
+
* year list is on show, for a popover to put focus on as it opens. It runs
|
|
15
|
+
* against the whole panel, so it matches only inside the grid or the list.
|
|
16
|
+
*/
|
|
17
|
+
const CALENDAR_FOCUSED_CELL = '[role="grid"] [data-focused="true"], [role="listbox"] [data-focused-year="true"]';
|
|
18
|
+
const BASE_VIEW = {
|
|
19
|
+
day: 'days',
|
|
20
|
+
month: 'months',
|
|
21
|
+
year: 'years',
|
|
22
|
+
};
|
|
23
|
+
// Months in rows of three, so the grid is four rows deep.
|
|
24
|
+
const MONTH_ROWS = [
|
|
25
|
+
[0, 1, 2],
|
|
26
|
+
[3, 4, 5],
|
|
27
|
+
[6, 7, 8],
|
|
28
|
+
[9, 10, 11],
|
|
29
|
+
];
|
|
30
|
+
const MONTHS_PER_ROW = 3;
|
|
31
|
+
// The year list's CSS lays it out four to a row.
|
|
32
|
+
const YEARS_PER_ROW = 4;
|
|
33
|
+
/**
|
|
34
|
+
* The month grid's keyboard, after the day grid's: arrows move a month or a
|
|
35
|
+
* row, Home/End go to the row's ends, PageUp/PageDown move a year. Returns
|
|
36
|
+
* the months to move by and the direction to search past disabled months, or
|
|
37
|
+
* `null` for a key the grid doesn't handle. `col` is the focused month's
|
|
38
|
+
* column.
|
|
39
|
+
*/
|
|
40
|
+
function monthStep(key, col) {
|
|
41
|
+
switch (key) {
|
|
42
|
+
case 'ArrowLeft':
|
|
43
|
+
return [-1, -1];
|
|
44
|
+
case 'ArrowRight':
|
|
45
|
+
return [1, 1];
|
|
46
|
+
case 'ArrowUp':
|
|
47
|
+
return [-MONTHS_PER_ROW, -1];
|
|
48
|
+
case 'ArrowDown':
|
|
49
|
+
return [MONTHS_PER_ROW, 1];
|
|
50
|
+
case 'PageUp':
|
|
51
|
+
return [-12, -1];
|
|
52
|
+
case 'PageDown':
|
|
53
|
+
return [12, 1];
|
|
54
|
+
case 'Home':
|
|
55
|
+
return [-col, 1];
|
|
56
|
+
case 'End':
|
|
57
|
+
return [MONTHS_PER_ROW - 1 - col, -1];
|
|
58
|
+
default:
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether DOM focus should follow a grid's tab stop as it moves: focus is in
|
|
64
|
+
* the grid, or fell to <body> while the grid held it last, as it does when
|
|
65
|
+
* the focused cell leaves the page or becomes disabled. Focus is read in the
|
|
66
|
+
* grid's own tree, so this holds inside a shadow root too.
|
|
67
|
+
*/
|
|
68
|
+
function focusFollows(grid, heldLast) {
|
|
69
|
+
const active = getActiveElementInTree(grid);
|
|
70
|
+
if (grid.contains(active))
|
|
71
|
+
return true;
|
|
72
|
+
return heldLast && active === grid.ownerDocument.body;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The cell nearest index `i` that `disabled` leaves enabled, `i` itself when
|
|
76
|
+
* it is. At equal distance the later cell wins. With every cell disabled it
|
|
77
|
+
* stays `i`, as there is nothing to reach.
|
|
78
|
+
*/
|
|
79
|
+
function nearestEnabled(i, disabled) {
|
|
80
|
+
if (!disabled[i])
|
|
81
|
+
return i;
|
|
82
|
+
for (let d = 1; d < disabled.length; d++) {
|
|
83
|
+
if (i + d < disabled.length && !disabled[i + d])
|
|
84
|
+
return i + d;
|
|
85
|
+
if (i - d >= 0 && !disabled[i - d])
|
|
86
|
+
return i - d;
|
|
87
|
+
}
|
|
88
|
+
return i;
|
|
89
|
+
}
|
|
90
|
+
/** The long, spoken form of a date in a range's announcement. */
|
|
91
|
+
const LONG_DATE = {
|
|
92
|
+
year: 'numeric',
|
|
93
|
+
month: 'long',
|
|
94
|
+
day: 'numeric',
|
|
95
|
+
};
|
|
96
|
+
/** A range's pending start: its start, when it has no end. */
|
|
97
|
+
const pendingStartOf = (range) => range?.[0] && !range[1] ? startOfDay(range[0]) : null;
|
|
98
|
+
/**
|
|
99
|
+
* The day grid's cells in rows of a week. A grid owns rows and rows own
|
|
100
|
+
* cells, so assistive technology can walk the grid, and the selection it
|
|
101
|
+
* reports, row by row.
|
|
102
|
+
*/
|
|
103
|
+
const inWeeks = (days, className) => {
|
|
104
|
+
const rows = [];
|
|
105
|
+
for (let i = 0; i < days.length; i += 7) {
|
|
106
|
+
rows.push(jsx("div", { role: "row", className: className, children: days.slice(i, i + 7) }, i));
|
|
107
|
+
}
|
|
108
|
+
return rows;
|
|
109
|
+
};
|
|
110
|
+
/** A date's time, or `null` for none, for comparing ranges by value. */
|
|
111
|
+
const timeOf = (d) => d ? d.getTime() : null;
|
|
112
|
+
/**
|
|
113
|
+
* The year list's keyboard, as the selection surface and as navigation:
|
|
114
|
+
* arrows move a year or a row, Home/End go to the list's ends. Returns the
|
|
115
|
+
* year to start from and the direction to search past disabled years, or
|
|
116
|
+
* `null` for a key the list doesn't handle. As navigation no year is
|
|
117
|
+
* disabled, so only the year is used.
|
|
118
|
+
*/
|
|
119
|
+
function yearStep(key, focusedYear, years) {
|
|
120
|
+
switch (key) {
|
|
121
|
+
case 'ArrowLeft':
|
|
122
|
+
return [focusedYear - 1, -1];
|
|
123
|
+
case 'ArrowRight':
|
|
124
|
+
return [focusedYear + 1, 1];
|
|
125
|
+
case 'ArrowUp':
|
|
126
|
+
return [focusedYear - YEARS_PER_ROW, -1];
|
|
127
|
+
case 'ArrowDown':
|
|
128
|
+
return [focusedYear + YEARS_PER_ROW, 1];
|
|
129
|
+
case 'Home':
|
|
130
|
+
return [years[0], 1];
|
|
131
|
+
case 'End':
|
|
132
|
+
return [years[years.length - 1], -1];
|
|
133
|
+
default:
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minProp, max, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, locale, dayNames, monthNames, nearbyMonthDays = true, color, size, className, id, autoFocusCell = false, labels, yearsRange, granularity = 'day', range, onRangeSelect, allowDisabledInRange = false, }) => {
|
|
138
|
+
const gridRef = useRef(null);
|
|
139
|
+
const monthGridRef = useRef(null);
|
|
140
|
+
const yearGridRef = useRef(null);
|
|
141
|
+
const { classPrefix } = useConfig();
|
|
142
|
+
const t = mergeLabels(labels);
|
|
143
|
+
const baseView = BASE_VIEW[granularity];
|
|
144
|
+
const isDayGranularity = granularity === 'day';
|
|
145
|
+
const isYearGranularity = granularity === 'year';
|
|
146
|
+
const [view, setView] = useState(baseView);
|
|
147
|
+
// Nothing before year 1 can be picked or reached, as in HTML's date inputs.
|
|
148
|
+
const min = useMemo(() => floorMin(minProp), [minProp]);
|
|
149
|
+
const computedDayNames = useMemo(() => {
|
|
150
|
+
if (dayNames && dayNames.length === 7)
|
|
151
|
+
return dayNames;
|
|
152
|
+
const sundayFirst = getDayNames(locale, 'short');
|
|
153
|
+
return [
|
|
154
|
+
...sundayFirst.slice(firstDayOfWeek),
|
|
155
|
+
...sundayFirst.slice(0, firstDayOfWeek),
|
|
156
|
+
];
|
|
157
|
+
}, [dayNames, locale, firstDayOfWeek]);
|
|
158
|
+
const computedMonthNames = useMemo(() => {
|
|
159
|
+
if (monthNames && monthNames.length === 12)
|
|
160
|
+
return monthNames;
|
|
161
|
+
return getMonthNames(locale, 'long');
|
|
162
|
+
}, [monthNames, locale]);
|
|
163
|
+
// The month grid's cells are too narrow for long names, so they show the
|
|
164
|
+
// short form unless the caller supplied names of their own.
|
|
165
|
+
const monthCellNames = useMemo(() => {
|
|
166
|
+
if (monthNames && monthNames.length === 12)
|
|
167
|
+
return monthNames;
|
|
168
|
+
return getMonthNames(locale, 'short');
|
|
169
|
+
}, [monthNames, locale]);
|
|
170
|
+
const cells = useMemo(() => buildMonthGrid(focusedDate, firstDayOfWeek), [focusedDate, firstDayOfWeek]);
|
|
171
|
+
const isDateUnselectable = useCallback((d) => isDayUnselectable(d, { min, max, shouldDisableDate, unselectableDates }), [min, max, shouldDisableDate, unselectableDates]);
|
|
172
|
+
// A month or year is out of reach only when every day in it is.
|
|
173
|
+
const isMonthUnselectable = useCallback((d) => isPeriodUnselectable(d, 'month', {
|
|
174
|
+
min,
|
|
175
|
+
max,
|
|
176
|
+
shouldDisableDate,
|
|
177
|
+
unselectableDates,
|
|
178
|
+
}), [min, max, shouldDisableDate, unselectableDates]);
|
|
179
|
+
const isYearUnselectable = useCallback((year) => isPeriodUnselectable(makeDate(year), 'year', {
|
|
180
|
+
min,
|
|
181
|
+
max,
|
|
182
|
+
shouldDisableDate,
|
|
183
|
+
unselectableDates,
|
|
184
|
+
}), [min, max, shouldDisableDate, unselectableDates]);
|
|
185
|
+
// The day grid's one tab stop. A disabled button can't take focus, so when
|
|
186
|
+
// the focused day is disabled the stop moves to the nearest enabled day of
|
|
187
|
+
// the month on show, and focusing it makes it the focused day. A nearby
|
|
188
|
+
// month's day is left out, as focusing it would turn the grid to its month.
|
|
189
|
+
const tabStopDay = useMemo(() => {
|
|
190
|
+
const days = cells.filter(c => c.inCurrentMonth).map(c => c.date);
|
|
191
|
+
const i = nearestEnabled(focusedDate.getDate() - 1, days.map(isDateUnselectable));
|
|
192
|
+
return days[i];
|
|
193
|
+
}, [cells, focusedDate, isDateUnselectable]);
|
|
194
|
+
// ----- Range mode -----
|
|
195
|
+
// Range props come only with the day grid.
|
|
196
|
+
const rangeMode = !!range;
|
|
197
|
+
const valuePending = pendingStartOf(range);
|
|
198
|
+
// The start of a range being picked, which nothing outside the calendar
|
|
199
|
+
// sees until its end is picked too. It starts as the range's own pending
|
|
200
|
+
// start, and starts there again whenever the range changes from outside.
|
|
201
|
+
const [anchor, setAnchor] = useState(valuePending);
|
|
202
|
+
const rangeKey = `${timeOf(range?.[0])}|${timeOf(range?.[1])}`;
|
|
203
|
+
const [anchorRangeKey, setAnchorRangeKey] = useState(rangeKey);
|
|
204
|
+
if (anchorRangeKey !== rangeKey) {
|
|
205
|
+
setAnchorRangeKey(rangeKey);
|
|
206
|
+
setAnchor(valuePending);
|
|
207
|
+
}
|
|
208
|
+
// The day under the pointer while a start is pending, which the preview
|
|
209
|
+
// runs to ahead of the focused day.
|
|
210
|
+
const [hoverDate, setHoverDate] = useState(null);
|
|
211
|
+
// Whether `day` can end a range that starts on `start`. The start is
|
|
212
|
+
// checked as well as the end: one the range brought, or one picked before
|
|
213
|
+
// the bounds or the disabled days changed, can rule itself out.
|
|
214
|
+
const canEndAt = useCallback((start, day) => !isDateUnselectable(start) &&
|
|
215
|
+
!isDateUnselectable(day) &&
|
|
216
|
+
canCloseRange(start, day, { shouldDisableDate, unselectableDates }, allowDisabledInRange), [
|
|
217
|
+
isDateUnselectable,
|
|
218
|
+
shouldDisableDate,
|
|
219
|
+
unselectableDates,
|
|
220
|
+
allowDisabledInRange,
|
|
221
|
+
]);
|
|
222
|
+
// A pick in range mode: the end of the pending range when it can be one,
|
|
223
|
+
// and otherwise the start of a new one.
|
|
224
|
+
const pickRangeDay = useCallback((picked) => {
|
|
225
|
+
const day = startOfDay(picked);
|
|
226
|
+
// The pointer is on the picked day, which the focused day now is too.
|
|
227
|
+
setHoverDate(null);
|
|
228
|
+
if (anchor && canEndAt(anchor, day)) {
|
|
229
|
+
setAnchor(null);
|
|
230
|
+
onRangeSelect?.(anchor, day);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
setAnchor(day);
|
|
234
|
+
}
|
|
235
|
+
}, [anchor, canEndAt, onRangeSelect]);
|
|
236
|
+
// What the grid shows: while a start is pending, the range from it to the
|
|
237
|
+
// hovered or focused day, when that day can end it; otherwise the
|
|
238
|
+
// committed range.
|
|
239
|
+
let shownStart;
|
|
240
|
+
let shownEnd;
|
|
241
|
+
if (anchor) {
|
|
242
|
+
shownStart = anchor;
|
|
243
|
+
const previewEnd = startOfDay(hoverDate ?? focusedDate);
|
|
244
|
+
shownEnd = canEndAt(anchor, previewEnd) ? previewEnd : null;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
shownStart = range?.[0] ? startOfDay(range[0]) : null;
|
|
248
|
+
shownEnd = range?.[1] ? startOfDay(range[1]) : null;
|
|
249
|
+
}
|
|
250
|
+
const previewing = !!anchor;
|
|
251
|
+
// Ids for the words that name a range's ends and state the whole range.
|
|
252
|
+
const reactId = useId();
|
|
253
|
+
const rangeIdBase = id ?? `calendar-${reactId}`;
|
|
254
|
+
const rangeStartId = `${rangeIdBase}-range-start`;
|
|
255
|
+
const rangeEndId = `${rangeIdBase}-range-end`;
|
|
256
|
+
const rangePreviewEndId = `${rangeIdBase}-range-preview-end`;
|
|
257
|
+
const rangeSummaryId = `${rangeIdBase}-range-summary`;
|
|
258
|
+
// The selection in words: the pending start alone, or the committed ends.
|
|
259
|
+
const selectedEnd = previewing ? null : shownEnd;
|
|
260
|
+
const rangeSummary = [
|
|
261
|
+
shownStart &&
|
|
262
|
+
`${t.rangeStart}: ${formatDate(shownStart, LONG_DATE, locale)}`,
|
|
263
|
+
selectedEnd &&
|
|
264
|
+
`${t.rangeEnd}: ${formatDate(selectedEnd, LONG_DATE, locale)}`,
|
|
265
|
+
]
|
|
266
|
+
.filter(Boolean)
|
|
267
|
+
.join(', ');
|
|
268
|
+
// Whether the grid on show holds focus, or held it last before focus went
|
|
269
|
+
// nowhere: the cell that had it left the page or became disabled. A blur
|
|
270
|
+
// that takes focus out of the grid, to nowhere included, is the user
|
|
271
|
+
// leaving, so it clears this. A render that removes or disables the focused
|
|
272
|
+
// cell sends no blur here: browsers fire none, or fire it inside React's
|
|
273
|
+
// commit, where React delivers no events.
|
|
274
|
+
const gridHeldFocusRef = useRef(false);
|
|
275
|
+
const gridFocusHandlers = {
|
|
276
|
+
onFocus: () => {
|
|
277
|
+
gridHeldFocusRef.current = true;
|
|
278
|
+
},
|
|
279
|
+
onBlur: (e) => {
|
|
280
|
+
const next = e.relatedTarget;
|
|
281
|
+
if (!next || !e.currentTarget.contains(next)) {
|
|
282
|
+
gridHeldFocusRef.current = false;
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
// Step from `next` by `direction` days until we hit a selectable date,
|
|
287
|
+
// skipping past dates blocked by min/max, shouldDisableDate, or
|
|
288
|
+
// unselectableDates. If the whole searched range is disabled, focus stays
|
|
289
|
+
// put. Cap iterations to guard against pathologically empty ranges.
|
|
290
|
+
const moveFocus = useCallback((next, direction) => {
|
|
291
|
+
let candidate = next;
|
|
292
|
+
for (let i = 0; i < 366; i++) {
|
|
293
|
+
if (candidate.getTime() < startOfDay(min).getTime())
|
|
294
|
+
return;
|
|
295
|
+
if (max && candidate.getTime() > startOfDay(max).getTime())
|
|
296
|
+
return;
|
|
297
|
+
if (!isDateUnselectable(candidate)) {
|
|
298
|
+
onFocusedDateChange(candidate);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
candidate = addDays(candidate, direction);
|
|
302
|
+
}
|
|
303
|
+
}, [min, max, isDateUnselectable, onFocusedDateChange]);
|
|
304
|
+
const handleKeyDown = useCallback((e) => {
|
|
305
|
+
if (isPopoverToggleKey(e))
|
|
306
|
+
return;
|
|
307
|
+
// A key takes the range preview back from the pointer.
|
|
308
|
+
setHoverDate(null);
|
|
309
|
+
switch (e.key) {
|
|
310
|
+
case 'ArrowLeft':
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
moveFocus(addDays(focusedDate, -1), -1);
|
|
313
|
+
break;
|
|
314
|
+
case 'ArrowRight':
|
|
315
|
+
e.preventDefault();
|
|
316
|
+
moveFocus(addDays(focusedDate, 1), 1);
|
|
317
|
+
break;
|
|
318
|
+
case 'ArrowUp':
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
moveFocus(addDays(focusedDate, -7), -1);
|
|
321
|
+
break;
|
|
322
|
+
case 'ArrowDown':
|
|
323
|
+
e.preventDefault();
|
|
324
|
+
moveFocus(addDays(focusedDate, 7), 1);
|
|
325
|
+
break;
|
|
326
|
+
case 'PageUp':
|
|
327
|
+
e.preventDefault();
|
|
328
|
+
moveFocus(e.shiftKey ? addYears(focusedDate, -1) : addMonths(focusedDate, -1), -1);
|
|
329
|
+
break;
|
|
330
|
+
case 'PageDown':
|
|
331
|
+
e.preventDefault();
|
|
332
|
+
moveFocus(e.shiftKey ? addYears(focusedDate, 1) : addMonths(focusedDate, 1), 1);
|
|
333
|
+
break;
|
|
334
|
+
case 'Home': {
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
const dayOfWeek = focusedDate.getDay();
|
|
337
|
+
const offset = (dayOfWeek - firstDayOfWeek + 7) % 7;
|
|
338
|
+
moveFocus(addDays(focusedDate, -offset), 1);
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
case 'End': {
|
|
342
|
+
e.preventDefault();
|
|
343
|
+
const dayOfWeek = focusedDate.getDay();
|
|
344
|
+
const offset = (dayOfWeek - firstDayOfWeek + 7) % 7;
|
|
345
|
+
moveFocus(addDays(focusedDate, 6 - offset), -1);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case 'Enter':
|
|
349
|
+
case ' ':
|
|
350
|
+
e.preventDefault();
|
|
351
|
+
if (isDateUnselectable(focusedDate))
|
|
352
|
+
break;
|
|
353
|
+
if (rangeMode)
|
|
354
|
+
pickRangeDay(focusedDate);
|
|
355
|
+
else
|
|
356
|
+
onSelect?.(focusedDate);
|
|
357
|
+
break;
|
|
358
|
+
case 'Escape':
|
|
359
|
+
// A pending pick is taken back, and the key stops there so a
|
|
360
|
+
// popover around the calendar stays open. The range's own pending
|
|
361
|
+
// start is no pick, so Escape goes on to close the popover.
|
|
362
|
+
if (anchor && !(valuePending && isSameDay(anchor, valuePending))) {
|
|
363
|
+
e.preventDefault();
|
|
364
|
+
e.stopPropagation();
|
|
365
|
+
setAnchor(valuePending);
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}, [
|
|
370
|
+
focusedDate,
|
|
371
|
+
firstDayOfWeek,
|
|
372
|
+
isDateUnselectable,
|
|
373
|
+
moveFocus,
|
|
374
|
+
onSelect,
|
|
375
|
+
rangeMode,
|
|
376
|
+
pickRangeDay,
|
|
377
|
+
anchor,
|
|
378
|
+
valuePending,
|
|
379
|
+
]);
|
|
380
|
+
// The month grid's counterpart to `moveFocus`: step by `direction` months
|
|
381
|
+
// until a month with a selectable day turns up, stopping at the min/max
|
|
382
|
+
// months. The cap guards against a range with nothing selectable.
|
|
383
|
+
const moveMonthFocus = useCallback((next, direction) => {
|
|
384
|
+
let candidate = next;
|
|
385
|
+
for (let i = 0; i < 120; i++) {
|
|
386
|
+
const month = startOfMonth(candidate).getTime();
|
|
387
|
+
if (month < startOfMonth(min).getTime())
|
|
388
|
+
return;
|
|
389
|
+
if (max && month > startOfMonth(max).getTime())
|
|
390
|
+
return;
|
|
391
|
+
if (!isMonthUnselectable(candidate)) {
|
|
392
|
+
onFocusedDateChange(candidate);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
candidate = addMonths(candidate, direction);
|
|
396
|
+
}
|
|
397
|
+
}, [min, max, isMonthUnselectable, onFocusedDateChange]);
|
|
398
|
+
const handleMonthKeyDown = useCallback((e) => {
|
|
399
|
+
if (isPopoverToggleKey(e))
|
|
400
|
+
return;
|
|
401
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
402
|
+
e.preventDefault();
|
|
403
|
+
if (!isMonthUnselectable(focusedDate)) {
|
|
404
|
+
onSelect?.(startOfMonth(focusedDate));
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const step = monthStep(e.key, focusedDate.getMonth() % MONTHS_PER_ROW);
|
|
409
|
+
if (!step)
|
|
410
|
+
return;
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
moveMonthFocus(addMonths(focusedDate, step[0]), step[1]);
|
|
413
|
+
}, [focusedDate, isMonthUnselectable, moveMonthFocus, onSelect]);
|
|
414
|
+
const calendarClass = usePrefixedClassNames('dateinput', {
|
|
415
|
+
[`is-${color}`]: !!color,
|
|
416
|
+
[`is-${size}`]: !!size,
|
|
417
|
+
});
|
|
418
|
+
const headerClass = usePrefixedClassNames('dateinput-header');
|
|
419
|
+
const monthTriggerClass = usePrefixedClassNames('dateinput-month-trigger', {
|
|
420
|
+
'is-active': view === 'years',
|
|
421
|
+
});
|
|
422
|
+
const monthCaretClass = usePrefixedClassNames('dateinput-month-caret');
|
|
423
|
+
const navGroupClass = usePrefixedClassNames('dateinput-nav-group');
|
|
424
|
+
const navPrevClass = usePrefixedClassNames('dateinput-nav-prev');
|
|
425
|
+
const navNextClass = usePrefixedClassNames('dateinput-nav-next');
|
|
426
|
+
const monthLabelClass = usePrefixedClassNames('dateinput-month-label');
|
|
427
|
+
const dayNamesRowClass = usePrefixedClassNames('dateinput-day-names');
|
|
428
|
+
const dayNameClass = usePrefixedClassNames('dateinput-day-name');
|
|
429
|
+
const gridClass = usePrefixedClassNames('dateinput-grid');
|
|
430
|
+
const weekClass = usePrefixedClassNames('dateinput-week');
|
|
431
|
+
const monthsGridClass = usePrefixedClassNames('dateinput-months-grid');
|
|
432
|
+
const monthsRowClass = usePrefixedClassNames('dateinput-months-row');
|
|
433
|
+
const yearsGridClass = usePrefixedClassNames('dateinput-years-grid');
|
|
434
|
+
const focusedYear = focusedDate.getFullYear();
|
|
435
|
+
const today = new Date();
|
|
436
|
+
const todayYear = today.getFullYear();
|
|
437
|
+
// The header steps a month at a time over the day grid and a year at a
|
|
438
|
+
// time over the month grid, kept inside min/max. A step is off once the
|
|
439
|
+
// month or year on show holds the bound or lies past it, as picking a year
|
|
440
|
+
// from the list can leave it.
|
|
441
|
+
const prevAnchor = isDayGranularity
|
|
442
|
+
? addMonths(focusedDate, -1)
|
|
443
|
+
: clampDate(addYears(focusedDate, -1), min, max);
|
|
444
|
+
const nextAnchor = isDayGranularity
|
|
445
|
+
? addMonths(focusedDate, 1)
|
|
446
|
+
: clampDate(addYears(focusedDate, 1), min, max);
|
|
447
|
+
const focusedMonth = startOfMonth(focusedDate).getTime();
|
|
448
|
+
const prevDisabled = isDayGranularity
|
|
449
|
+
? focusedMonth <= startOfMonth(min).getTime()
|
|
450
|
+
: focusedYear <= min.getFullYear();
|
|
451
|
+
const nextDisabled = isDayGranularity
|
|
452
|
+
? !!max && focusedMonth >= startOfMonth(max).getTime()
|
|
453
|
+
: !!max && focusedYear >= max.getFullYear();
|
|
454
|
+
const labelId = id ? `${id}-label` : undefined;
|
|
455
|
+
const monthLabel = isDayGranularity
|
|
456
|
+
? `${computedMonthNames[focusedDate.getMonth()]} ${focusedYear}`
|
|
457
|
+
: String(focusedYear);
|
|
458
|
+
// As the selection surface the year list is where focus moves, so its
|
|
459
|
+
// window stays centred on the year focused at mount: re-centring on every
|
|
460
|
+
// move would reflow the list under the keyboard.
|
|
461
|
+
const [mountYear] = useState(focusedYear);
|
|
462
|
+
const windowYear = isYearGranularity ? mountYear : focusedYear;
|
|
463
|
+
const yearList = useMemo(() => {
|
|
464
|
+
const minYear = minProp
|
|
465
|
+
? min.getFullYear()
|
|
466
|
+
: Math.max(FIRST_YEAR, windowYear - 100);
|
|
467
|
+
const maxYear = max ? max.getFullYear() : windowYear + 100;
|
|
468
|
+
const [lo, hi] = yearsRange ?? [minYear, maxYear];
|
|
469
|
+
const start = Math.max(lo, minYear);
|
|
470
|
+
const end = Math.min(hi, maxYear);
|
|
471
|
+
const out = [];
|
|
472
|
+
for (let y = start; y <= end; y++)
|
|
473
|
+
out.push(y);
|
|
474
|
+
return out;
|
|
475
|
+
}, [windowYear, minProp, min, max, yearsRange]);
|
|
476
|
+
// Only the selection surface disables years; as navigation every listed
|
|
477
|
+
// year can be visited.
|
|
478
|
+
const disabledYears = useMemo(() => new Set(isYearGranularity ? yearList.filter(y => isYearUnselectable(y)) : []), [isYearGranularity, yearList, isYearUnselectable]);
|
|
479
|
+
// The year list's one tab stop, after the month grid's: when the focused
|
|
480
|
+
// year is disabled the stop moves to the nearest enabled year in the list,
|
|
481
|
+
// and focusing it makes it the focused year. As the selection surface a
|
|
482
|
+
// year outside a non-empty list starts from the end it fell past, so the
|
|
483
|
+
// list keeps a stop. As navigation it marks the current year, so a year
|
|
484
|
+
// outside the list marks no option, and the stop itself follows focus.
|
|
485
|
+
const tabStopYear = useMemo(() => {
|
|
486
|
+
let i = yearList.indexOf(focusedYear);
|
|
487
|
+
if (i < 0) {
|
|
488
|
+
if (!isYearGranularity || yearList.length === 0)
|
|
489
|
+
return focusedYear;
|
|
490
|
+
i = focusedYear < yearList[0] ? 0 : yearList.length - 1;
|
|
491
|
+
}
|
|
492
|
+
return yearList[nearestEnabled(i, yearList.map(y => disabledYears.has(y)))];
|
|
493
|
+
}, [yearList, focusedYear, disabledYears, isYearGranularity]);
|
|
494
|
+
// As navigation the keys move focus and leave the focused year alone, so
|
|
495
|
+
// the tab stop goes to the year that last had focus while it is listed,
|
|
496
|
+
// and Tab leaves the list rather than landing on the current year. It
|
|
497
|
+
// starts over each time the list opens.
|
|
498
|
+
const [navYear, setNavYear] = useState(null);
|
|
499
|
+
const listTabStop = navYear !== null && yearList.includes(navYear) ? navYear : tabStopYear;
|
|
500
|
+
// Which of the focused year's months have no selectable day.
|
|
501
|
+
const disabledMonths = useMemo(() => granularity === 'month'
|
|
502
|
+
? Array.from({ length: 12 }, (_, m) => isMonthUnselectable(makeDate(focusedYear, m)))
|
|
503
|
+
: [], [granularity, focusedYear, isMonthUnselectable]);
|
|
504
|
+
// The month grid's one tab stop. A disabled button can't take focus, so
|
|
505
|
+
// when the focused month is disabled the stop moves to the nearest enabled
|
|
506
|
+
// month, and focusing it makes it the focused month.
|
|
507
|
+
const tabStopMonth = nearestEnabled(focusedDate.getMonth(), disabledMonths);
|
|
508
|
+
const handleYearSelect = useCallback((year) => {
|
|
509
|
+
const next = new Date(focusedDate);
|
|
510
|
+
next.setFullYear(year);
|
|
511
|
+
// Keep month/day. The month grid also clamps into [min, max] so its
|
|
512
|
+
// focused month is never one the bounds rule out.
|
|
513
|
+
onFocusedDateChange(isDayGranularity ? next : clampDate(next, min, max));
|
|
514
|
+
setView(baseView);
|
|
515
|
+
}, [focusedDate, onFocusedDateChange, isDayGranularity, min, max, baseView]);
|
|
516
|
+
// Picking from the year list as the selection surface: focus follows the
|
|
517
|
+
// pick and `onSelect` gets the year's first day.
|
|
518
|
+
const handleYearPick = useCallback((year) => {
|
|
519
|
+
onFocusedDateChange(clampDate(makeDate(year), min, max));
|
|
520
|
+
onSelect?.(makeDate(year));
|
|
521
|
+
}, [min, max, onFocusedDateChange, onSelect]);
|
|
522
|
+
// Step through the listed years from `year` by `direction`, skipping
|
|
523
|
+
// disabled ones, and focus the first selectable year found.
|
|
524
|
+
const moveYearFocus = useCallback((year, direction) => {
|
|
525
|
+
const first = yearList[0];
|
|
526
|
+
const last = yearList[yearList.length - 1];
|
|
527
|
+
for (let y = year; y >= first && y <= last; y += direction) {
|
|
528
|
+
if (!disabledYears.has(y)) {
|
|
529
|
+
onFocusedDateChange(clampDate(makeDate(y), min, max));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}, [yearList, disabledYears, onFocusedDateChange, min, max]);
|
|
534
|
+
const handleYearListKeyDown = useCallback((e) => {
|
|
535
|
+
if (isPopoverToggleKey(e))
|
|
536
|
+
return;
|
|
537
|
+
if (isYearGranularity) {
|
|
538
|
+
const step = yearStep(e.key, focusedYear, yearList);
|
|
539
|
+
if (!step)
|
|
540
|
+
return;
|
|
541
|
+
e.preventDefault();
|
|
542
|
+
moveYearFocus(step[0], step[1]);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// As navigation, Escape returns to the grid the list was opened from.
|
|
546
|
+
// It stops there, so a popover around the calendar stays open and only
|
|
547
|
+
// a second Escape closes it.
|
|
548
|
+
if (e.key === 'Escape') {
|
|
549
|
+
e.preventDefault();
|
|
550
|
+
e.stopPropagation();
|
|
551
|
+
setView(baseView);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
// The other keys move DOM focus from the year that has it and leave
|
|
555
|
+
// the focused date alone. The list is centred on that date, so moving
|
|
556
|
+
// it would reflow the list under the keyboard, and it would turn the
|
|
557
|
+
// grid behind the list before anything is picked. Enter or Space
|
|
558
|
+
// clicks the year with focus, which picks it.
|
|
559
|
+
const options = Array.from(e.currentTarget.querySelectorAll('[role="option"]'));
|
|
560
|
+
const from = options.indexOf(e.target);
|
|
561
|
+
if (from < 0)
|
|
562
|
+
return;
|
|
563
|
+
const step = yearStep(e.key, yearList[from], yearList);
|
|
564
|
+
if (!step)
|
|
565
|
+
return;
|
|
566
|
+
e.preventDefault();
|
|
567
|
+
// The listed years run one apart, so a year's index is its distance
|
|
568
|
+
// from the first. A step past either end finds no option.
|
|
569
|
+
options[step[0] - yearList[0]]?.focus();
|
|
570
|
+
}, [isYearGranularity, baseView, focusedYear, yearList, moveYearFocus]);
|
|
571
|
+
// The day or month grid takes focus as the popover opens on it, or comes
|
|
572
|
+
// back to it from the year list.
|
|
573
|
+
useEffect(() => {
|
|
574
|
+
if (!autoFocusCell)
|
|
575
|
+
return;
|
|
576
|
+
const grid = view === 'days'
|
|
577
|
+
? gridRef.current
|
|
578
|
+
: view === 'months'
|
|
579
|
+
? monthGridRef.current
|
|
580
|
+
: null;
|
|
581
|
+
grid?.querySelector('[data-focused="true"]')?.focus();
|
|
582
|
+
}, [autoFocusCell, view]);
|
|
583
|
+
// From then on focus follows the tab stop only while the grid has it, or
|
|
584
|
+
// has just lost it, so focus on a header button or a time wheel stays put.
|
|
585
|
+
const tabStopDayTime = tabStopDay.getTime();
|
|
586
|
+
useEffect(() => {
|
|
587
|
+
const grid = gridRef.current;
|
|
588
|
+
if (view !== 'days' || !grid)
|
|
589
|
+
return;
|
|
590
|
+
if (!focusFollows(grid, gridHeldFocusRef.current))
|
|
591
|
+
return;
|
|
592
|
+
grid.querySelector('[data-focused="true"]')?.focus();
|
|
593
|
+
}, [tabStopDayTime, view]);
|
|
594
|
+
useEffect(() => {
|
|
595
|
+
const grid = monthGridRef.current;
|
|
596
|
+
if (view !== 'months' || !grid)
|
|
597
|
+
return;
|
|
598
|
+
if (!focusFollows(grid, gridHeldFocusRef.current))
|
|
599
|
+
return;
|
|
600
|
+
grid.querySelector('[data-focused="true"]')?.focus();
|
|
601
|
+
}, [focusedYear, tabStopMonth, view]);
|
|
602
|
+
// When the year view opens as navigation, scroll the focused year into
|
|
603
|
+
// view.
|
|
604
|
+
useEffect(() => {
|
|
605
|
+
if (view !== 'years' || isYearGranularity || !yearGridRef.current)
|
|
606
|
+
return;
|
|
607
|
+
const sel = yearGridRef.current.querySelector('[data-focused-year="true"]');
|
|
608
|
+
sel?.scrollIntoView({ block: 'center' });
|
|
609
|
+
sel?.focus();
|
|
610
|
+
}, [view, isYearGranularity]);
|
|
611
|
+
// As the selection surface the year list opens with the focused year in
|
|
612
|
+
// its middle. It scrolls the list itself rather than calling
|
|
613
|
+
// `scrollIntoView`, which would also scroll the page to an inline calendar
|
|
614
|
+
// on load, and focuses that year only when the popover asks.
|
|
615
|
+
useEffect(() => {
|
|
616
|
+
const list = yearGridRef.current;
|
|
617
|
+
if (!isYearGranularity || !list)
|
|
618
|
+
return;
|
|
619
|
+
const target = list.querySelector('[data-focused-year="true"]');
|
|
620
|
+
if (!target)
|
|
621
|
+
return;
|
|
622
|
+
const rect = target.getBoundingClientRect();
|
|
623
|
+
list.scrollTop +=
|
|
624
|
+
rect.top -
|
|
625
|
+
list.getBoundingClientRect().top -
|
|
626
|
+
(list.clientHeight - rect.height) / 2;
|
|
627
|
+
if (autoFocusCell)
|
|
628
|
+
target.focus({ preventScroll: true });
|
|
629
|
+
}, [isYearGranularity, autoFocusCell]);
|
|
630
|
+
// Keyboard moves keep DOM focus on the focused year while the list has it.
|
|
631
|
+
useEffect(() => {
|
|
632
|
+
const list = yearGridRef.current;
|
|
633
|
+
if (!isYearGranularity || !list)
|
|
634
|
+
return;
|
|
635
|
+
if (!list.contains(getActiveElementInTree(list)))
|
|
636
|
+
return;
|
|
637
|
+
list.querySelector('[data-focused-year="true"]')?.focus();
|
|
638
|
+
}, [isYearGranularity, focusedYear]);
|
|
639
|
+
const navArrow = (d) => (jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", children: jsx("path", { d: d, fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
640
|
+
return (jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => {
|
|
641
|
+
setNavYear(null);
|
|
642
|
+
setView(v => (v === 'years' ? baseView : 'years'));
|
|
643
|
+
}, children: [jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxs("div", { className: navGroupClass, children: [jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxs(Fragment, { children: [jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsx("div", { className: dayNameClass, children: name }, i))) }), jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, "aria-multiselectable": rangeMode || undefined, "aria-describedby": rangeMode ? rangeSummaryId : undefined, className: gridClass, onKeyDown: handleKeyDown, onMouseLeave: anchor ? () => setHoverDate(null) : undefined, ...gridFocusHandlers, children: inWeeks(cells.map(cell => {
|
|
644
|
+
const disabled = isDateUnselectable(cell.date);
|
|
645
|
+
const time = cell.date.getTime();
|
|
646
|
+
// Range mode. The range on show has its ends filled. Every day
|
|
647
|
+
// of a committed range is selected, and of a preview only the
|
|
648
|
+
// start.
|
|
649
|
+
const isStart = time === timeOf(shownStart);
|
|
650
|
+
const isEnd = time === timeOf(shownEnd);
|
|
651
|
+
const inRange = !!shownStart &&
|
|
652
|
+
!!shownEnd &&
|
|
653
|
+
time > shownStart.getTime() &&
|
|
654
|
+
time < shownEnd.getTime();
|
|
655
|
+
// The day a pending range would end on, which isn't picked
|
|
656
|
+
// yet, so its description says what picking it does.
|
|
657
|
+
const isPreviewEnd = previewing && isEnd && !isStart;
|
|
658
|
+
const isPreview = (previewing && inRange) || isPreviewEnd;
|
|
659
|
+
const isFilled = rangeMode
|
|
660
|
+
? isStart || (isEnd && !previewing)
|
|
661
|
+
: !!value && isSameDay(value, cell.date);
|
|
662
|
+
const isSelected = isFilled || (rangeMode && !previewing && inRange);
|
|
663
|
+
const describedBy = rangeMode
|
|
664
|
+
? [
|
|
665
|
+
isStart && rangeStartId,
|
|
666
|
+
isEnd && !previewing && rangeEndId,
|
|
667
|
+
isPreviewEnd && rangePreviewEndId,
|
|
668
|
+
]
|
|
669
|
+
.filter(Boolean)
|
|
670
|
+
.join(' ') || undefined
|
|
671
|
+
: undefined;
|
|
672
|
+
const isFocused = isSameDay(cell.date, tabStopDay);
|
|
673
|
+
const otherMonth = !cell.inCurrentMonth;
|
|
674
|
+
const cellClass = prefixedClassNames(classPrefix, 'dateinput-cell', {
|
|
675
|
+
'is-selected': isFilled,
|
|
676
|
+
'is-range-start': isStart,
|
|
677
|
+
'is-range-end': isEnd,
|
|
678
|
+
'is-in-range': inRange,
|
|
679
|
+
'is-preview': isPreview,
|
|
680
|
+
'is-today': cell.isToday,
|
|
681
|
+
'is-disabled': disabled,
|
|
682
|
+
'is-other-month': otherMonth,
|
|
683
|
+
});
|
|
684
|
+
const display = !otherMonth || nearbyMonthDays;
|
|
685
|
+
return (jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": cell.isToday ? 'date' : undefined, "aria-describedby": describedBy, "data-focused": isFocused ? 'true' : undefined, disabled: disabled || !display, className: cellClass, onFocus: () => {
|
|
686
|
+
// Reached by Tab while the focused day is disabled, or
|
|
687
|
+
// by pointer: keys move on from here. A nearby month's
|
|
688
|
+
// day waits for its click, which turns the grid.
|
|
689
|
+
if (!otherMonth && !isSameDay(cell.date, focusedDate)) {
|
|
690
|
+
onFocusedDateChange(cell.date);
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
// Mouseover rather than mouseenter: browsers send it to a
|
|
694
|
+
// disabled day as well, where React holds mouseenter back,
|
|
695
|
+
// so the preview leaves the last enabled day behind.
|
|
696
|
+
onMouseOver: anchor ? () => setHoverDate(cell.date) : undefined, onClick: () => {
|
|
697
|
+
if (disabled)
|
|
698
|
+
return;
|
|
699
|
+
onFocusedDateChange(cell.date);
|
|
700
|
+
if (rangeMode)
|
|
701
|
+
pickRangeDay(cell.date);
|
|
702
|
+
else
|
|
703
|
+
onSelect?.(cell.date);
|
|
704
|
+
}, style: !display ? { visibility: 'hidden' } : undefined, children: cell.date.getDate() }, cell.date.toISOString()));
|
|
705
|
+
}), weekClass) })] })), view === 'months' && (jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
|
|
706
|
+
const date = makeDate(focusedYear, month);
|
|
707
|
+
const disabled = disabledMonths[month];
|
|
708
|
+
const isSelected = !!value && isSameMonth(value, date);
|
|
709
|
+
const isFocused = month === tabStopMonth;
|
|
710
|
+
const isCurrent = isSameMonth(date, today);
|
|
711
|
+
const cellClass = prefixedClassNames(classPrefix, 'dateinput-month-cell', {
|
|
712
|
+
'is-selected': isSelected,
|
|
713
|
+
'is-today': isCurrent,
|
|
714
|
+
'is-disabled': disabled,
|
|
715
|
+
});
|
|
716
|
+
return (jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": isCurrent ? 'date' : undefined, "aria-label": computedMonthNames[month], "data-focused": isFocused ? 'true' : undefined, disabled: disabled, className: cellClass, onFocus: () => {
|
|
717
|
+
// Reached by Tab while the focused month is disabled,
|
|
718
|
+
// or by pointer: keys move on from here.
|
|
719
|
+
if (month !== focusedDate.getMonth()) {
|
|
720
|
+
onFocusedDateChange(clampDate(date, min, max));
|
|
721
|
+
}
|
|
722
|
+
}, onClick: () => {
|
|
723
|
+
onFocusedDateChange(clampDate(date, min, max));
|
|
724
|
+
onSelect?.(date);
|
|
725
|
+
}, children: monthCellNames[month] }, month));
|
|
726
|
+
}) }, row[0]))) })), view === 'years' && (jsx("div", { ref: yearGridRef, className: yearsGridClass, role: "listbox", "aria-label": isYearGranularity ? t.chooseYear : monthLabel, onKeyDown: handleYearListKeyDown, children: yearList.map(year => {
|
|
727
|
+
const isFocused = year === tabStopYear;
|
|
728
|
+
const isToday = year === todayYear;
|
|
729
|
+
// As navigation the focused year is the current one. As the
|
|
730
|
+
// selection surface the value's year is selected and focus roams.
|
|
731
|
+
const isSelected = isYearGranularity
|
|
732
|
+
? !!value && value.getFullYear() === year
|
|
733
|
+
: isFocused;
|
|
734
|
+
const disabled = disabledYears.has(year);
|
|
735
|
+
const cellCls = prefixedClassNames(classPrefix, 'dateinput-year-cell', {
|
|
736
|
+
'is-selected': isSelected,
|
|
737
|
+
'is-today': isToday,
|
|
738
|
+
'is-disabled': disabled,
|
|
739
|
+
});
|
|
740
|
+
return (jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: year === listTabStop ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
|
|
741
|
+
// As navigation, focusing a year by key or pointer only
|
|
742
|
+
// moves the tab stop: nothing is committed until a year is
|
|
743
|
+
// picked. As the selection surface it is reached by Tab
|
|
744
|
+
// while the focused year is disabled, or by pointer, and
|
|
745
|
+
// keys move on from here.
|
|
746
|
+
if (!isYearGranularity) {
|
|
747
|
+
setNavYear(year);
|
|
748
|
+
}
|
|
749
|
+
else if (year !== focusedYear) {
|
|
750
|
+
onFocusedDateChange(clampDate(makeDate(year), min, max));
|
|
751
|
+
}
|
|
752
|
+
}, onClick: () => isYearGranularity
|
|
753
|
+
? handleYearPick(year)
|
|
754
|
+
: handleYearSelect(year), children: year }, year));
|
|
755
|
+
}) })), rangeMode && (jsxs(Fragment, { children: [jsx("span", { id: rangeStartId, hidden: true, children: t.rangeStart }), jsx("span", { id: rangeEndId, hidden: true, children: t.rangeEnd }), jsx("span", { id: rangePreviewEndId, hidden: true, children: t.rangePreviewEnd }), jsx("div", { id: rangeSummaryId, "aria-live": "polite", style: visuallyHidden, children: rangeSummary })] }))] }));
|
|
756
|
+
};
|
|
757
|
+
Calendar.displayName = 'Calendar';
|
|
758
|
+
|
|
759
|
+
export { CALENDAR_FOCUSED_CELL, Calendar };
|
|
760
|
+
//# sourceMappingURL=Calendar.js.map
|