@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.
Files changed (538) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/dist/Avatar/Avatar.d.ts +10 -5
  3. package/dist/Avatar/Avatar.d.ts.map +1 -1
  4. package/dist/Avatar/Avatar.js +139 -30
  5. package/dist/Avatar/AvatarStatusDot.d.ts +2 -1
  6. package/dist/Avatar/AvatarStatusDot.d.ts.map +1 -1
  7. package/dist/Avatar/AvatarStatusDot.js +30 -4
  8. package/dist/Avatar/AvatarStatusLabelContext.d.ts +39 -0
  9. package/dist/Avatar/AvatarStatusLabelContext.d.ts.map +1 -0
  10. package/dist/Avatar/AvatarStatusLabelContext.js +41 -0
  11. package/dist/AvatarGroup/AvatarGroup.d.ts +3 -1
  12. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  13. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  14. package/dist/AvatarGroup/AvatarGroupOverflow.js +15 -9
  15. package/dist/Blockquote/Blockquote.d.ts +4 -3
  16. package/dist/Blockquote/Blockquote.d.ts.map +1 -1
  17. package/dist/Blockquote/Blockquote.js +9 -10
  18. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  19. package/dist/BottomSheet/BottomSheetPanel.js +21 -5
  20. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  21. package/dist/Breadcrumbs/BreadcrumbItem.js +47 -34
  22. package/dist/Button/Button.d.ts.map +1 -1
  23. package/dist/Button/Button.js +7 -7
  24. package/dist/Calendar/Calendar.d.ts.map +1 -1
  25. package/dist/Calendar/Calendar.js +21 -12
  26. package/dist/Calendar/hooks/useCalendarNavigation.d.ts.map +1 -1
  27. package/dist/Calendar/hooks/useCalendarNavigation.js +8 -4
  28. package/dist/Calendar/styles.d.ts +2 -2
  29. package/dist/Calendar/styles.d.ts.map +1 -1
  30. package/dist/Calendar/styles.js +5 -5
  31. package/dist/Center/Center.d.ts +23 -1
  32. package/dist/Center/Center.d.ts.map +1 -1
  33. package/dist/Center/Center.js +12 -6
  34. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  35. package/dist/Chat/ChatComposer.js +3 -3
  36. package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
  37. package/dist/Chat/ChatComposerDrawer.js +2 -2
  38. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  39. package/dist/Chat/ChatToolCalls.js +2 -2
  40. package/dist/Chat/useChatDictation.d.ts +1 -1
  41. package/dist/Chat/useChatDictation.d.ts.map +1 -1
  42. package/dist/Chat/useSpeechRecognition.d.ts +1 -1
  43. package/dist/Chat/useSpeechRecognition.d.ts.map +1 -1
  44. package/dist/Chat/useSpeechRecognition.js +4 -3
  45. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  46. package/dist/Chat/useTriggerMenu.js +2 -2
  47. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  48. package/dist/CheckboxInput/CheckboxInput.js +2 -2
  49. package/dist/Citation/Citation.d.ts.map +1 -1
  50. package/dist/Citation/Citation.js +5 -5
  51. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  52. package/dist/ClickableCard/ClickableCard.js +4 -4
  53. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  54. package/dist/CodeBlock/CodeBlock.js +1 -1
  55. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  56. package/dist/Collapsible/Collapsible.js +2 -2
  57. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  58. package/dist/CommandPalette/CommandPaletteItem.js +3 -3
  59. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  60. package/dist/ComplexSelector/ComplexSelector.js +6 -6
  61. package/dist/DateInput/DateInput.d.ts +31 -2
  62. package/dist/DateInput/DateInput.d.ts.map +1 -1
  63. package/dist/DateInput/DateInput.js +90 -22
  64. package/dist/DateInput/MonthScroller.d.ts +35 -0
  65. package/dist/DateInput/MonthScroller.d.ts.map +1 -0
  66. package/dist/DateInput/MonthScroller.js +658 -0
  67. package/dist/DateInput/MonthYearWheels.d.ts +24 -0
  68. package/dist/DateInput/MonthYearWheels.d.ts.map +1 -0
  69. package/dist/DateInput/MonthYearWheels.js +114 -0
  70. package/dist/DateInput/TouchDateField.d.ts +10 -0
  71. package/dist/DateInput/TouchDateField.d.ts.map +1 -0
  72. package/dist/DateInput/TouchDateField.js +810 -0
  73. package/dist/DateInput/Wheel.d.ts +41 -0
  74. package/dist/DateInput/Wheel.d.ts.map +1 -0
  75. package/dist/DateInput/Wheel.js +315 -0
  76. package/dist/DateInput/monthGeometry.d.ts +65 -0
  77. package/dist/DateInput/monthGeometry.d.ts.map +1 -0
  78. package/dist/DateInput/monthGeometry.js +98 -0
  79. package/dist/DateInput/tokens.stylex.d.ts +63 -0
  80. package/dist/DateInput/tokens.stylex.d.ts.map +1 -0
  81. package/dist/DateInput/tokens.stylex.js +74 -0
  82. package/dist/DateInput/useOwnScrollGesture.d.ts +41 -0
  83. package/dist/DateInput/useOwnScrollGesture.d.ts.map +1 -0
  84. package/dist/DateInput/useOwnScrollGesture.js +202 -0
  85. package/dist/DateInput/usePointerDragScroll.d.ts +15 -0
  86. package/dist/DateInput/usePointerDragScroll.d.ts.map +1 -0
  87. package/dist/DateInput/usePointerDragScroll.js +216 -0
  88. package/dist/DateInput/useScrollSettle.d.ts +26 -0
  89. package/dist/DateInput/useScrollSettle.d.ts.map +1 -0
  90. package/dist/DateInput/useScrollSettle.js +149 -0
  91. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  92. package/dist/DateRangeInput/DateRangeInput.js +17 -14
  93. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  94. package/dist/DateTimeInput/DateTimeInput.js +17 -14
  95. package/dist/Dialog/Dialog.d.ts.map +1 -1
  96. package/dist/Dialog/Dialog.js +3 -3
  97. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  98. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +2 -2
  99. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  100. package/dist/DropdownMenu/DropdownMenuItem.js +2 -2
  101. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
  102. package/dist/DropdownMenu/DropdownMenuRadioItem.js +2 -2
  103. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  104. package/dist/DropdownMenu/DropdownMenuSubMenu.js +2 -2
  105. package/dist/Field/FieldLabel.d.ts.map +1 -1
  106. package/dist/Field/FieldLabel.js +4 -4
  107. package/dist/Field/InputClearButton.d.ts.map +1 -1
  108. package/dist/Field/InputClearButton.js +6 -0
  109. package/dist/Field/inputStyles.stylex.d.ts +1 -1
  110. package/dist/Field/inputStyles.stylex.d.ts.map +1 -1
  111. package/dist/Field/inputStyles.stylex.js +5 -5
  112. package/dist/FileInput/FileInput.d.ts.map +1 -1
  113. package/dist/FileInput/FileInput.js +6 -6
  114. package/dist/HoverCard/HoverCard.d.ts +13 -3
  115. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  116. package/dist/HoverCard/HoverCard.js +2 -0
  117. package/dist/HoverCard/index.d.ts +1 -1
  118. package/dist/HoverCard/index.d.ts.map +1 -1
  119. package/dist/HoverCard/useHoverCard.d.ts +17 -2
  120. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  121. package/dist/HoverCard/useHoverCard.js +78 -9
  122. package/dist/InputGroup/InputGroup.js +1 -1
  123. package/dist/Item/Item.d.ts +9 -1
  124. package/dist/Item/Item.d.ts.map +1 -1
  125. package/dist/Item/Item.js +52 -14
  126. package/dist/Layer/index.d.ts +4 -2
  127. package/dist/Layer/index.d.ts.map +1 -1
  128. package/dist/Layer/index.js +2 -0
  129. package/dist/Layer/useLayer.d.ts.map +1 -1
  130. package/dist/Layer/useLayer.js +3 -2
  131. package/dist/Layer/useTouchTrigger.d.ts +113 -0
  132. package/dist/Layer/useTouchTrigger.d.ts.map +1 -0
  133. package/dist/Layer/useTouchTrigger.js +241 -0
  134. package/dist/Layout/container.stylex.d.ts +4 -2
  135. package/dist/Layout/container.stylex.d.ts.map +1 -1
  136. package/dist/Layout/container.stylex.js +19 -11
  137. package/dist/Layout/index.d.ts +1 -0
  138. package/dist/Layout/index.d.ts.map +1 -1
  139. package/dist/Layout/index.js +4 -0
  140. package/dist/Layout/padding.stylex.d.ts +315 -12
  141. package/dist/Layout/padding.stylex.d.ts.map +1 -1
  142. package/dist/Layout/padding.stylex.js +389 -12
  143. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  144. package/dist/Lightbox/Lightbox.js +5 -4
  145. package/dist/Link/Link.d.ts.map +1 -1
  146. package/dist/Link/Link.js +6 -6
  147. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  148. package/dist/MetadataList/MetadataList.js +1 -1
  149. package/dist/MobileNav/MobileNav.d.ts +1 -0
  150. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  151. package/dist/MobileNav/MobileNav.js +24 -6
  152. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  153. package/dist/MultiSelector/MultiSelector.js +8 -8
  154. package/dist/NavItem/navItemStyles.stylex.d.ts +4 -4
  155. package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
  156. package/dist/NavItem/navItemStyles.stylex.js +4 -4
  157. package/dist/NavMenu/NavHeadingMenuItem.d.ts.map +1 -1
  158. package/dist/NavMenu/NavHeadingMenuItem.js +3 -3
  159. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  160. package/dist/NumberInput/NumberInput.js +36 -8
  161. package/dist/Outline/Outline.d.ts.map +1 -1
  162. package/dist/Outline/Outline.js +3 -3
  163. package/dist/Pagination/Pagination.d.ts.map +1 -1
  164. package/dist/Pagination/Pagination.js +2 -2
  165. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  166. package/dist/PowerSearch/PowerSearch.js +11 -12
  167. package/dist/PowerSearch/PowerSearchToken.d.ts.map +1 -1
  168. package/dist/PowerSearch/PowerSearchToken.js +3 -1
  169. package/dist/PowerSearch/formatFilterValue.d.ts +11 -2
  170. package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
  171. package/dist/PowerSearch/formatFilterValue.js +23 -8
  172. package/dist/RadioList/RadioListItem.d.ts +2 -2
  173. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  174. package/dist/RadioList/RadioListItem.js +70 -40
  175. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  176. package/dist/Resizable/ResizeHandle.js +7 -7
  177. package/dist/Section/Section.d.ts +36 -1
  178. package/dist/Section/Section.d.ts.map +1 -1
  179. package/dist/Section/Section.js +12 -2
  180. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  181. package/dist/SegmentedControl/SegmentedControlItem.js +7 -2
  182. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  183. package/dist/SelectableCard/SelectableCard.js +3 -3
  184. package/dist/Selector/Selector.d.ts +26 -1
  185. package/dist/Selector/Selector.d.ts.map +1 -1
  186. package/dist/Selector/Selector.js +93 -20
  187. package/dist/Selector/SelectorOption.d.ts +14 -1
  188. package/dist/Selector/SelectorOption.d.ts.map +1 -1
  189. package/dist/Selector/SelectorOption.js +6 -0
  190. package/dist/Selector/SelectorRowLayoutContext.d.ts +8 -0
  191. package/dist/Selector/SelectorRowLayoutContext.d.ts.map +1 -0
  192. package/dist/Selector/SelectorRowLayoutContext.js +27 -0
  193. package/dist/Selector/types.d.ts +1 -0
  194. package/dist/Selector/types.d.ts.map +1 -1
  195. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  196. package/dist/SideNav/SideNavHeading.js +4 -4
  197. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  198. package/dist/SideNav/SideNavItem.js +3 -3
  199. package/dist/Slider/Slider.d.ts.map +1 -1
  200. package/dist/Slider/Slider.js +7 -7
  201. package/dist/Stack/Stack.d.ts +23 -1
  202. package/dist/Stack/Stack.d.ts.map +1 -1
  203. package/dist/Stack/Stack.js +12 -6
  204. package/dist/Switch/Switch.d.ts.map +1 -1
  205. package/dist/Switch/Switch.js +2 -2
  206. package/dist/TabList/Tab.d.ts.map +1 -1
  207. package/dist/TabList/Tab.js +1 -1
  208. package/dist/TabList/TabMenu.d.ts.map +1 -1
  209. package/dist/TabList/TabMenu.js +3 -3
  210. package/dist/Table/TableRow.js +4 -4
  211. package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
  212. package/dist/Table/plugins/columnResize/useTableColumnResize.js +1 -1
  213. package/dist/Table/plugins/filtering/useTableFiltering.d.ts.map +1 -1
  214. package/dist/Table/plugins/filtering/useTableFiltering.js +2 -2
  215. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  216. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +2 -2
  217. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  218. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +2 -2
  219. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  220. package/dist/Table/plugins/sortable/useTableSortable.js +1 -1
  221. package/dist/Table/plugins/sortable/useTableSortableState.d.ts +5 -3
  222. package/dist/Table/plugins/sortable/useTableSortableState.d.ts.map +1 -1
  223. package/dist/Table/plugins/sortable/useTableSortableState.js +12 -9
  224. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
  225. package/dist/Table/plugins/tree/useTableTreeData.js +3 -3
  226. package/dist/TextArea/TextArea.js +1 -1
  227. package/dist/TextInput/TextInput.js +1 -1
  228. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  229. package/dist/Thumbnail/Thumbnail.js +3 -3
  230. package/dist/TimeInput/TimeInput.js +2 -2
  231. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  232. package/dist/Timestamp/Timestamp.js +6 -4
  233. package/dist/Timestamp/formatInstant.d.ts +3 -2
  234. package/dist/Timestamp/formatInstant.d.ts.map +1 -1
  235. package/dist/Timestamp/formatInstant.js +8 -8
  236. package/dist/Timestamp/tooltipEntries.d.ts +2 -1
  237. package/dist/Timestamp/tooltipEntries.d.ts.map +1 -1
  238. package/dist/Timestamp/tooltipEntries.js +5 -3
  239. package/dist/Toast/Toast.d.ts +4 -3
  240. package/dist/Toast/Toast.d.ts.map +1 -1
  241. package/dist/Toast/Toast.js +10 -7
  242. package/dist/Token/Token.d.ts.map +1 -1
  243. package/dist/Token/Token.js +6 -6
  244. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  245. package/dist/Tokenizer/Tokenizer.js +1 -1
  246. package/dist/Tooltip/Tooltip.d.ts +16 -3
  247. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  248. package/dist/Tooltip/Tooltip.js +2 -0
  249. package/dist/Tooltip/index.d.ts +1 -1
  250. package/dist/Tooltip/index.d.ts.map +1 -1
  251. package/dist/Tooltip/useTooltip.d.ts +21 -2
  252. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  253. package/dist/Tooltip/useTooltip.js +71 -13
  254. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  255. package/dist/TopNav/TopNavHeading.js +4 -4
  256. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  257. package/dist/TopNav/TopNavItem.js +3 -3
  258. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  259. package/dist/TopNav/TopNavMegaMenu.js +2 -2
  260. package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
  261. package/dist/TopNav/TopNavMegaMenuItem.js +2 -2
  262. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  263. package/dist/TopNav/TopNavMenu.js +4 -4
  264. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  265. package/dist/TreeList/TreeListItem.js +7 -7
  266. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  267. package/dist/Typeahead/BaseTypeahead.js +2 -2
  268. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  269. package/dist/Typeahead/Typeahead.js +1 -1
  270. package/dist/astryx.css +124 -62
  271. package/dist/hooks/containerReveal.stylex.d.ts.map +1 -1
  272. package/dist/hooks/containerReveal.stylex.js +18 -18
  273. package/dist/hooks/scrollbarGutter.d.ts +28 -0
  274. package/dist/hooks/scrollbarGutter.d.ts.map +1 -0
  275. package/dist/hooks/scrollbarGutter.js +103 -0
  276. package/dist/hooks/useAutoMediaMode.d.ts +31 -0
  277. package/dist/hooks/useAutoMediaMode.d.ts.map +1 -0
  278. package/dist/hooks/useAutoMediaMode.js +201 -0
  279. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  280. package/dist/hooks/useInputStatusIcon.js +1 -1
  281. package/dist/hooks/useListFocus.d.ts +5 -2
  282. package/dist/hooks/useListFocus.d.ts.map +1 -1
  283. package/dist/hooks/useListFocus.js +12 -6
  284. package/dist/hooks/useScrollLock.d.ts +4 -0
  285. package/dist/hooks/useScrollLock.d.ts.map +1 -1
  286. package/dist/hooks/useScrollLock.js +13 -1
  287. package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
  288. package/dist/i18n/InternationalizationContext.js +2 -0
  289. package/dist/i18n/index.d.ts +6 -0
  290. package/dist/i18n/index.d.ts.map +1 -1
  291. package/dist/i18n/index.js +6 -0
  292. package/dist/i18n/useCollator.d.ts +12 -0
  293. package/dist/i18n/useCollator.d.ts.map +1 -0
  294. package/dist/i18n/useCollator.js +40 -0
  295. package/dist/i18n/useLocale.d.ts +3 -0
  296. package/dist/i18n/useLocale.d.ts.map +1 -0
  297. package/dist/i18n/useLocale.js +31 -0
  298. package/dist/theme/MediaTheme.d.ts +25 -5
  299. package/dist/theme/MediaTheme.d.ts.map +1 -1
  300. package/dist/theme/MediaTheme.js +31 -6
  301. package/dist/theme/defineTheme.d.ts +5 -0
  302. package/dist/theme/defineTheme.d.ts.map +1 -1
  303. package/dist/theme/defineTheme.js +5 -0
  304. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  305. package/dist/theme/derivedVarRegistry.js +7 -0
  306. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  307. package/dist/theme/generateThemeRules.js +66 -5
  308. package/dist/theme/index.d.ts +1 -1
  309. package/dist/theme/index.d.ts.map +1 -1
  310. package/dist/theme/syntax/index.d.ts +6 -0
  311. package/dist/theme/syntax/index.d.ts.map +1 -1
  312. package/dist/theme/syntax/index.js +7 -2
  313. package/dist/utils/dateParser.d.ts +4 -3
  314. package/dist/utils/dateParser.d.ts.map +1 -1
  315. package/dist/utils/dateParser.js +8 -8
  316. package/dist/utils/index.d.ts +1 -1
  317. package/dist/utils/index.d.ts.map +1 -1
  318. package/dist/utils/index.js +1 -1
  319. package/dist/utils/plainDate.d.ts +5 -2
  320. package/dist/utils/plainDate.d.ts.map +1 -1
  321. package/dist/utils/plainDate.js +20 -4
  322. package/locales/en.json +28 -0
  323. package/locales/pseudo.json +21 -0
  324. package/package.json +3 -3
  325. package/src/Avatar/Avatar.doc.mjs +10 -6
  326. package/src/Avatar/Avatar.test.tsx +219 -1
  327. package/src/Avatar/Avatar.tsx +179 -37
  328. package/src/Avatar/AvatarStatusDot.tsx +30 -4
  329. package/src/Avatar/AvatarStatusLabelContext.ts +47 -0
  330. package/src/AvatarGroup/AvatarGroup.doc.mjs +10 -6
  331. package/src/AvatarGroup/AvatarGroup.test.tsx +62 -0
  332. package/src/AvatarGroup/AvatarGroup.tsx +3 -1
  333. package/src/AvatarGroup/AvatarGroupOverflow.tsx +31 -8
  334. package/src/Banner/Banner.doc.mjs +2 -0
  335. package/src/Blockquote/Blockquote.doc.mjs +30 -7
  336. package/src/Blockquote/Blockquote.test.tsx +34 -3
  337. package/src/Blockquote/Blockquote.tsx +11 -9
  338. package/src/BottomSheet/BottomSheet.doc.mjs +3 -3
  339. package/src/BottomSheet/BottomSheet.test.tsx +24 -0
  340. package/src/BottomSheet/BottomSheetPanel.test.tsx +91 -0
  341. package/src/BottomSheet/BottomSheetPanel.tsx +58 -3
  342. package/src/Breadcrumbs/BreadcrumbItem.tsx +36 -23
  343. package/src/Breadcrumbs/Breadcrumbs.doc.mjs +26 -2
  344. package/src/Breadcrumbs/Breadcrumbs.test.tsx +31 -0
  345. package/src/Button/Button.tsx +12 -9
  346. package/src/Calendar/Calendar.tsx +15 -9
  347. package/src/Calendar/hooks/useCalendarNavigation.ts +6 -4
  348. package/src/Calendar/styles.ts +8 -5
  349. package/src/Center/Center.doc.mjs +48 -0
  350. package/src/Center/Center.test.tsx +158 -0
  351. package/src/Center/Center.tsx +50 -9
  352. package/src/Chat/ChatComposer.tsx +11 -5
  353. package/src/Chat/ChatComposerDrawer.tsx +4 -1
  354. package/src/Chat/ChatToolCalls.tsx +13 -4
  355. package/src/Chat/{useChatDictation.test.ts → useChatDictation.test.tsx} +29 -14
  356. package/src/Chat/useChatDictation.ts +1 -1
  357. package/src/Chat/useSpeechRecognition.ts +5 -3
  358. package/src/Chat/useTriggerMenu.tsx +4 -1
  359. package/src/CheckboxInput/CheckboxInput.tsx +5 -2
  360. package/src/Citation/Citation.test.tsx +6 -1
  361. package/src/Citation/Citation.tsx +11 -5
  362. package/src/ClickableCard/ClickableCard.tsx +7 -4
  363. package/src/CodeBlock/CodeBlock.tsx +4 -1
  364. package/src/Collapsible/Collapsible.tsx +5 -2
  365. package/src/CommandPalette/CommandPaletteItem.tsx +6 -3
  366. package/src/ComplexSelector/ComplexSelector.doc.mjs +10 -0
  367. package/src/ComplexSelector/ComplexSelector.tsx +14 -7
  368. package/src/DateInput/DateInput.test.tsx +17 -1
  369. package/src/DateInput/DateInput.tsx +93 -21
  370. package/src/DateInput/DateInputTouch.test.tsx +2582 -0
  371. package/src/DateInput/MonthScroller.tsx +878 -0
  372. package/src/DateInput/MonthYearWheels.tsx +147 -0
  373. package/src/DateInput/TouchDateField.tsx +1223 -0
  374. package/src/DateInput/Wheel.tsx +473 -0
  375. package/src/DateInput/monthGeometry.ts +118 -0
  376. package/src/DateInput/tokens.stylex.ts +99 -0
  377. package/src/DateInput/useOwnScrollGesture.ts +227 -0
  378. package/src/DateInput/usePointerDragScroll.ts +220 -0
  379. package/src/DateInput/useScrollSettle.ts +164 -0
  380. package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
  381. package/src/DateRangeInput/DateRangeInput.tsx +31 -13
  382. package/src/DateTimeInput/DateTimeInput.tsx +25 -9
  383. package/src/Dialog/Dialog.test.tsx +24 -0
  384. package/src/Dialog/Dialog.tsx +3 -0
  385. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +5 -2
  386. package/src/DropdownMenu/DropdownMenuItem.tsx +5 -2
  387. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +5 -2
  388. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +5 -2
  389. package/src/Field/Field.doc.mjs +2 -0
  390. package/src/Field/FieldLabel.tsx +9 -3
  391. package/src/Field/InputClearButton.test.tsx +110 -3
  392. package/src/Field/InputClearButton.tsx +38 -0
  393. package/src/Field/inputStyles.stylex.ts +17 -13
  394. package/src/FileInput/FileInput.tsx +15 -8
  395. package/src/FormLayout/FormLayout.doc.mjs +1 -1
  396. package/src/HoverCard/HoverCard.doc.mjs +18 -0
  397. package/src/HoverCard/HoverCard.test.tsx +189 -0
  398. package/src/HoverCard/HoverCard.tsx +22 -2
  399. package/src/HoverCard/index.ts +1 -0
  400. package/src/HoverCard/useHoverCard.doc.mjs +2 -0
  401. package/src/HoverCard/useHoverCard.tsx +103 -7
  402. package/src/InputGroup/InputGroup.tsx +1 -1
  403. package/src/Item/Item.doc.mjs +1 -0
  404. package/src/Item/Item.test.tsx +50 -0
  405. package/src/Item/Item.tsx +54 -6
  406. package/src/Layer/index.ts +10 -0
  407. package/src/Layer/useLayer.tsx +13 -2
  408. package/src/Layer/useTouchTrigger.test.ts +70 -0
  409. package/src/Layer/useTouchTrigger.ts +366 -0
  410. package/src/Layout/Layout.doc.mjs +18 -0
  411. package/src/Layout/container.stylex.ts +11 -3
  412. package/src/Layout/index.ts +4 -0
  413. package/src/Layout/overlayPaddingReset.test.tsx +222 -0
  414. package/src/Layout/padding.stylex.ts +192 -12
  415. package/src/Lightbox/Lightbox.tsx +11 -3
  416. package/src/Link/Link.tsx +9 -6
  417. package/src/MetadataList/MetadataList.tsx +4 -1
  418. package/src/MobileNav/MobileNav.tsx +61 -5
  419. package/src/MobileNav/MobileNavEntryAnimation.test.tsx +212 -0
  420. package/src/MobileNav/MobileNavScrollbarGutter.test.tsx +109 -0
  421. package/src/MultiSelector/MultiSelector.test.tsx +1 -1
  422. package/src/MultiSelector/MultiSelector.tsx +26 -10
  423. package/src/NavItem/navItemStyles.stylex.ts +7 -4
  424. package/src/NavMenu/NavHeadingMenuItem.tsx +6 -3
  425. package/src/NumberInput/NumberInput.doc.mjs +24 -0
  426. package/src/NumberInput/NumberInput.test.tsx +138 -0
  427. package/src/NumberInput/NumberInput.tsx +47 -8
  428. package/src/Outline/Outline.tsx +5 -2
  429. package/src/Pagination/Pagination.tsx +5 -2
  430. package/src/Popover/Popover.test.tsx +27 -1
  431. package/src/PowerSearch/PowerSearch.tsx +17 -8
  432. package/src/PowerSearch/PowerSearchToken.tsx +3 -0
  433. package/src/PowerSearch/formatFilterValue.test.ts +22 -2
  434. package/src/PowerSearch/formatFilterValue.ts +31 -6
  435. package/src/RadioList/RadioList.doc.mjs +1 -1
  436. package/src/RadioList/RadioList.test.tsx +170 -2
  437. package/src/RadioList/RadioListItem.tsx +88 -53
  438. package/src/Resizable/ResizeHandle.test.tsx +22 -0
  439. package/src/Resizable/ResizeHandle.tsx +25 -7
  440. package/src/Section/Section.doc.mjs +55 -0
  441. package/src/Section/Section.test.tsx +201 -4
  442. package/src/Section/Section.tsx +69 -0
  443. package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
  444. package/src/SegmentedControl/SegmentedControlItem.tsx +15 -3
  445. package/src/SelectableCard/SelectableCard.tsx +6 -3
  446. package/src/Selector/Selector.doc.mjs +16 -2
  447. package/src/Selector/Selector.test.tsx +525 -1
  448. package/src/Selector/Selector.tsx +180 -25
  449. package/src/Selector/SelectorOption.doc.mjs +7 -0
  450. package/src/Selector/SelectorOption.tsx +21 -0
  451. package/src/Selector/SelectorRowLayoutContext.ts +32 -0
  452. package/src/Selector/types.ts +4 -4
  453. package/src/SideNav/SideNavHeading.tsx +14 -5
  454. package/src/SideNav/SideNavItem.tsx +9 -3
  455. package/src/Slider/Slider.tsx +25 -6
  456. package/src/Stack/Stack.doc.mjs +72 -0
  457. package/src/Stack/Stack.test.tsx +156 -0
  458. package/src/Stack/Stack.tsx +50 -8
  459. package/src/Switch/Switch.tsx +5 -2
  460. package/src/TabList/Tab.tsx +4 -1
  461. package/src/TabList/TabMenu.tsx +9 -3
  462. package/src/Table/TableRow.tsx +4 -4
  463. package/src/Table/plugins/columnResize/useTableColumnResize.tsx +6 -2
  464. package/src/Table/plugins/filtering/useTableFiltering.tsx +4 -1
  465. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +10 -3
  466. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +6 -3
  467. package/src/Table/plugins/sortable/useTableSortable.tsx +4 -1
  468. package/src/Table/plugins/sortable/useTableSortableState.test.tsx +34 -0
  469. package/src/Table/plugins/sortable/useTableSortableState.tsx +26 -23
  470. package/src/Table/plugins/tree/useTableTreeData.tsx +10 -4
  471. package/src/TextArea/TextArea.doc.mjs +1 -1
  472. package/src/TextArea/TextArea.tsx +1 -1
  473. package/src/TextInput/TextInput.tsx +1 -1
  474. package/src/Thumbnail/Thumbnail.tsx +9 -3
  475. package/src/TimeInput/TimeInput.tsx +1 -1
  476. package/src/Timestamp/Timestamp.test.tsx +27 -7
  477. package/src/Timestamp/Timestamp.tsx +8 -4
  478. package/src/Timestamp/formatInstant.ts +9 -7
  479. package/src/Timestamp/tooltipEntries.test.ts +11 -6
  480. package/src/Timestamp/tooltipEntries.ts +6 -2
  481. package/src/Toast/Toast.tsx +9 -7
  482. package/src/Token/Token.tsx +14 -5
  483. package/src/Tokenizer/Tokenizer.tsx +4 -1
  484. package/src/Tooltip/Tooltip.doc.mjs +18 -0
  485. package/src/Tooltip/Tooltip.test.tsx +254 -1
  486. package/src/Tooltip/Tooltip.tsx +22 -2
  487. package/src/Tooltip/index.ts +1 -0
  488. package/src/Tooltip/useTooltip.doc.mjs +2 -0
  489. package/src/Tooltip/useTooltip.tsx +99 -21
  490. package/src/TopNav/TopNavHeading.tsx +13 -4
  491. package/src/TopNav/TopNavItem.tsx +6 -3
  492. package/src/TopNav/TopNavMegaMenu.tsx +5 -2
  493. package/src/TopNav/TopNavMegaMenuItem.tsx +5 -2
  494. package/src/TopNav/TopNavMenu.tsx +10 -4
  495. package/src/TreeList/TreeListItem.tsx +22 -7
  496. package/src/Typeahead/BaseTypeahead.tsx +5 -2
  497. package/src/Typeahead/Typeahead.tsx +4 -1
  498. package/src/hooks/containerReveal.stylex.ts +54 -18
  499. package/src/hooks/scrollbarGutter.test.ts +167 -0
  500. package/src/hooks/scrollbarGutter.ts +121 -0
  501. package/src/hooks/useAutoMediaMode.test.ts +109 -0
  502. package/src/hooks/useAutoMediaMode.ts +233 -0
  503. package/src/hooks/useInputStatusIcon.tsx +4 -1
  504. package/src/hooks/useListFocus.doc.mjs +2 -2
  505. package/src/hooks/useListFocus.test.tsx +65 -3
  506. package/src/hooks/useListFocus.ts +15 -7
  507. package/src/hooks/useScrollLock.doc.mjs +4 -4
  508. package/src/hooks/useScrollLock.test.ts +73 -0
  509. package/src/hooks/useScrollLock.ts +14 -0
  510. package/src/i18n/InternationalizationContext.ts +2 -0
  511. package/src/i18n/__tests__/e2e-powersearch.test.tsx +37 -1
  512. package/src/i18n/__tests__/useCollator.test.tsx +115 -0
  513. package/src/i18n/__tests__/useLocale.test.tsx +52 -0
  514. package/src/i18n/index.ts +6 -0
  515. package/src/i18n/useCollator.doc.mjs +84 -0
  516. package/src/i18n/useCollator.ts +44 -0
  517. package/src/i18n/useLocale.doc.mjs +70 -0
  518. package/src/i18n/useLocale.ts +34 -0
  519. package/src/i18n/useTranslator.doc.mjs +1 -1
  520. package/src/reset.css +9 -2
  521. package/src/reset.test.ts +22 -0
  522. package/src/theme/MediaTheme.doc.mjs +23 -4
  523. package/src/theme/MediaTheme.dom.test.tsx +97 -0
  524. package/src/theme/MediaTheme.tsx +42 -7
  525. package/src/theme/defineTheme.test.ts +9 -3
  526. package/src/theme/defineTheme.ts +5 -0
  527. package/src/theme/derivedVarRegistry.test.ts +19 -5
  528. package/src/theme/derivedVarRegistry.ts +4 -0
  529. package/src/theme/generateThemeRules.test.ts +118 -0
  530. package/src/theme/generateThemeRules.ts +83 -5
  531. package/src/theme/index.ts +1 -1
  532. package/src/theme/syntax/index.ts +6 -2
  533. package/src/theme/syntax/serverSafeSyntax.test.ts +68 -0
  534. package/src/utils/dateParser.test.ts +20 -0
  535. package/src/utils/dateParser.ts +14 -7
  536. package/src/utils/index.ts +2 -0
  537. package/src/utils/plainDate.test.ts +6 -0
  538. 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';