@astryxdesign/core 0.4.5 → 0.4.6-canary.cea9ecd
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/CHANGELOG.md +134 -0
- package/dist/Avatar/Avatar.d.ts +10 -5
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +139 -30
- package/dist/Avatar/AvatarStatusDot.d.ts +2 -1
- package/dist/Avatar/AvatarStatusDot.d.ts.map +1 -1
- package/dist/Avatar/AvatarStatusDot.js +30 -4
- package/dist/Avatar/AvatarStatusLabelContext.d.ts +39 -0
- package/dist/Avatar/AvatarStatusLabelContext.d.ts.map +1 -0
- package/dist/Avatar/AvatarStatusLabelContext.js +41 -0
- package/dist/AvatarGroup/AvatarGroup.d.ts +3 -1
- package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +15 -9
- package/dist/Blockquote/Blockquote.d.ts +4 -3
- package/dist/Blockquote/Blockquote.d.ts.map +1 -1
- package/dist/Blockquote/Blockquote.js +9 -10
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +21 -5
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +47 -34
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +7 -7
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +21 -12
- package/dist/Calendar/hooks/useCalendarNavigation.d.ts.map +1 -1
- package/dist/Calendar/hooks/useCalendarNavigation.js +8 -4
- package/dist/Calendar/styles.d.ts +2 -2
- package/dist/Calendar/styles.d.ts.map +1 -1
- package/dist/Calendar/styles.js +5 -5
- package/dist/Center/Center.d.ts +23 -1
- package/dist/Center/Center.d.ts.map +1 -1
- package/dist/Center/Center.js +12 -6
- package/dist/Chat/ChatComposer.d.ts.map +1 -1
- package/dist/Chat/ChatComposer.js +3 -3
- package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
- package/dist/Chat/ChatComposerDrawer.js +2 -2
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +2 -2
- package/dist/Chat/useChatDictation.d.ts +1 -1
- package/dist/Chat/useChatDictation.d.ts.map +1 -1
- package/dist/Chat/useSpeechRecognition.d.ts +1 -1
- package/dist/Chat/useSpeechRecognition.d.ts.map +1 -1
- package/dist/Chat/useSpeechRecognition.js +4 -3
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Chat/useTriggerMenu.js +2 -2
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +2 -2
- package/dist/Citation/Citation.d.ts.map +1 -1
- package/dist/Citation/Citation.js +5 -5
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +4 -4
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +1 -1
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +2 -2
- package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteItem.js +3 -3
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +6 -6
- package/dist/DateInput/DateInput.d.ts +31 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +90 -22
- package/dist/DateInput/MonthScroller.d.ts +35 -0
- package/dist/DateInput/MonthScroller.d.ts.map +1 -0
- package/dist/DateInput/MonthScroller.js +658 -0
- package/dist/DateInput/MonthYearWheels.d.ts +24 -0
- package/dist/DateInput/MonthYearWheels.d.ts.map +1 -0
- package/dist/DateInput/MonthYearWheels.js +114 -0
- package/dist/DateInput/TouchDateField.d.ts +10 -0
- package/dist/DateInput/TouchDateField.d.ts.map +1 -0
- package/dist/DateInput/TouchDateField.js +810 -0
- package/dist/DateInput/Wheel.d.ts +41 -0
- package/dist/DateInput/Wheel.d.ts.map +1 -0
- package/dist/DateInput/Wheel.js +315 -0
- package/dist/DateInput/monthGeometry.d.ts +65 -0
- package/dist/DateInput/monthGeometry.d.ts.map +1 -0
- package/dist/DateInput/monthGeometry.js +98 -0
- package/dist/DateInput/tokens.stylex.d.ts +63 -0
- package/dist/DateInput/tokens.stylex.d.ts.map +1 -0
- package/dist/DateInput/tokens.stylex.js +74 -0
- package/dist/DateInput/useOwnScrollGesture.d.ts +41 -0
- package/dist/DateInput/useOwnScrollGesture.d.ts.map +1 -0
- package/dist/DateInput/useOwnScrollGesture.js +202 -0
- package/dist/DateInput/usePointerDragScroll.d.ts +15 -0
- package/dist/DateInput/usePointerDragScroll.d.ts.map +1 -0
- package/dist/DateInput/usePointerDragScroll.js +216 -0
- package/dist/DateInput/useScrollSettle.d.ts +26 -0
- package/dist/DateInput/useScrollSettle.d.ts.map +1 -0
- package/dist/DateInput/useScrollSettle.js +149 -0
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +17 -14
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +17 -14
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +3 -3
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +2 -2
- package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.js +2 -2
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +2 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +2 -2
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +4 -4
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +6 -0
- package/dist/Field/inputStyles.stylex.d.ts +1 -1
- package/dist/Field/inputStyles.stylex.d.ts.map +1 -1
- package/dist/Field/inputStyles.stylex.js +5 -5
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +6 -6
- package/dist/HoverCard/HoverCard.d.ts +13 -3
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +2 -0
- package/dist/HoverCard/index.d.ts +1 -1
- package/dist/HoverCard/index.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.d.ts +17 -2
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +78 -9
- package/dist/InputGroup/InputGroup.js +1 -1
- package/dist/Item/Item.d.ts +9 -1
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +52 -14
- package/dist/Layer/index.d.ts +4 -2
- package/dist/Layer/index.d.ts.map +1 -1
- package/dist/Layer/index.js +2 -0
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +3 -2
- package/dist/Layer/useTouchTrigger.d.ts +113 -0
- package/dist/Layer/useTouchTrigger.d.ts.map +1 -0
- package/dist/Layer/useTouchTrigger.js +241 -0
- package/dist/Layout/container.stylex.d.ts +4 -2
- package/dist/Layout/container.stylex.d.ts.map +1 -1
- package/dist/Layout/container.stylex.js +19 -11
- package/dist/Layout/index.d.ts +1 -0
- package/dist/Layout/index.d.ts.map +1 -1
- package/dist/Layout/index.js +4 -0
- package/dist/Layout/padding.stylex.d.ts +315 -12
- package/dist/Layout/padding.stylex.d.ts.map +1 -1
- package/dist/Layout/padding.stylex.js +389 -12
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +5 -4
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +6 -6
- package/dist/MetadataList/MetadataList.d.ts.map +1 -1
- package/dist/MetadataList/MetadataList.js +1 -1
- package/dist/MobileNav/MobileNav.d.ts +1 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +24 -6
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +8 -8
- package/dist/NavItem/navItemStyles.stylex.d.ts +4 -4
- package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
- package/dist/NavItem/navItemStyles.stylex.js +4 -4
- package/dist/NavMenu/NavHeadingMenuItem.d.ts.map +1 -1
- package/dist/NavMenu/NavHeadingMenuItem.js +3 -3
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +36 -8
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +3 -3
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +2 -2
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +11 -12
- package/dist/PowerSearch/PowerSearchToken.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchToken.js +3 -1
- package/dist/PowerSearch/formatFilterValue.d.ts +11 -2
- package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
- package/dist/PowerSearch/formatFilterValue.js +23 -8
- package/dist/RadioList/RadioListItem.d.ts +2 -2
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +70 -40
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +7 -7
- package/dist/Section/Section.d.ts +36 -1
- package/dist/Section/Section.d.ts.map +1 -1
- package/dist/Section/Section.js +12 -2
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +7 -2
- package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
- package/dist/SelectableCard/SelectableCard.js +3 -3
- package/dist/Selector/Selector.d.ts +26 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +93 -20
- package/dist/Selector/SelectorOption.d.ts +14 -1
- package/dist/Selector/SelectorOption.d.ts.map +1 -1
- package/dist/Selector/SelectorOption.js +6 -0
- package/dist/Selector/SelectorRowLayoutContext.d.ts +8 -0
- package/dist/Selector/SelectorRowLayoutContext.d.ts.map +1 -0
- package/dist/Selector/SelectorRowLayoutContext.js +27 -0
- package/dist/Selector/types.d.ts +1 -0
- package/dist/Selector/types.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.js +4 -4
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +3 -3
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +7 -7
- package/dist/Stack/Stack.d.ts +23 -1
- package/dist/Stack/Stack.d.ts.map +1 -1
- package/dist/Stack/Stack.js +12 -6
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +2 -2
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +1 -1
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +3 -3
- package/dist/Table/TableRow.js +4 -4
- package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
- package/dist/Table/plugins/columnResize/useTableColumnResize.js +1 -1
- package/dist/Table/plugins/filtering/useTableFiltering.d.ts.map +1 -1
- package/dist/Table/plugins/filtering/useTableFiltering.js +2 -2
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +2 -2
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +2 -2
- package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortable.js +1 -1
- package/dist/Table/plugins/sortable/useTableSortableState.d.ts +5 -3
- package/dist/Table/plugins/sortable/useTableSortableState.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortableState.js +12 -9
- package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
- package/dist/Table/plugins/tree/useTableTreeData.js +3 -3
- package/dist/TextArea/TextArea.js +1 -1
- package/dist/TextInput/TextInput.js +1 -1
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +3 -3
- package/dist/TimeInput/TimeInput.js +2 -2
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +6 -4
- package/dist/Timestamp/formatInstant.d.ts +3 -2
- package/dist/Timestamp/formatInstant.d.ts.map +1 -1
- package/dist/Timestamp/formatInstant.js +8 -8
- package/dist/Timestamp/tooltipEntries.d.ts +2 -1
- package/dist/Timestamp/tooltipEntries.d.ts.map +1 -1
- package/dist/Timestamp/tooltipEntries.js +5 -3
- package/dist/Toast/Toast.d.ts +4 -3
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +10 -7
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +6 -6
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +1 -1
- package/dist/Tooltip/Tooltip.d.ts +16 -3
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +2 -0
- package/dist/Tooltip/index.d.ts +1 -1
- package/dist/Tooltip/index.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.d.ts +21 -2
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +71 -13
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +4 -4
- package/dist/TopNav/TopNavItem.d.ts.map +1 -1
- package/dist/TopNav/TopNavItem.js +3 -3
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +2 -2
- package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenuItem.js +2 -2
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +4 -4
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +7 -7
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +2 -2
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +1 -1
- package/dist/astryx.css +124 -62
- package/dist/hooks/containerReveal.stylex.d.ts.map +1 -1
- package/dist/hooks/containerReveal.stylex.js +18 -18
- package/dist/hooks/scrollbarGutter.d.ts +28 -0
- package/dist/hooks/scrollbarGutter.d.ts.map +1 -0
- package/dist/hooks/scrollbarGutter.js +103 -0
- package/dist/hooks/useAutoMediaMode.d.ts +31 -0
- package/dist/hooks/useAutoMediaMode.d.ts.map +1 -0
- package/dist/hooks/useAutoMediaMode.js +201 -0
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +1 -1
- package/dist/hooks/useListFocus.d.ts +5 -2
- package/dist/hooks/useListFocus.d.ts.map +1 -1
- package/dist/hooks/useListFocus.js +12 -6
- package/dist/hooks/useScrollLock.d.ts +4 -0
- package/dist/hooks/useScrollLock.d.ts.map +1 -1
- package/dist/hooks/useScrollLock.js +13 -1
- package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
- package/dist/i18n/InternationalizationContext.js +2 -0
- package/dist/i18n/index.d.ts +6 -0
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/useCollator.d.ts +12 -0
- package/dist/i18n/useCollator.d.ts.map +1 -0
- package/dist/i18n/useCollator.js +40 -0
- package/dist/i18n/useLocale.d.ts +3 -0
- package/dist/i18n/useLocale.d.ts.map +1 -0
- package/dist/i18n/useLocale.js +31 -0
- package/dist/theme/MediaTheme.d.ts +25 -5
- package/dist/theme/MediaTheme.d.ts.map +1 -1
- package/dist/theme/MediaTheme.js +31 -6
- package/dist/theme/defineTheme.d.ts +5 -0
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +5 -0
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +7 -0
- package/dist/theme/generateThemeRules.d.ts.map +1 -1
- package/dist/theme/generateThemeRules.js +66 -5
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/syntax/index.d.ts +6 -0
- package/dist/theme/syntax/index.d.ts.map +1 -1
- package/dist/theme/syntax/index.js +7 -2
- package/dist/utils/dateParser.d.ts +4 -3
- package/dist/utils/dateParser.d.ts.map +1 -1
- package/dist/utils/dateParser.js +8 -8
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/plainDate.d.ts +5 -2
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +20 -4
- package/locales/en.json +28 -0
- package/locales/pseudo.json +21 -0
- package/package.json +3 -3
- package/src/Avatar/Avatar.doc.mjs +10 -6
- package/src/Avatar/Avatar.test.tsx +219 -1
- package/src/Avatar/Avatar.tsx +179 -37
- package/src/Avatar/AvatarStatusDot.tsx +30 -4
- package/src/Avatar/AvatarStatusLabelContext.ts +47 -0
- package/src/AvatarGroup/AvatarGroup.doc.mjs +10 -6
- package/src/AvatarGroup/AvatarGroup.test.tsx +62 -0
- package/src/AvatarGroup/AvatarGroup.tsx +3 -1
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +31 -8
- package/src/Banner/Banner.doc.mjs +2 -0
- package/src/Blockquote/Blockquote.doc.mjs +30 -7
- package/src/Blockquote/Blockquote.test.tsx +34 -3
- package/src/Blockquote/Blockquote.tsx +11 -9
- package/src/BottomSheet/BottomSheet.doc.mjs +3 -3
- package/src/BottomSheet/BottomSheet.test.tsx +24 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +91 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +58 -3
- package/src/Breadcrumbs/BreadcrumbItem.tsx +36 -23
- package/src/Breadcrumbs/Breadcrumbs.doc.mjs +26 -2
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +31 -0
- package/src/Button/Button.tsx +12 -9
- package/src/Calendar/Calendar.tsx +15 -9
- package/src/Calendar/hooks/useCalendarNavigation.ts +6 -4
- package/src/Calendar/styles.ts +8 -5
- package/src/Center/Center.doc.mjs +48 -0
- package/src/Center/Center.test.tsx +158 -0
- package/src/Center/Center.tsx +50 -9
- package/src/Chat/ChatComposer.tsx +11 -5
- package/src/Chat/ChatComposerDrawer.tsx +4 -1
- package/src/Chat/ChatToolCalls.tsx +13 -4
- package/src/Chat/{useChatDictation.test.ts → useChatDictation.test.tsx} +29 -14
- package/src/Chat/useChatDictation.ts +1 -1
- package/src/Chat/useSpeechRecognition.ts +5 -3
- package/src/Chat/useTriggerMenu.tsx +4 -1
- package/src/CheckboxInput/CheckboxInput.tsx +5 -2
- package/src/Citation/Citation.test.tsx +6 -1
- package/src/Citation/Citation.tsx +11 -5
- package/src/ClickableCard/ClickableCard.tsx +7 -4
- package/src/CodeBlock/CodeBlock.tsx +4 -1
- package/src/Collapsible/Collapsible.tsx +5 -2
- package/src/CommandPalette/CommandPaletteItem.tsx +6 -3
- package/src/ComplexSelector/ComplexSelector.doc.mjs +10 -0
- package/src/ComplexSelector/ComplexSelector.tsx +14 -7
- package/src/DateInput/DateInput.test.tsx +17 -1
- package/src/DateInput/DateInput.tsx +93 -21
- package/src/DateInput/DateInputTouch.test.tsx +2582 -0
- package/src/DateInput/MonthScroller.tsx +878 -0
- package/src/DateInput/MonthYearWheels.tsx +147 -0
- package/src/DateInput/TouchDateField.tsx +1223 -0
- package/src/DateInput/Wheel.tsx +473 -0
- package/src/DateInput/monthGeometry.ts +118 -0
- package/src/DateInput/tokens.stylex.ts +99 -0
- package/src/DateInput/useOwnScrollGesture.ts +227 -0
- package/src/DateInput/usePointerDragScroll.ts +220 -0
- package/src/DateInput/useScrollSettle.ts +164 -0
- package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
- package/src/DateRangeInput/DateRangeInput.tsx +31 -13
- package/src/DateTimeInput/DateTimeInput.tsx +25 -9
- package/src/Dialog/Dialog.test.tsx +24 -0
- package/src/Dialog/Dialog.tsx +3 -0
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +5 -2
- package/src/DropdownMenu/DropdownMenuItem.tsx +5 -2
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +5 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +5 -2
- package/src/Field/Field.doc.mjs +2 -0
- package/src/Field/FieldLabel.tsx +9 -3
- package/src/Field/InputClearButton.test.tsx +110 -3
- package/src/Field/InputClearButton.tsx +38 -0
- package/src/Field/inputStyles.stylex.ts +17 -13
- package/src/FileInput/FileInput.tsx +15 -8
- package/src/FormLayout/FormLayout.doc.mjs +1 -1
- package/src/HoverCard/HoverCard.doc.mjs +18 -0
- package/src/HoverCard/HoverCard.test.tsx +189 -0
- package/src/HoverCard/HoverCard.tsx +22 -2
- package/src/HoverCard/index.ts +1 -0
- package/src/HoverCard/useHoverCard.doc.mjs +2 -0
- package/src/HoverCard/useHoverCard.tsx +103 -7
- package/src/InputGroup/InputGroup.tsx +1 -1
- package/src/Item/Item.doc.mjs +1 -0
- package/src/Item/Item.test.tsx +50 -0
- package/src/Item/Item.tsx +54 -6
- package/src/Layer/index.ts +10 -0
- package/src/Layer/useLayer.tsx +13 -2
- package/src/Layer/useTouchTrigger.test.ts +70 -0
- package/src/Layer/useTouchTrigger.ts +366 -0
- package/src/Layout/Layout.doc.mjs +18 -0
- package/src/Layout/container.stylex.ts +11 -3
- package/src/Layout/index.ts +4 -0
- package/src/Layout/overlayPaddingReset.test.tsx +222 -0
- package/src/Layout/padding.stylex.ts +192 -12
- package/src/Lightbox/Lightbox.tsx +11 -3
- package/src/Link/Link.tsx +9 -6
- package/src/MetadataList/MetadataList.tsx +4 -1
- package/src/MobileNav/MobileNav.tsx +61 -5
- package/src/MobileNav/MobileNavEntryAnimation.test.tsx +212 -0
- package/src/MobileNav/MobileNavScrollbarGutter.test.tsx +109 -0
- package/src/MultiSelector/MultiSelector.test.tsx +1 -1
- package/src/MultiSelector/MultiSelector.tsx +26 -10
- package/src/NavItem/navItemStyles.stylex.ts +7 -4
- package/src/NavMenu/NavHeadingMenuItem.tsx +6 -3
- package/src/NumberInput/NumberInput.doc.mjs +24 -0
- package/src/NumberInput/NumberInput.test.tsx +138 -0
- package/src/NumberInput/NumberInput.tsx +47 -8
- package/src/Outline/Outline.tsx +5 -2
- package/src/Pagination/Pagination.tsx +5 -2
- package/src/Popover/Popover.test.tsx +27 -1
- package/src/PowerSearch/PowerSearch.tsx +17 -8
- package/src/PowerSearch/PowerSearchToken.tsx +3 -0
- package/src/PowerSearch/formatFilterValue.test.ts +22 -2
- package/src/PowerSearch/formatFilterValue.ts +31 -6
- package/src/RadioList/RadioList.doc.mjs +1 -1
- package/src/RadioList/RadioList.test.tsx +170 -2
- package/src/RadioList/RadioListItem.tsx +88 -53
- package/src/Resizable/ResizeHandle.test.tsx +22 -0
- package/src/Resizable/ResizeHandle.tsx +25 -7
- package/src/Section/Section.doc.mjs +55 -0
- package/src/Section/Section.test.tsx +201 -4
- package/src/Section/Section.tsx +69 -0
- package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +15 -3
- package/src/SelectableCard/SelectableCard.tsx +6 -3
- package/src/Selector/Selector.doc.mjs +16 -2
- package/src/Selector/Selector.test.tsx +525 -1
- package/src/Selector/Selector.tsx +180 -25
- package/src/Selector/SelectorOption.doc.mjs +7 -0
- package/src/Selector/SelectorOption.tsx +21 -0
- package/src/Selector/SelectorRowLayoutContext.ts +32 -0
- package/src/Selector/types.ts +4 -4
- package/src/SideNav/SideNavHeading.tsx +14 -5
- package/src/SideNav/SideNavItem.tsx +9 -3
- package/src/Slider/Slider.tsx +25 -6
- package/src/Stack/Stack.doc.mjs +72 -0
- package/src/Stack/Stack.test.tsx +156 -0
- package/src/Stack/Stack.tsx +50 -8
- package/src/Switch/Switch.tsx +5 -2
- package/src/TabList/Tab.tsx +4 -1
- package/src/TabList/TabMenu.tsx +9 -3
- package/src/Table/TableRow.tsx +4 -4
- package/src/Table/plugins/columnResize/useTableColumnResize.tsx +6 -2
- package/src/Table/plugins/filtering/useTableFiltering.tsx +4 -1
- package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +10 -3
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +6 -3
- package/src/Table/plugins/sortable/useTableSortable.tsx +4 -1
- package/src/Table/plugins/sortable/useTableSortableState.test.tsx +34 -0
- package/src/Table/plugins/sortable/useTableSortableState.tsx +26 -23
- package/src/Table/plugins/tree/useTableTreeData.tsx +10 -4
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextArea/TextArea.tsx +1 -1
- package/src/TextInput/TextInput.tsx +1 -1
- package/src/Thumbnail/Thumbnail.tsx +9 -3
- package/src/TimeInput/TimeInput.tsx +1 -1
- package/src/Timestamp/Timestamp.test.tsx +27 -7
- package/src/Timestamp/Timestamp.tsx +8 -4
- package/src/Timestamp/formatInstant.ts +9 -7
- package/src/Timestamp/tooltipEntries.test.ts +11 -6
- package/src/Timestamp/tooltipEntries.ts +6 -2
- package/src/Toast/Toast.tsx +9 -7
- package/src/Token/Token.tsx +14 -5
- package/src/Tokenizer/Tokenizer.tsx +4 -1
- package/src/Tooltip/Tooltip.doc.mjs +18 -0
- package/src/Tooltip/Tooltip.test.tsx +254 -1
- package/src/Tooltip/Tooltip.tsx +22 -2
- package/src/Tooltip/index.ts +1 -0
- package/src/Tooltip/useTooltip.doc.mjs +2 -0
- package/src/Tooltip/useTooltip.tsx +99 -21
- package/src/TopNav/TopNavHeading.tsx +13 -4
- package/src/TopNav/TopNavItem.tsx +6 -3
- package/src/TopNav/TopNavMegaMenu.tsx +5 -2
- package/src/TopNav/TopNavMegaMenuItem.tsx +5 -2
- package/src/TopNav/TopNavMenu.tsx +10 -4
- package/src/TreeList/TreeListItem.tsx +22 -7
- package/src/Typeahead/BaseTypeahead.tsx +5 -2
- package/src/Typeahead/Typeahead.tsx +4 -1
- package/src/hooks/containerReveal.stylex.ts +54 -18
- package/src/hooks/scrollbarGutter.test.ts +167 -0
- package/src/hooks/scrollbarGutter.ts +121 -0
- package/src/hooks/useAutoMediaMode.test.ts +109 -0
- package/src/hooks/useAutoMediaMode.ts +233 -0
- package/src/hooks/useInputStatusIcon.tsx +4 -1
- package/src/hooks/useListFocus.doc.mjs +2 -2
- package/src/hooks/useListFocus.test.tsx +65 -3
- package/src/hooks/useListFocus.ts +15 -7
- package/src/hooks/useScrollLock.doc.mjs +4 -4
- package/src/hooks/useScrollLock.test.ts +73 -0
- package/src/hooks/useScrollLock.ts +14 -0
- package/src/i18n/InternationalizationContext.ts +2 -0
- package/src/i18n/__tests__/e2e-powersearch.test.tsx +37 -1
- package/src/i18n/__tests__/useCollator.test.tsx +115 -0
- package/src/i18n/__tests__/useLocale.test.tsx +52 -0
- package/src/i18n/index.ts +6 -0
- package/src/i18n/useCollator.doc.mjs +84 -0
- package/src/i18n/useCollator.ts +44 -0
- package/src/i18n/useLocale.doc.mjs +70 -0
- package/src/i18n/useLocale.ts +34 -0
- package/src/i18n/useTranslator.doc.mjs +1 -1
- package/src/reset.css +9 -2
- package/src/reset.test.ts +22 -0
- package/src/theme/MediaTheme.doc.mjs +23 -4
- package/src/theme/MediaTheme.dom.test.tsx +97 -0
- package/src/theme/MediaTheme.tsx +42 -7
- package/src/theme/defineTheme.test.ts +9 -3
- package/src/theme/defineTheme.ts +5 -0
- package/src/theme/derivedVarRegistry.test.ts +19 -5
- package/src/theme/derivedVarRegistry.ts +4 -0
- package/src/theme/generateThemeRules.test.ts +118 -0
- package/src/theme/generateThemeRules.ts +83 -5
- package/src/theme/index.ts +1 -1
- package/src/theme/syntax/index.ts +6 -2
- package/src/theme/syntax/serverSafeSyntax.test.ts +68 -0
- package/src/utils/dateParser.test.ts +20 -0
- package/src/utils/dateParser.ts +14 -7
- package/src/utils/index.ts +2 -0
- package/src/utils/plainDate.test.ts +6 -0
- package/src/utils/plainDate.ts +21 -4
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file MonthScroller.tsx
|
|
7
|
+
* @input Month bounds, selected value, constraints, week start
|
|
8
|
+
* @output Exports MonthScroller and MonthScrollerHandle
|
|
9
|
+
* @position Internal component; consumed by TouchDateField.tsx
|
|
10
|
+
*
|
|
11
|
+
* The continuous surface: months stacked vertically in one scroller, each pane
|
|
12
|
+
* exactly as tall as the scrollport and snapped to its start. Two consequences
|
|
13
|
+
* fall out of that single geometric choice — the picker never changes height,
|
|
14
|
+
* and there is no resting position that shows half of two months.
|
|
15
|
+
*
|
|
16
|
+
* Every pane is a fixed six-row grid, including the months that only need
|
|
17
|
+
* four or five, because a pane whose height depended on its contents would
|
|
18
|
+
* make snap offsets vary from month to month and the scroll position drift.
|
|
19
|
+
*
|
|
20
|
+
* The list is long (a century by default) but only a few panes are ever
|
|
21
|
+
* mounted: a spacer holds the full scroll height and the visible panes are
|
|
22
|
+
* positioned into it absolutely. Nothing is stitched or recycled during a
|
|
23
|
+
* scroll, so snap offsets are constant and momentum is never interrupted —
|
|
24
|
+
* which is the failure mode of the usual "append months at the edge" approach.
|
|
25
|
+
*
|
|
26
|
+
* SYNC: When modified, update:
|
|
27
|
+
* - /packages/core/src/DateInput/TouchDateField.tsx
|
|
28
|
+
* - /packages/core/src/DateInput/DateInput.doc.mjs
|
|
29
|
+
* - /packages/core/src/DateInput/DateInputTouch.test.tsx
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
useCallback,
|
|
34
|
+
useEffect,
|
|
35
|
+
useImperativeHandle,
|
|
36
|
+
useLayoutEffect,
|
|
37
|
+
useRef,
|
|
38
|
+
useState,
|
|
39
|
+
} from 'react';
|
|
40
|
+
import * as stylex from '@stylexjs/stylex';
|
|
41
|
+
import {useCalendarDays} from '../Calendar';
|
|
42
|
+
import {useDirection} from '../i18n';
|
|
43
|
+
import {
|
|
44
|
+
colorVars,
|
|
45
|
+
radiusVars,
|
|
46
|
+
fontWeightVars,
|
|
47
|
+
typeScaleVars,
|
|
48
|
+
durationVars,
|
|
49
|
+
borderVars,
|
|
50
|
+
} from '../theme/tokens.stylex';
|
|
51
|
+
import {focusOutlineStyles} from '../utils';
|
|
52
|
+
import {
|
|
53
|
+
type DayOfWeek,
|
|
54
|
+
type ISODateString,
|
|
55
|
+
type PlainDate,
|
|
56
|
+
plainDateAddDays,
|
|
57
|
+
plainDateDayOfWeek,
|
|
58
|
+
plainDateFormat,
|
|
59
|
+
plainDateIsEqual,
|
|
60
|
+
plainDateToISO,
|
|
61
|
+
DATE_FORMAT_MONTH_YEAR,
|
|
62
|
+
DATE_FORMAT_WITH_WEEKDAY,
|
|
63
|
+
} from '../utils';
|
|
64
|
+
import {dateInputTouchSizes, dateInputTouchGeometry} from './tokens.stylex';
|
|
65
|
+
import {useOwnScrollGesture} from './useOwnScrollGesture';
|
|
66
|
+
import {useScrollSettle} from './useScrollSettle';
|
|
67
|
+
import {
|
|
68
|
+
fromMonthIndex,
|
|
69
|
+
monthIndexOf,
|
|
70
|
+
paneWindow,
|
|
71
|
+
rowAtScrollOffset,
|
|
72
|
+
rowsIn,
|
|
73
|
+
scrollOffsetForRow,
|
|
74
|
+
} from './monthGeometry';
|
|
75
|
+
|
|
76
|
+
const DAY_SIZE = dateInputTouchSizes.daySize;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Panes mounted on each side of the visible one. Sized against a fast fling:
|
|
80
|
+
* the window widens once per animation frame, so it only fails if a single
|
|
81
|
+
* frame travels further than this — three months, about 800px at the default
|
|
82
|
+
* day size. Under `scroll-snap-type: mandatory`, landing on an unmounted pane
|
|
83
|
+
* would not merely show a gap, it would re-snap to the nearest mounted one.
|
|
84
|
+
*/
|
|
85
|
+
const OVERSCAN = 3;
|
|
86
|
+
|
|
87
|
+
const styles = stylex.create({
|
|
88
|
+
scroller: {
|
|
89
|
+
position: 'relative',
|
|
90
|
+
blockSize: dateInputTouchGeometry.paneBlockSize,
|
|
91
|
+
// Stated, not inherited from the reset: the pane size, the snap offsets
|
|
92
|
+
// and the virtualization all key off the measured box, so a consumer
|
|
93
|
+
// without reset.css (or with a stray box-sizing rule — the reset's is
|
|
94
|
+
// zero-specificity `:where`) must not be able to change what it means.
|
|
95
|
+
boxSizing: 'border-box',
|
|
96
|
+
overflowX: 'auto',
|
|
97
|
+
overflowY: 'hidden',
|
|
98
|
+
// The paging behaviour, now along the inline axis. Every snap area is a
|
|
99
|
+
// full pane, so there is exactly one resting position per month.
|
|
100
|
+
scrollSnapType: 'x mandatory',
|
|
101
|
+
// Keeps a fling inside the picker rather than handing it to the page.
|
|
102
|
+
overscrollBehavior: 'contain',
|
|
103
|
+
scrollbarWidth: 'none',
|
|
104
|
+
// pan-x, so the browser keeps horizontal pans here and hands VERTICAL
|
|
105
|
+
// ones straight to the sheet. That is why this scroller no longer claims
|
|
106
|
+
// the gesture the way the wheels do: with the axes separated there is
|
|
107
|
+
// nothing to fight over, and a downward drag on the calendar can go back
|
|
108
|
+
// to meaning swipe-to-dismiss.
|
|
109
|
+
touchAction: 'pan-x',
|
|
110
|
+
},
|
|
111
|
+
spacer: {
|
|
112
|
+
position: 'relative',
|
|
113
|
+
blockSize: '100%',
|
|
114
|
+
},
|
|
115
|
+
pane: {
|
|
116
|
+
position: 'absolute',
|
|
117
|
+
insetBlock: 0,
|
|
118
|
+
blockSize: dateInputTouchGeometry.paneBlockSize,
|
|
119
|
+
scrollSnapAlign: 'start',
|
|
120
|
+
// No `scroll-snap-stop: always`. It would cap every fling at one month,
|
|
121
|
+
// which is tidy but stops the surface being continuous — "three months
|
|
122
|
+
// ahead" should be one throw, not three flicks. Momentum carrying past
|
|
123
|
+
// several panes is why OVERSCAN is what it is.
|
|
124
|
+
display: 'grid',
|
|
125
|
+
gridTemplateColumns: 'repeat(7, 1fr)',
|
|
126
|
+
gridTemplateRows: dateInputTouchGeometry.paneRows,
|
|
127
|
+
},
|
|
128
|
+
row: {
|
|
129
|
+
display: 'contents',
|
|
130
|
+
},
|
|
131
|
+
cell: {
|
|
132
|
+
position: 'relative',
|
|
133
|
+
display: 'flex',
|
|
134
|
+
alignItems: 'center',
|
|
135
|
+
justifyContent: 'center',
|
|
136
|
+
},
|
|
137
|
+
day: {
|
|
138
|
+
display: 'flex',
|
|
139
|
+
alignItems: 'center',
|
|
140
|
+
justifyContent: 'center',
|
|
141
|
+
inlineSize: '100%',
|
|
142
|
+
blockSize: '100%',
|
|
143
|
+
minInlineSize: DAY_SIZE,
|
|
144
|
+
minBlockSize: DAY_SIZE,
|
|
145
|
+
padding: 0,
|
|
146
|
+
borderWidth: 0,
|
|
147
|
+
borderStyle: 'none',
|
|
148
|
+
backgroundColor: 'transparent',
|
|
149
|
+
color: colorVars['--color-text-primary'],
|
|
150
|
+
fontSize: typeScaleVars['--text-body-size'],
|
|
151
|
+
fontWeight: fontWeightVars['--font-weight-normal'],
|
|
152
|
+
cursor: {
|
|
153
|
+
default: 'pointer',
|
|
154
|
+
':is(:disabled,[aria-disabled="true"])': 'default',
|
|
155
|
+
},
|
|
156
|
+
// A tap on a 44px target should not also select the number inside it.
|
|
157
|
+
userSelect: 'none',
|
|
158
|
+
WebkitTapHighlightColor: 'transparent',
|
|
159
|
+
},
|
|
160
|
+
/**
|
|
161
|
+
* The circular hit/selection puck inside the tap target. Sizing the puck
|
|
162
|
+
* rather than the button keeps the touch target at the full cell while the
|
|
163
|
+
* visual stays a tidy circle.
|
|
164
|
+
*/
|
|
165
|
+
puck: {
|
|
166
|
+
display: 'flex',
|
|
167
|
+
alignItems: 'center',
|
|
168
|
+
justifyContent: 'center',
|
|
169
|
+
inlineSize: `calc(${DAY_SIZE} - 8px)`,
|
|
170
|
+
blockSize: `calc(${DAY_SIZE} - 8px)`,
|
|
171
|
+
borderRadius: radiusVars['--radius-full'],
|
|
172
|
+
borderWidth: borderVars['--border-width'],
|
|
173
|
+
borderStyle: 'solid',
|
|
174
|
+
borderColor: 'transparent',
|
|
175
|
+
backgroundColor: 'transparent',
|
|
176
|
+
transitionProperty: 'background-color, color, border-color',
|
|
177
|
+
transitionDuration: durationVars['--duration-fast'],
|
|
178
|
+
},
|
|
179
|
+
puckHoverable: {
|
|
180
|
+
// Two guards, for two different failure modes.
|
|
181
|
+
//
|
|
182
|
+
// `@media (hover: hover)`: on a touch screen a :hover tint sticks to the
|
|
183
|
+
// last tapped day until something else is tapped.
|
|
184
|
+
//
|
|
185
|
+
// `:where(:not(:disabled,[aria-disabled="true"]))`: a browser suppresses a
|
|
186
|
+
// disabled control's EVENTS, not its hover styling, so an unguarded
|
|
187
|
+
// :hover tints a day you cannot pick. The day's disabled state lives on
|
|
188
|
+
// the button (as `aria-disabled`, which keeps it focusable) and this puck
|
|
189
|
+
// is the span inside it, so today the working mechanism is the JS gate at
|
|
190
|
+
// the call site — this guard is what keeps the rule true if the styles
|
|
191
|
+
// ever move onto the button itself.
|
|
192
|
+
backgroundColor: {
|
|
193
|
+
default: 'transparent',
|
|
194
|
+
'@media (hover: hover)': {
|
|
195
|
+
default: 'transparent',
|
|
196
|
+
':hover:where(:not(:disabled,[aria-disabled="true"]))':
|
|
197
|
+
colorVars['--color-overlay-hover'],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
puckToday: {
|
|
202
|
+
borderColor: colorVars['--color-border-emphasized'],
|
|
203
|
+
fontWeight: fontWeightVars['--font-weight-semibold'],
|
|
204
|
+
},
|
|
205
|
+
puckSelected: {
|
|
206
|
+
backgroundColor: {
|
|
207
|
+
default: colorVars['--color-accent'],
|
|
208
|
+
// Holds the accent through hover, overriding puckHoverable's tint.
|
|
209
|
+
// Same guarded selector, so the two have equal specificity and
|
|
210
|
+
// application order decides — which is what puts this one on top.
|
|
211
|
+
':hover:where(:not(:disabled,[aria-disabled="true"]))':
|
|
212
|
+
colorVars['--color-accent'],
|
|
213
|
+
},
|
|
214
|
+
borderColor: colorVars['--color-accent'],
|
|
215
|
+
color: colorVars['--color-on-accent'],
|
|
216
|
+
fontWeight: fontWeightVars['--font-weight-semibold'],
|
|
217
|
+
},
|
|
218
|
+
/**
|
|
219
|
+
* A day belonging to the month either side of this pane. Muted the way
|
|
220
|
+
* Calendar mutes its own, and unselectable for the same reason it is
|
|
221
|
+
* there: it exists to show where the month ends, not to offer a date. A
|
|
222
|
+
* pane is one month, and tapping out of it would move the calendar under
|
|
223
|
+
* your thumb; the swipe or the arrow says "next month" without ambiguity.
|
|
224
|
+
*
|
|
225
|
+
* It needs no special handling for the roving tab order: `tabbableISO` is
|
|
226
|
+
* resolved per pane and only ever names a date in THAT pane's own month,
|
|
227
|
+
* so an outside cell can never match it. Which is the property that keeps
|
|
228
|
+
* a date from having two tab stops — the pane that owns it, and the
|
|
229
|
+
* neighbour that merely shows it.
|
|
230
|
+
*/
|
|
231
|
+
dayOutside: {
|
|
232
|
+
// Exactly what the desktop calendar does to its own spill days: the
|
|
233
|
+
// secondary text colour AND half opacity, which is what makes them read
|
|
234
|
+
// as background rather than as dimmer choices. (Calendar splits these
|
|
235
|
+
// across two style objects — `dayOutside` in its theme layer carries the
|
|
236
|
+
// colour, the one in its structural layer the opacity — so applying only
|
|
237
|
+
// the colour, as this did at first, was half the treatment.)
|
|
238
|
+
color: colorVars['--color-text-secondary'],
|
|
239
|
+
opacity: 0.5,
|
|
240
|
+
},
|
|
241
|
+
dayDisabled: {
|
|
242
|
+
// The desktop's disabled treatment, and deliberately not a disabled
|
|
243
|
+
// COLOUR: it fades whatever colour the day already had (0.3), so an
|
|
244
|
+
// in-month day and a spilled one stay a step apart while disabled, and
|
|
245
|
+
// both land LIGHTER than an enabled spill day rather than darker.
|
|
246
|
+
//
|
|
247
|
+
// A flat `--color-text-disabled` is what this had, and it put an enabled
|
|
248
|
+
// adjacent day at ~168 against a disabled in-month day at ~163 on white:
|
|
249
|
+
// five levels apart, which is no difference at all. That is the whole
|
|
250
|
+
// reason the two were hard to tell apart.
|
|
251
|
+
opacity: 0.3,
|
|
252
|
+
cursor: 'default',
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const dynamic = stylex.create({
|
|
257
|
+
spacer: (inlineSize: number) => ({
|
|
258
|
+
inlineSize: `${inlineSize}px`,
|
|
259
|
+
}),
|
|
260
|
+
// insetInlineStart, not left: under RTL the panes have to lay out from the
|
|
261
|
+
// right, and the scroll math mirrors with them (see scrollOffsetForRow).
|
|
262
|
+
pane: (insetInlineStart: number, inlineSize: number) => ({
|
|
263
|
+
insetInlineStart: `${insetInlineStart}px`,
|
|
264
|
+
inlineSize: `${inlineSize}px`,
|
|
265
|
+
}),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
export interface MonthScrollerHandle {
|
|
269
|
+
/** Bring a month to rest at the top of the scrollport. */
|
|
270
|
+
scrollToMonth: (monthIndex: number, behavior?: ScrollBehavior) => void;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface MonthScrollerProps {
|
|
274
|
+
/** Imperative handle for programmatic navigation (Today, the wheels). */
|
|
275
|
+
handleRef?: React.RefObject<MonthScrollerHandle | null>;
|
|
276
|
+
/** First reachable month, as a month index. */
|
|
277
|
+
minMonthIndex: number;
|
|
278
|
+
/** Last reachable month, as a month index. */
|
|
279
|
+
maxMonthIndex: number;
|
|
280
|
+
/** Month to rest on at mount. */
|
|
281
|
+
initialMonthIndex: number;
|
|
282
|
+
/** Fires as the scroller passes each month, settled or not. */
|
|
283
|
+
onVisibleMonthChange: (monthIndex: number) => void;
|
|
284
|
+
/** Currently selected date. */
|
|
285
|
+
selectedDate: PlainDate | null;
|
|
286
|
+
/** Today, for the `aria-current` marker. */
|
|
287
|
+
today: PlainDate;
|
|
288
|
+
/** Whether a date fails min/max or a custom constraint. */
|
|
289
|
+
isDateDisabled: (date: PlainDate) => boolean;
|
|
290
|
+
/** First column of the week. */
|
|
291
|
+
weekStartsOn: DayOfWeek;
|
|
292
|
+
/** Fired when a day is tapped. */
|
|
293
|
+
onSelect: (value: ISODateString) => void;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* A vertically continuous, snap-paged run of month grids.
|
|
298
|
+
*/
|
|
299
|
+
export function MonthScroller({
|
|
300
|
+
handleRef,
|
|
301
|
+
minMonthIndex,
|
|
302
|
+
maxMonthIndex,
|
|
303
|
+
initialMonthIndex,
|
|
304
|
+
onVisibleMonthChange,
|
|
305
|
+
selectedDate,
|
|
306
|
+
today,
|
|
307
|
+
isDateDisabled,
|
|
308
|
+
weekStartsOn,
|
|
309
|
+
onSelect,
|
|
310
|
+
}: MonthScrollerProps) {
|
|
311
|
+
const scrollerRef = useRef<HTMLDivElement | null>(null);
|
|
312
|
+
const rowCount = maxMonthIndex - minMonthIndex + 1;
|
|
313
|
+
|
|
314
|
+
// Pane WIDTH is read from layout, never assumed: it is whatever the CSS
|
|
315
|
+
// says the scrollport is, so the snap offsets and the virtualization follow
|
|
316
|
+
// a resize (rotation, a split-screen change) without re-deriving anything.
|
|
317
|
+
const [paneSize, setPaneSize] = useState(0);
|
|
318
|
+
// Which way the inline axis runs. Under RTL the panes lay out from the
|
|
319
|
+
// right and scrollLeft counts DOWN from 0, so every conversion between a
|
|
320
|
+
// row and a scroll offset has to know.
|
|
321
|
+
const isRTL = useDirection() === 'rtl';
|
|
322
|
+
const [centerRow, setCenterRow] = useState(initialMonthIndex - minMonthIndex);
|
|
323
|
+
// The visible row, mirrored so the scroll handler can tell "changed" from
|
|
324
|
+
// "same" without reading state, and notify the parent OUTSIDE a state
|
|
325
|
+
// updater — React runs updaters during render, where another component's
|
|
326
|
+
// setState is illegal.
|
|
327
|
+
const centerRowRef = useRef(centerRow);
|
|
328
|
+
// A row to scroll to once its pane is mounted; see scrollToMonth.
|
|
329
|
+
const pendingScrollRef = useRef<number | null>(null);
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The row a programmatic scroll is heading for, so its own scroll events
|
|
333
|
+
* are not reported back as if the user had gone there. Null once a finger
|
|
334
|
+
* touches the scroller, or once the steered scroll has arrived.
|
|
335
|
+
*/
|
|
336
|
+
const steeredRowRef = useRef<number | null>(null);
|
|
337
|
+
|
|
338
|
+
// Keyboard focus moves by date, not by cell index, so it crosses month
|
|
339
|
+
// boundaries the way a calendar should. Null until the user takes the grid
|
|
340
|
+
// with the keyboard.
|
|
341
|
+
const [focusedDate, setFocusedDate] = useState<PlainDate | null>(null);
|
|
342
|
+
const shouldRestoreFocusRef = useRef(false);
|
|
343
|
+
|
|
344
|
+
const onVisibleMonthChangeRef = useRef(onVisibleMonthChange);
|
|
345
|
+
onVisibleMonthChangeRef.current = onVisibleMonthChange;
|
|
346
|
+
|
|
347
|
+
useLayoutEffect(() => {
|
|
348
|
+
const scroller = scrollerRef.current;
|
|
349
|
+
if (scroller == null) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const measure = () => {
|
|
353
|
+
// Ignore zero: the picker can be mounted inside something not yet
|
|
354
|
+
// displayed (a closed BottomSheet), and a zero width would unmount
|
|
355
|
+
// every pane and lose the scroll position.
|
|
356
|
+
const measured = scroller.clientWidth;
|
|
357
|
+
if (measured > 0) {
|
|
358
|
+
// eslint-disable-next-line @eslint-react/set-state-in-effect -- pane width is a measurement; layout is the only place it exists
|
|
359
|
+
setPaneSize(measured);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
measure();
|
|
363
|
+
const observer = new ResizeObserver(measure);
|
|
364
|
+
observer.observe(scroller);
|
|
365
|
+
return () => observer.disconnect();
|
|
366
|
+
}, []);
|
|
367
|
+
|
|
368
|
+
// Land on the initial month before the first paint. Runs once the height is
|
|
369
|
+
// known; `hasPositioned` keeps a later resize from yanking the user back.
|
|
370
|
+
const hasPositionedRef = useRef(false);
|
|
371
|
+
useLayoutEffect(() => {
|
|
372
|
+
const scroller = scrollerRef.current;
|
|
373
|
+
if (scroller == null || paneSize === 0 || hasPositionedRef.current) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
hasPositionedRef.current = true;
|
|
377
|
+
scroller.scrollLeft = scrollOffsetForRow(
|
|
378
|
+
initialMonthIndex - minMonthIndex,
|
|
379
|
+
paneSize,
|
|
380
|
+
isRTL,
|
|
381
|
+
);
|
|
382
|
+
}, [paneSize, initialMonthIndex, minMonthIndex, isRTL]);
|
|
383
|
+
|
|
384
|
+
const scrollToMonth = useCallback(
|
|
385
|
+
(monthIndex: number, behavior: ScrollBehavior = 'smooth') => {
|
|
386
|
+
const scroller = scrollerRef.current;
|
|
387
|
+
if (scroller == null || paneSize === 0) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const row = Math.min(
|
|
391
|
+
rowCount - 1,
|
|
392
|
+
Math.max(0, monthIndex - minMonthIndex),
|
|
393
|
+
);
|
|
394
|
+
if (Math.abs(row - centerRowRef.current) > OVERSCAN) {
|
|
395
|
+
// Beyond the mounted window there is no pane at the target offset, and
|
|
396
|
+
// `scroll-snap-type: mandatory` will not leave the scroller resting
|
|
397
|
+
// where no snap area is — it re-snaps to the nearest mounted pane, so
|
|
398
|
+
// a jump to another year lands one month away instead. Mount the
|
|
399
|
+
// target first and scroll once it exists (see the layout effect
|
|
400
|
+
// below); a jump that far is never worth animating either.
|
|
401
|
+
pendingScrollRef.current = row;
|
|
402
|
+
centerRowRef.current = row;
|
|
403
|
+
setCenterRow(row);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
steeredRowRef.current = row;
|
|
407
|
+
scroller.scrollTo({
|
|
408
|
+
left: scrollOffsetForRow(row, paneSize, isRTL),
|
|
409
|
+
behavior,
|
|
410
|
+
});
|
|
411
|
+
},
|
|
412
|
+
[paneSize, minMonthIndex, rowCount, isRTL],
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// Deliberately un-keyed: it runs after every render, costs a null check, and
|
|
416
|
+
// must fire on whichever render finally mounts the pending row's pane.
|
|
417
|
+
useLayoutEffect(() => {
|
|
418
|
+
const row = pendingScrollRef.current;
|
|
419
|
+
const scroller = scrollerRef.current;
|
|
420
|
+
if (row == null || scroller == null || paneSize === 0) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
pendingScrollRef.current = null;
|
|
424
|
+
scroller.scrollTo({
|
|
425
|
+
left: scrollOffsetForRow(row, paneSize, isRTL),
|
|
426
|
+
behavior: 'auto',
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
useImperativeHandle(handleRef, () => ({scrollToMonth}), [scrollToMonth]);
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Put the scroller back on a pane boundary once the gesture is over.
|
|
434
|
+
*
|
|
435
|
+
* `scroll-snap-type: mandatory` is supposed to make this unnecessary, and
|
|
436
|
+
* on a static list it does. This list is virtualized: seven panes exist out
|
|
437
|
+
* of twelve hundred, and the panes ARE the snap areas — so every month the
|
|
438
|
+
* finger crosses mounts one and unmounts another, mid-fling.
|
|
439
|
+
*
|
|
440
|
+
* iOS scrolls off the main thread. It picks a landing place from the snap
|
|
441
|
+
* points it knows about at the time, and a React re-render that lands after
|
|
442
|
+
* that decision moves them; the scroller then comes to rest where no snap
|
|
443
|
+
* point exists any more, and nothing re-snaps it. That is the calendar
|
|
444
|
+
* sitting between two months with the weekday header still square — the
|
|
445
|
+
* grid is not skewed, the scrollport is simply parked a couple of columns
|
|
446
|
+
* into a pane. Chrome hides it by snapping again after the mutation.
|
|
447
|
+
*
|
|
448
|
+
* So the rest position is corrected here rather than trusted. It waits for
|
|
449
|
+
* a true settle — touch released AND quiet, since iOS below 26 has no
|
|
450
|
+
* `scrollend` and its momentum outlasts any naive timer (see
|
|
451
|
+
* useScrollSettle) — and only moves when the offset is genuinely off, so a
|
|
452
|
+
* scroller the browser snapped for itself is left alone.
|
|
453
|
+
*
|
|
454
|
+
* ## Why it re-checks that the scroller is still
|
|
455
|
+
*
|
|
456
|
+
* A quiet period is not proof of rest, and getting that wrong here does not
|
|
457
|
+
* merely fail to fix the bug — it REVERSES the user's swipe. iOS runs its
|
|
458
|
+
* own snap animation for ~150-300ms after the finger lifts, and the scroll
|
|
459
|
+
* events it fires during that animation arrive irregularly; a gap longer
|
|
460
|
+
* than the quiet period is routine in the slow tail. The settle then lands
|
|
461
|
+
* mid-animation, reads a scrollLeft still travelling toward April, rounds
|
|
462
|
+
* THAT to the nearest pane — which is still March, because the animation is
|
|
463
|
+
* not yet halfway — and drags the calendar back where it came from. Swipe
|
|
464
|
+
* forward, get pulled backward, which is what this looked like on a device.
|
|
465
|
+
*
|
|
466
|
+
* Two samples a frame apart settle it: if the offset moved, the scroller is
|
|
467
|
+
* still going somewhere and its destination is not ours to guess. Skipping
|
|
468
|
+
* costs nothing, because the animation's own scroll events re-arm the
|
|
469
|
+
* settle, and the last of them gets a quiet period that ends at true rest.
|
|
470
|
+
*
|
|
471
|
+
* The threshold is a subpixel rather than exact equality. A scroller at rest
|
|
472
|
+
* on a fractional-density viewport can report an offset that wobbles in the
|
|
473
|
+
* last decimal place, and exact equality would read that as travel and never
|
|
474
|
+
* correct at all — the failure mode being silent, which is the worst kind.
|
|
475
|
+
* A tail crawling slower than half a pixel a frame has effectively arrived,
|
|
476
|
+
* so treating it as arrived is right anyway.
|
|
477
|
+
*
|
|
478
|
+
* The correction is at most half a pane by construction, and its own scroll
|
|
479
|
+
* settles onto the boundary it just aimed at, so it cannot oscillate.
|
|
480
|
+
*/
|
|
481
|
+
const settleFrameRef = useRef<number | undefined>(undefined);
|
|
482
|
+
useEffect(
|
|
483
|
+
() => () => {
|
|
484
|
+
if (settleFrameRef.current != null) {
|
|
485
|
+
cancelAnimationFrame(settleFrameRef.current);
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
[],
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
useScrollSettle(scrollerRef, scroller => {
|
|
492
|
+
if (paneSize === 0) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const offsetBefore = scroller.scrollLeft;
|
|
496
|
+
if (settleFrameRef.current != null) {
|
|
497
|
+
cancelAnimationFrame(settleFrameRef.current);
|
|
498
|
+
}
|
|
499
|
+
settleFrameRef.current = requestAnimationFrame(() => {
|
|
500
|
+
settleFrameRef.current = undefined;
|
|
501
|
+
// Still travelling — including a snap animation iOS has not finished.
|
|
502
|
+
// Correcting toward the nearest pane from a position mid-flight would
|
|
503
|
+
// undo the swipe rather than complete it.
|
|
504
|
+
if (Math.abs(scroller.scrollLeft - offsetBefore) >= 0.5) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const row = rowAtScrollOffset(
|
|
508
|
+
scroller.scrollLeft,
|
|
509
|
+
paneSize,
|
|
510
|
+
rowCount,
|
|
511
|
+
isRTL,
|
|
512
|
+
);
|
|
513
|
+
const target = scrollOffsetForRow(row, paneSize, isRTL);
|
|
514
|
+
// Sub-pixel drift is the browser's own rounding, not a failed snap.
|
|
515
|
+
if (Math.abs(scroller.scrollLeft - target) < 1) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
scroller.scrollTo({left: target, behavior: 'smooth'});
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Claim horizontal gestures, leave vertical ones to the sheet.
|
|
523
|
+
//
|
|
524
|
+
// `touch-action: pan-x` alone is NOT enough: it governs what the browser
|
|
525
|
+
// pans natively, and the sheet's listener is JavaScript whose
|
|
526
|
+
// preventDefault() cancels the scroll regardless. Measured — a swipe 9° off
|
|
527
|
+
// horizontal had every touchmove cancelled by the sheet and the month never
|
|
528
|
+
// changed. The axis lock is biased toward horizontal, because a thumb arcs
|
|
529
|
+
// as it swipes; `onSwipe` then pages the diagonals the browser itself
|
|
530
|
+
// declines to pan, so no angle is left doing nothing. See
|
|
531
|
+
// useOwnScrollGesture.
|
|
532
|
+
useOwnScrollGesture(scrollerRef, 'inline', {
|
|
533
|
+
onSwipe: direction => {
|
|
534
|
+
const scroller = scrollerRef.current;
|
|
535
|
+
if (scroller == null || paneSize === 0) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
scroller.scrollBy({left: direction * paneSize, behavior: 'smooth'});
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// rAF-throttled: a touch scroll fires far more scroll events than frames,
|
|
543
|
+
// and all this does is move a label and widen a window.
|
|
544
|
+
const frameRef = useRef<number | undefined>(undefined);
|
|
545
|
+
useEffect(() => {
|
|
546
|
+
const scroller = scrollerRef.current;
|
|
547
|
+
// Not before the height is known. A listener attached while it was zero
|
|
548
|
+
// would close over that zero and report row 0 for any scroll — and the
|
|
549
|
+
// scroll event fired by the initial positioning arrives just late enough
|
|
550
|
+
// to be handled by that stale listener. Every pane then unmounts except
|
|
551
|
+
// rows 0-2, and `scroll-snap-type: mandatory` yanks the scroller to the
|
|
552
|
+
// nearest surviving snap area, a century away from where it just landed.
|
|
553
|
+
if (scroller == null || paneSize === 0) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
// A finger on the scroller ends any steering: from here the months it
|
|
557
|
+
// passes are the user's doing and every one of them is worth reporting.
|
|
558
|
+
const onTouchStart = () => {
|
|
559
|
+
steeredRowRef.current = null;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const onScroll = () => {
|
|
563
|
+
if (frameRef.current != null) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
frameRef.current = requestAnimationFrame(() => {
|
|
567
|
+
frameRef.current = undefined;
|
|
568
|
+
const row = rowAtScrollOffset(
|
|
569
|
+
scroller.scrollLeft,
|
|
570
|
+
paneSize,
|
|
571
|
+
rowCount,
|
|
572
|
+
isRTL,
|
|
573
|
+
);
|
|
574
|
+
if (row === centerRowRef.current) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
centerRowRef.current = row;
|
|
578
|
+
setCenterRow(row);
|
|
579
|
+
// Report nothing while a steer is in flight. `scrollToMonth` is only
|
|
580
|
+
// ever called by something that already knows the month — a wheel
|
|
581
|
+
// commit, a header arrow, the re-assert when the wheels close — so
|
|
582
|
+
// none of the scrolling it causes is news, including whatever the
|
|
583
|
+
// scroller passes through on the way.
|
|
584
|
+
//
|
|
585
|
+
// Reporting it is a feedback cycle, and worse on iOS. A wheel commit
|
|
586
|
+
// steers this scroller while it is hidden behind the wheels, and a
|
|
587
|
+
// hidden scroller does not reliably stay where it was put: iOS
|
|
588
|
+
// re-snaps it when the panel becomes visible again, firing scrolls at
|
|
589
|
+
// the exact moment reports start being trusted again. The month drifted
|
|
590
|
+
// on the way back to the calendar.
|
|
591
|
+
//
|
|
592
|
+
// Cleared on arrival, or by a touch below — once a finger is on the
|
|
593
|
+
// scroller the user owns it, and every month it passes is worth
|
|
594
|
+
// reporting.
|
|
595
|
+
if (steeredRowRef.current != null) {
|
|
596
|
+
if (steeredRowRef.current === row) {
|
|
597
|
+
steeredRowRef.current = null;
|
|
598
|
+
}
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
onVisibleMonthChangeRef.current(minMonthIndex + row);
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
scroller.addEventListener('scroll', onScroll, {passive: true});
|
|
605
|
+
scroller.addEventListener('touchstart', onTouchStart, {passive: true});
|
|
606
|
+
return () => {
|
|
607
|
+
scroller.removeEventListener('scroll', onScroll);
|
|
608
|
+
scroller.removeEventListener('touchstart', onTouchStart);
|
|
609
|
+
if (frameRef.current != null) {
|
|
610
|
+
cancelAnimationFrame(frameRef.current);
|
|
611
|
+
frameRef.current = undefined;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
}, [paneSize, rowCount, minMonthIndex, isRTL]);
|
|
615
|
+
|
|
616
|
+
// Move the keyboard focus by whole days and let the scroller follow. Paging
|
|
617
|
+
// by month is PageUp/PageDown; everything else is the APG grid vocabulary.
|
|
618
|
+
const moveFocus = useCallback(
|
|
619
|
+
(from: PlainDate, deltaDays: number) => {
|
|
620
|
+
const next = plainDateAddDays(from, deltaDays);
|
|
621
|
+
const nextIndex = monthIndexOf(next);
|
|
622
|
+
if (nextIndex < minMonthIndex || nextIndex > maxMonthIndex) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
shouldRestoreFocusRef.current = true;
|
|
626
|
+
setFocusedDate(next);
|
|
627
|
+
if (nextIndex !== monthIndexOf(from)) {
|
|
628
|
+
scrollToMonth(nextIndex, 'smooth');
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
[minMonthIndex, maxMonthIndex, scrollToMonth],
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
const handleKeyDown = useCallback(
|
|
635
|
+
(event: React.KeyboardEvent, date: PlainDate) => {
|
|
636
|
+
// Column within the displayed week, which is what Home/End move to —
|
|
637
|
+
// not the day of the month.
|
|
638
|
+
const column = (plainDateDayOfWeek(date) - weekStartsOn + 7) % 7;
|
|
639
|
+
switch (event.key) {
|
|
640
|
+
case 'ArrowLeft':
|
|
641
|
+
event.preventDefault();
|
|
642
|
+
moveFocus(date, -1);
|
|
643
|
+
break;
|
|
644
|
+
case 'ArrowRight':
|
|
645
|
+
event.preventDefault();
|
|
646
|
+
moveFocus(date, 1);
|
|
647
|
+
break;
|
|
648
|
+
case 'ArrowUp':
|
|
649
|
+
event.preventDefault();
|
|
650
|
+
moveFocus(date, -7);
|
|
651
|
+
break;
|
|
652
|
+
case 'ArrowDown':
|
|
653
|
+
event.preventDefault();
|
|
654
|
+
moveFocus(date, 7);
|
|
655
|
+
break;
|
|
656
|
+
case 'Home':
|
|
657
|
+
event.preventDefault();
|
|
658
|
+
moveFocus(date, -column);
|
|
659
|
+
break;
|
|
660
|
+
case 'End':
|
|
661
|
+
event.preventDefault();
|
|
662
|
+
moveFocus(date, 6 - column);
|
|
663
|
+
break;
|
|
664
|
+
default:
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
[moveFocus, weekStartsOn],
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
// The focused day may have been in an unmounted pane a frame ago; focus it
|
|
672
|
+
// once it exists, and only in response to a key press so a scroll never
|
|
673
|
+
// steals focus.
|
|
674
|
+
const focusedISO = focusedDate == null ? null : plainDateToISO(focusedDate);
|
|
675
|
+
useEffect(() => {
|
|
676
|
+
if (!shouldRestoreFocusRef.current || focusedISO == null) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const target = scrollerRef.current?.querySelector<HTMLElement>(
|
|
680
|
+
`[data-date="${focusedISO}"]`,
|
|
681
|
+
);
|
|
682
|
+
if (target != null) {
|
|
683
|
+
shouldRestoreFocusRef.current = false;
|
|
684
|
+
// preventScroll: the scroller is already snapping to this month; a
|
|
685
|
+
// browser scroll-into-view here would fight the snap and land between
|
|
686
|
+
// two panes.
|
|
687
|
+
target.focus({preventScroll: true});
|
|
688
|
+
}
|
|
689
|
+
}, [focusedISO]);
|
|
690
|
+
|
|
691
|
+
const visibleRows = paneWindow(centerRow, rowCount, OVERSCAN);
|
|
692
|
+
|
|
693
|
+
// A focus event fires for the programmatic focus above too; only take the
|
|
694
|
+
// date when it is actually a different day.
|
|
695
|
+
const handleDayFocus = useCallback((date: PlainDate) => {
|
|
696
|
+
setFocusedDate(previous =>
|
|
697
|
+
previous != null && plainDateIsEqual(previous, date) ? previous : date,
|
|
698
|
+
);
|
|
699
|
+
}, []);
|
|
700
|
+
|
|
701
|
+
return (
|
|
702
|
+
<div
|
|
703
|
+
ref={scrollerRef}
|
|
704
|
+
data-scroller="months"
|
|
705
|
+
{...stylex.props(styles.scroller)}>
|
|
706
|
+
<div
|
|
707
|
+
{...stylex.props(styles.spacer, dynamic.spacer(rowCount * paneSize))}>
|
|
708
|
+
{paneSize > 0 &&
|
|
709
|
+
rowsIn(visibleRows).map(row => (
|
|
710
|
+
<MonthPane
|
|
711
|
+
key={row}
|
|
712
|
+
monthIndex={minMonthIndex + row}
|
|
713
|
+
insetInlineStart={row * paneSize}
|
|
714
|
+
inlineSize={paneSize}
|
|
715
|
+
selectedDate={selectedDate}
|
|
716
|
+
focusedDate={focusedDate}
|
|
717
|
+
today={today}
|
|
718
|
+
isDateDisabled={isDateDisabled}
|
|
719
|
+
weekStartsOn={weekStartsOn}
|
|
720
|
+
onSelect={onSelect}
|
|
721
|
+
onDayKeyDown={handleKeyDown}
|
|
722
|
+
onDayFocus={handleDayFocus}
|
|
723
|
+
/>
|
|
724
|
+
))}
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
MonthScroller.displayName = 'MonthScroller';
|
|
731
|
+
|
|
732
|
+
interface MonthPaneProps {
|
|
733
|
+
monthIndex: number;
|
|
734
|
+
insetInlineStart: number;
|
|
735
|
+
inlineSize: number;
|
|
736
|
+
selectedDate: PlainDate | null;
|
|
737
|
+
focusedDate: PlainDate | null;
|
|
738
|
+
today: PlainDate;
|
|
739
|
+
isDateDisabled: (date: PlainDate) => boolean;
|
|
740
|
+
weekStartsOn: DayOfWeek;
|
|
741
|
+
onSelect: (value: ISODateString) => void;
|
|
742
|
+
onDayKeyDown: (event: React.KeyboardEvent, date: PlainDate) => void;
|
|
743
|
+
onDayFocus: (date: PlainDate) => void;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* One month, as a six-row grid.
|
|
748
|
+
*
|
|
749
|
+
* There is no `role="columnheader"` row in here: the weekday names are a
|
|
750
|
+
* single sticky row outside the scroller (they would otherwise scroll away),
|
|
751
|
+
* so each day instead carries its weekday in its accessible name.
|
|
752
|
+
*/
|
|
753
|
+
function MonthPane({
|
|
754
|
+
monthIndex,
|
|
755
|
+
insetInlineStart,
|
|
756
|
+
inlineSize,
|
|
757
|
+
selectedDate,
|
|
758
|
+
focusedDate,
|
|
759
|
+
today,
|
|
760
|
+
isDateDisabled,
|
|
761
|
+
weekStartsOn,
|
|
762
|
+
onSelect,
|
|
763
|
+
onDayKeyDown,
|
|
764
|
+
onDayFocus,
|
|
765
|
+
}: MonthPaneProps) {
|
|
766
|
+
const {year, month} = fromMonthIndex(monthIndex);
|
|
767
|
+
const {weeks} = useCalendarDays({
|
|
768
|
+
year,
|
|
769
|
+
month,
|
|
770
|
+
weekStartsOn,
|
|
771
|
+
// Always six rows: a variable grid would make pane heights differ, and
|
|
772
|
+
// with them every snap offset below this month.
|
|
773
|
+
hasVariableRowCount: false,
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
const monthLabel = plainDateFormat(
|
|
777
|
+
{year, month, day: 1},
|
|
778
|
+
DATE_FORMAT_MONTH_YEAR,
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
// Exactly one day per pane is tab-reachable, so Tab moves through the
|
|
782
|
+
// picker rather than through 42 buttons: the focused day if the keyboard
|
|
783
|
+
// owns one, else the selection, else the first of the month.
|
|
784
|
+
const tabbableISO = (() => {
|
|
785
|
+
if (
|
|
786
|
+
focusedDate != null &&
|
|
787
|
+
focusedDate.year === year &&
|
|
788
|
+
focusedDate.month === month
|
|
789
|
+
) {
|
|
790
|
+
return plainDateToISO(focusedDate);
|
|
791
|
+
}
|
|
792
|
+
if (
|
|
793
|
+
selectedDate != null &&
|
|
794
|
+
selectedDate.year === year &&
|
|
795
|
+
selectedDate.month === month
|
|
796
|
+
) {
|
|
797
|
+
return plainDateToISO(selectedDate);
|
|
798
|
+
}
|
|
799
|
+
return plainDateToISO({year, month, day: 1});
|
|
800
|
+
})();
|
|
801
|
+
|
|
802
|
+
return (
|
|
803
|
+
<div
|
|
804
|
+
role="grid"
|
|
805
|
+
aria-label={monthLabel}
|
|
806
|
+
data-month={monthLabel}
|
|
807
|
+
{...stylex.props(
|
|
808
|
+
styles.pane,
|
|
809
|
+
dynamic.pane(insetInlineStart, inlineSize),
|
|
810
|
+
)}>
|
|
811
|
+
{weeks.map(week => (
|
|
812
|
+
<div key={week[0].iso} role="row" {...stylex.props(styles.row)}>
|
|
813
|
+
{week.map(day => {
|
|
814
|
+
// A spilled day is context, not a choice. Calendar computes the
|
|
815
|
+
// same thing as `effectivelyDisabled: isDisabled || isOutside`,
|
|
816
|
+
// and guards today/selected on `!isOutside` beside it, so a date
|
|
817
|
+
// borrowed from a neighbouring month never carries a ring or a
|
|
818
|
+
// puck in the pane that is only showing it.
|
|
819
|
+
const isDisabled = day.isOutside || isDateDisabled(day.date);
|
|
820
|
+
const isSelected =
|
|
821
|
+
!day.isOutside &&
|
|
822
|
+
selectedDate != null &&
|
|
823
|
+
plainDateIsEqual(day.date, selectedDate);
|
|
824
|
+
const isToday = !day.isOutside && plainDateIsEqual(day.date, today);
|
|
825
|
+
|
|
826
|
+
return (
|
|
827
|
+
<div
|
|
828
|
+
key={day.iso}
|
|
829
|
+
role="gridcell"
|
|
830
|
+
aria-selected={isSelected || undefined}
|
|
831
|
+
{...stylex.props(styles.cell)}>
|
|
832
|
+
<button
|
|
833
|
+
type="button"
|
|
834
|
+
data-date={day.iso}
|
|
835
|
+
tabIndex={day.iso === tabbableISO ? 0 : -1}
|
|
836
|
+
aria-label={plainDateFormat(
|
|
837
|
+
day.date,
|
|
838
|
+
DATE_FORMAT_WITH_WEEKDAY,
|
|
839
|
+
)}
|
|
840
|
+
aria-disabled={isDisabled || undefined}
|
|
841
|
+
aria-current={isToday ? 'date' : undefined}
|
|
842
|
+
onClick={() => {
|
|
843
|
+
if (!isDisabled) {
|
|
844
|
+
onSelect(day.iso);
|
|
845
|
+
}
|
|
846
|
+
}}
|
|
847
|
+
onFocus={() => onDayFocus(day.date)}
|
|
848
|
+
onKeyDown={event => onDayKeyDown(event, day.date)}
|
|
849
|
+
{...stylex.props(
|
|
850
|
+
styles.day,
|
|
851
|
+
focusOutlineStyles.focusVisible,
|
|
852
|
+
// Outside FIRST, so disabled wins where both apply: a
|
|
853
|
+
// spilled day past the range is unselectable, and must
|
|
854
|
+
// not be painted more available than the in-month days
|
|
855
|
+
// beside it.
|
|
856
|
+
day.isOutside && styles.dayOutside,
|
|
857
|
+
isDisabled && styles.dayDisabled,
|
|
858
|
+
)}>
|
|
859
|
+
<span
|
|
860
|
+
{...stylex.props(
|
|
861
|
+
styles.puck,
|
|
862
|
+
!isDisabled && styles.puckHoverable,
|
|
863
|
+
isToday && styles.puckToday,
|
|
864
|
+
isSelected && styles.puckSelected,
|
|
865
|
+
)}>
|
|
866
|
+
{day.dayNumber}
|
|
867
|
+
</span>
|
|
868
|
+
</button>
|
|
869
|
+
</div>
|
|
870
|
+
);
|
|
871
|
+
})}
|
|
872
|
+
</div>
|
|
873
|
+
))}
|
|
874
|
+
</div>
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
MonthPane.displayName = 'MonthPane';
|