@navikt/ds-react 6.13.0 → 6.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/cjs/accordion/AccordionContext.d.ts +0 -1
  2. package/cjs/collapsible/Collapsible.context.d.ts +0 -1
  3. package/cjs/date/context/useDateInputContext.d.ts +0 -1
  4. package/cjs/date/datepicker/parts/HeadRow.d.ts +0 -1
  5. package/cjs/date/datepicker/parts/HeadRow.js +2 -3
  6. package/cjs/date/datepicker/parts/HeadRow.js.map +1 -1
  7. package/cjs/date/datepicker/parts/Row.d.ts +0 -1
  8. package/cjs/date/datepicker/parts/TableHead.d.ts +0 -1
  9. package/cjs/date/datepicker/parts/WeekNumber.d.ts +0 -1
  10. package/cjs/date/datepicker/types.d.ts +0 -1
  11. package/cjs/date/monthpicker/types.d.ts +0 -1
  12. package/cjs/date/utils/check-dates.js +2 -2
  13. package/cjs/date/utils/check-dates.js.map +1 -1
  14. package/cjs/date/utils/get-initial-year.js +1 -2
  15. package/cjs/date/utils/get-initial-year.js.map +1 -1
  16. package/cjs/date/utils/get-month-weeks.js +2 -3
  17. package/cjs/date/utils/get-month-weeks.js.map +1 -1
  18. package/cjs/date/utils/is-match.js +2 -3
  19. package/cjs/date/utils/is-match.js.map +1 -1
  20. package/cjs/dropdown/Menu/index.d.ts +1 -1
  21. package/cjs/dropdown/context.d.ts +0 -1
  22. package/cjs/expansion-card/context.d.ts +0 -1
  23. package/cjs/form/checkbox/useCheckbox.d.ts +3 -3
  24. package/cjs/form/combobox/Combobox.d.ts +1 -1
  25. package/cjs/form/combobox/Combobox.js.map +1 -1
  26. package/cjs/form/combobox/ComboboxProvider.js +3 -1
  27. package/cjs/form/combobox/ComboboxProvider.js.map +1 -1
  28. package/cjs/form/combobox/FilteredOptions/AddNewOption.d.ts +3 -0
  29. package/cjs/form/combobox/FilteredOptions/AddNewOption.js +41 -0
  30. package/cjs/form/combobox/FilteredOptions/AddNewOption.js.map +1 -0
  31. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +13 -57
  32. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  33. package/cjs/form/combobox/FilteredOptions/FilteredOptionsItem.d.ts +6 -0
  34. package/cjs/form/combobox/FilteredOptions/FilteredOptionsItem.js +43 -0
  35. package/cjs/form/combobox/FilteredOptions/FilteredOptionsItem.js.map +1 -0
  36. package/cjs/form/combobox/FilteredOptions/LoadingMessage.d.ts +3 -0
  37. package/cjs/form/combobox/FilteredOptions/LoadingMessage.js +16 -0
  38. package/cjs/form/combobox/FilteredOptions/LoadingMessage.js.map +1 -0
  39. package/cjs/form/combobox/FilteredOptions/MaxSelectedMessage.d.ts +3 -0
  40. package/cjs/form/combobox/FilteredOptions/MaxSelectedMessage.js +20 -0
  41. package/cjs/form/combobox/FilteredOptions/MaxSelectedMessage.js.map +1 -0
  42. package/cjs/form/combobox/FilteredOptions/NoSearchHitsMessage.d.ts +3 -0
  43. package/cjs/form/combobox/FilteredOptions/NoSearchHitsMessage.js +14 -0
  44. package/cjs/form/combobox/FilteredOptions/NoSearchHitsMessage.js.map +1 -0
  45. package/cjs/form/combobox/FilteredOptions/filtered-options-util.d.ts +1 -0
  46. package/cjs/form/combobox/FilteredOptions/filtered-options-util.js +6 -1
  47. package/cjs/form/combobox/FilteredOptions/filtered-options-util.js.map +1 -1
  48. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +5 -5
  49. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  50. package/cjs/form/combobox/Input/Input.context.d.ts +20 -7
  51. package/cjs/form/combobox/Input/Input.context.js +6 -12
  52. package/cjs/form/combobox/Input/Input.context.js.map +1 -1
  53. package/cjs/form/combobox/Input/Input.d.ts +2 -1
  54. package/cjs/form/combobox/Input/Input.js +45 -20
  55. package/cjs/form/combobox/Input/Input.js.map +1 -1
  56. package/cjs/form/combobox/Input/InputController.d.ts +1 -1
  57. package/cjs/form/combobox/Input/InputController.js +1 -1
  58. package/cjs/form/combobox/Input/InputController.js.map +1 -1
  59. package/cjs/form/combobox/types.d.ts +8 -4
  60. package/cjs/form/fieldset/context.d.ts +0 -1
  61. package/cjs/form/fieldset/useFieldset.d.ts +1 -1
  62. package/cjs/form/file-upload/FileUpload.context.d.ts +0 -1
  63. package/cjs/form/file-upload/parts/dropzone/dropzone.types.d.ts +0 -1
  64. package/cjs/form/file-upload/parts/item/utils/format-file-size.js +1 -2
  65. package/cjs/form/file-upload/parts/item/utils/format-file-size.js.map +1 -1
  66. package/cjs/form/file-upload/useFileUpload.d.ts +1 -2
  67. package/cjs/form/file-upload/utils/is-accepted-file-type.js +1 -2
  68. package/cjs/form/file-upload/utils/is-accepted-file-type.js.map +1 -1
  69. package/cjs/form/file-upload/utils/is-accepted-size.js +1 -2
  70. package/cjs/form/file-upload/utils/is-accepted-size.js.map +1 -1
  71. package/cjs/form/radio/useRadio.d.ts +3 -3
  72. package/cjs/form/search/context.d.ts +0 -1
  73. package/cjs/layout/base/PrimitiveAsChildProps.d.ts +0 -1
  74. package/cjs/layout/grid/HGrid.js +4 -1
  75. package/cjs/layout/grid/HGrid.js.map +1 -1
  76. package/cjs/layout/stack/Stack.js +7 -2
  77. package/cjs/layout/stack/Stack.js.map +1 -1
  78. package/cjs/layout/utilities/css.js +2 -3
  79. package/cjs/layout/utilities/css.js.map +1 -1
  80. package/cjs/list/context.d.ts +0 -1
  81. package/cjs/list/types.d.ts +0 -1
  82. package/cjs/modal/ModalUtils.js +3 -3
  83. package/cjs/modal/ModalUtils.js.map +1 -1
  84. package/cjs/modal/types.d.ts +0 -1
  85. package/cjs/overlays/dismissablelayer/DismissableLayer.d.ts +1 -1
  86. package/cjs/overlays/dismissablelayer/util/dispatchCustomEvent.js +2 -2
  87. package/cjs/overlays/dismissablelayer/util/dispatchCustomEvent.js.map +1 -1
  88. package/cjs/overlays/dismissablelayer/util/useEscapeKeydown.js +1 -2
  89. package/cjs/overlays/dismissablelayer/util/useEscapeKeydown.js.map +1 -1
  90. package/cjs/overlays/dismissablelayer/util/useFocusOutside.js +1 -2
  91. package/cjs/overlays/dismissablelayer/util/useFocusOutside.js.map +1 -1
  92. package/cjs/overlays/dismissablelayer/util/usePointerDownOutside.js +1 -2
  93. package/cjs/overlays/dismissablelayer/util/usePointerDownOutside.js.map +1 -1
  94. package/cjs/overlays/floating/Floating.utils.js +2 -3
  95. package/cjs/overlays/floating/Floating.utils.js.map +1 -1
  96. package/cjs/overlays/floating-menu/Menu.d.ts +106 -0
  97. package/cjs/overlays/floating-menu/Menu.js +593 -0
  98. package/cjs/overlays/floating-menu/Menu.js.map +1 -0
  99. package/cjs/overlays/floating-menu/parts/FocusScope.d.ts +22 -0
  100. package/cjs/overlays/floating-menu/parts/FocusScope.js +89 -0
  101. package/cjs/overlays/floating-menu/parts/FocusScope.js.map +1 -0
  102. package/cjs/overlays/floating-menu/parts/RovingFocus.d.ts +9 -0
  103. package/cjs/overlays/floating-menu/parts/RovingFocus.js +112 -0
  104. package/cjs/overlays/floating-menu/parts/RovingFocus.js.map +1 -0
  105. package/cjs/overlays/floating-menu/parts/SlottedDivElement.d.ts +7 -0
  106. package/cjs/overlays/floating-menu/parts/SlottedDivElement.js +46 -0
  107. package/cjs/overlays/floating-menu/parts/SlottedDivElement.js.map +1 -0
  108. package/cjs/slot/merge-props.js +1 -2
  109. package/cjs/slot/merge-props.js.map +1 -1
  110. package/cjs/stepper/context.d.ts +0 -1
  111. package/cjs/table/context.d.ts +0 -1
  112. package/cjs/tabs/Tabs.context.d.ts +1 -2
  113. package/cjs/tabs/parts/tab/useTab.d.ts +1 -2
  114. package/cjs/tabs/parts/tab/useTab.js +1 -2
  115. package/cjs/tabs/parts/tab/useTab.js.map +1 -1
  116. package/cjs/tabs/parts/tablist/useScrollButtons.d.ts +0 -1
  117. package/cjs/tabs/parts/tablist/useScrollButtons.js +1 -2
  118. package/cjs/tabs/parts/tablist/useScrollButtons.js.map +1 -1
  119. package/cjs/tabs/parts/tablist/useTabList.js +3 -3
  120. package/cjs/tabs/parts/tablist/useTabList.js.map +1 -1
  121. package/cjs/tabs/parts/tabpanel/useTabPanel.js +1 -2
  122. package/cjs/tabs/parts/tabpanel/useTabPanel.js.map +1 -1
  123. package/cjs/tabs/useTabs.d.ts +0 -1
  124. package/cjs/tabs/useTabs.js +1 -2
  125. package/cjs/tabs/useTabs.js.map +1 -1
  126. package/cjs/timeline/hooks/usePeriodContext.d.ts +0 -1
  127. package/cjs/timeline/hooks/useRowContext.d.ts +0 -1
  128. package/cjs/timeline/hooks/useTimelineContext.d.ts +0 -1
  129. package/cjs/timeline/period/types.d.ts +0 -1
  130. package/cjs/timeline/zoom/index.d.ts +1 -1
  131. package/cjs/toggle-group/ToggleGroup.context.d.ts +1 -2
  132. package/cjs/toggle-group/parts/useToggleItem.d.ts +1 -2
  133. package/cjs/toggle-group/parts/useToggleItem.js +3 -3
  134. package/cjs/toggle-group/parts/useToggleItem.js.map +1 -1
  135. package/cjs/toggle-group/useToggleGroup.d.ts +0 -1
  136. package/cjs/toggle-group/useToggleGroup.js +1 -2
  137. package/cjs/toggle-group/useToggleGroup.js.map +1 -1
  138. package/cjs/util/composeEventHandlers.d.ts +1 -2
  139. package/cjs/util/composeEventHandlers.js +1 -2
  140. package/cjs/util/composeEventHandlers.js.map +1 -1
  141. package/cjs/util/copy.js +1 -1
  142. package/cjs/util/copy.js.map +1 -1
  143. package/cjs/util/create-context.js +1 -2
  144. package/cjs/util/create-context.js.map +1 -1
  145. package/cjs/util/debounce.js +1 -1
  146. package/cjs/util/debounce.js.map +1 -1
  147. package/cjs/util/hooks/descendants/useDescendant.d.ts +1 -1
  148. package/cjs/util/hooks/descendants/useDescendant.js +1 -2
  149. package/cjs/util/hooks/descendants/useDescendant.js.map +1 -1
  150. package/cjs/util/hooks/descendants/utils.js +4 -4
  151. package/cjs/util/hooks/descendants/utils.js.map +1 -1
  152. package/cjs/util/hooks/useCallbackRef.d.ts +0 -1
  153. package/cjs/util/hooks/useCallbackRef.js +1 -2
  154. package/cjs/util/hooks/useCallbackRef.js.map +1 -1
  155. package/cjs/util/hooks/useControllableState.js +1 -2
  156. package/cjs/util/hooks/useControllableState.js.map +1 -1
  157. package/cjs/util/hooks/useId.js +1 -2
  158. package/cjs/util/hooks/useId.js.map +1 -1
  159. package/cjs/util/hooks/useMergeRefs.d.ts +1 -1
  160. package/cjs/util/hooks/useMergeRefs.js +2 -3
  161. package/cjs/util/hooks/useMergeRefs.js.map +1 -1
  162. package/cjs/util/i18n/get.js +1 -2
  163. package/cjs/util/i18n/get.js.map +1 -1
  164. package/cjs/util/i18n/i18n.context.d.ts +0 -1
  165. package/cjs/util/i18n/i18n.context.js +2 -2
  166. package/cjs/util/i18n/i18n.context.js.map +1 -1
  167. package/cjs/util/i18n/merge.js +1 -2
  168. package/cjs/util/i18n/merge.js.map +1 -1
  169. package/cjs/util/omit.js +1 -2
  170. package/cjs/util/omit.js.map +1 -1
  171. package/cjs/util/types/AsChild.d.ts +0 -1
  172. package/cjs/util/types/AsChildProps.d.ts +0 -1
  173. package/cjs/util/virtualfocus/Context.d.ts +43 -0
  174. package/cjs/util/virtualfocus/Context.js +9 -0
  175. package/cjs/util/virtualfocus/Context.js.map +1 -0
  176. package/cjs/util/virtualfocus/SlottedDivElement.d.ts +7 -0
  177. package/cjs/util/virtualfocus/SlottedDivElement.js +46 -0
  178. package/cjs/util/virtualfocus/SlottedDivElement.js.map +1 -0
  179. package/cjs/util/virtualfocus/VirtualFocus.d.ts +62 -0
  180. package/cjs/util/virtualfocus/VirtualFocus.js +90 -0
  181. package/cjs/util/virtualfocus/VirtualFocus.js.map +1 -0
  182. package/cjs/util/virtualfocus/parts/VirtualFocusAnchor.d.ts +33 -0
  183. package/cjs/util/virtualfocus/parts/VirtualFocusAnchor.js +80 -0
  184. package/cjs/util/virtualfocus/parts/VirtualFocusAnchor.js.map +1 -0
  185. package/cjs/util/virtualfocus/parts/VirtualFocusContent.d.ts +4 -0
  186. package/cjs/util/virtualfocus/parts/VirtualFocusContent.js +45 -0
  187. package/cjs/util/virtualfocus/parts/VirtualFocusContent.js.map +1 -0
  188. package/cjs/util/virtualfocus/parts/VirtualFocusItem.d.ts +18 -0
  189. package/cjs/util/virtualfocus/parts/VirtualFocusItem.js +64 -0
  190. package/cjs/util/virtualfocus/parts/VirtualFocusItem.js.map +1 -0
  191. package/esm/accordion/AccordionContext.d.ts +0 -1
  192. package/esm/collapsible/Collapsible.context.d.ts +0 -1
  193. package/esm/date/context/useDateInputContext.d.ts +0 -1
  194. package/esm/date/datepicker/parts/HeadRow.d.ts +0 -1
  195. package/esm/date/datepicker/parts/Row.d.ts +0 -1
  196. package/esm/date/datepicker/parts/TableHead.d.ts +0 -1
  197. package/esm/date/datepicker/parts/WeekNumber.d.ts +0 -1
  198. package/esm/date/datepicker/types.d.ts +0 -1
  199. package/esm/date/monthpicker/types.d.ts +0 -1
  200. package/esm/dropdown/Menu/index.d.ts +1 -1
  201. package/esm/dropdown/context.d.ts +0 -1
  202. package/esm/expansion-card/context.d.ts +0 -1
  203. package/esm/form/checkbox/useCheckbox.d.ts +3 -3
  204. package/esm/form/combobox/Combobox.d.ts +1 -1
  205. package/esm/form/combobox/Combobox.js.map +1 -1
  206. package/esm/form/combobox/ComboboxProvider.js +3 -1
  207. package/esm/form/combobox/ComboboxProvider.js.map +1 -1
  208. package/esm/form/combobox/FilteredOptions/AddNewOption.d.ts +3 -0
  209. package/esm/form/combobox/FilteredOptions/AddNewOption.js +36 -0
  210. package/esm/form/combobox/FilteredOptions/AddNewOption.js.map +1 -0
  211. package/esm/form/combobox/FilteredOptions/FilteredOptions.js +13 -57
  212. package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  213. package/esm/form/combobox/FilteredOptions/FilteredOptionsItem.d.ts +6 -0
  214. package/esm/form/combobox/FilteredOptions/FilteredOptionsItem.js +38 -0
  215. package/esm/form/combobox/FilteredOptions/FilteredOptionsItem.js.map +1 -0
  216. package/esm/form/combobox/FilteredOptions/LoadingMessage.d.ts +3 -0
  217. package/esm/form/combobox/FilteredOptions/LoadingMessage.js +11 -0
  218. package/esm/form/combobox/FilteredOptions/LoadingMessage.js.map +1 -0
  219. package/esm/form/combobox/FilteredOptions/MaxSelectedMessage.d.ts +3 -0
  220. package/esm/form/combobox/FilteredOptions/MaxSelectedMessage.js +15 -0
  221. package/esm/form/combobox/FilteredOptions/MaxSelectedMessage.js.map +1 -0
  222. package/esm/form/combobox/FilteredOptions/NoSearchHitsMessage.d.ts +3 -0
  223. package/esm/form/combobox/FilteredOptions/NoSearchHitsMessage.js +9 -0
  224. package/esm/form/combobox/FilteredOptions/NoSearchHitsMessage.js.map +1 -0
  225. package/esm/form/combobox/FilteredOptions/filtered-options-util.d.ts +1 -0
  226. package/esm/form/combobox/FilteredOptions/filtered-options-util.js +6 -1
  227. package/esm/form/combobox/FilteredOptions/filtered-options-util.js.map +1 -1
  228. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +5 -5
  229. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  230. package/esm/form/combobox/Input/Input.context.d.ts +20 -7
  231. package/esm/form/combobox/Input/Input.context.js +7 -13
  232. package/esm/form/combobox/Input/Input.context.js.map +1 -1
  233. package/esm/form/combobox/Input/Input.d.ts +2 -1
  234. package/esm/form/combobox/Input/Input.js +46 -21
  235. package/esm/form/combobox/Input/Input.js.map +1 -1
  236. package/esm/form/combobox/Input/InputController.d.ts +1 -1
  237. package/esm/form/combobox/Input/InputController.js +1 -1
  238. package/esm/form/combobox/Input/InputController.js.map +1 -1
  239. package/esm/form/combobox/types.d.ts +8 -4
  240. package/esm/form/fieldset/context.d.ts +0 -1
  241. package/esm/form/fieldset/useFieldset.d.ts +1 -1
  242. package/esm/form/file-upload/FileUpload.context.d.ts +0 -1
  243. package/esm/form/file-upload/parts/dropzone/dropzone.types.d.ts +0 -1
  244. package/esm/form/file-upload/useFileUpload.d.ts +1 -2
  245. package/esm/form/radio/useRadio.d.ts +3 -3
  246. package/esm/form/search/context.d.ts +0 -1
  247. package/esm/layout/base/PrimitiveAsChildProps.d.ts +0 -1
  248. package/esm/layout/grid/HGrid.js +4 -1
  249. package/esm/layout/grid/HGrid.js.map +1 -1
  250. package/esm/layout/stack/Stack.js +7 -2
  251. package/esm/layout/stack/Stack.js.map +1 -1
  252. package/esm/list/context.d.ts +0 -1
  253. package/esm/list/types.d.ts +0 -1
  254. package/esm/modal/types.d.ts +0 -1
  255. package/esm/overlays/dismissablelayer/DismissableLayer.d.ts +1 -1
  256. package/esm/overlays/floating-menu/Menu.d.ts +106 -0
  257. package/esm/overlays/floating-menu/Menu.js +551 -0
  258. package/esm/overlays/floating-menu/Menu.js.map +1 -0
  259. package/esm/overlays/floating-menu/parts/FocusScope.d.ts +22 -0
  260. package/esm/overlays/floating-menu/parts/FocusScope.js +63 -0
  261. package/esm/overlays/floating-menu/parts/FocusScope.js.map +1 -0
  262. package/esm/overlays/floating-menu/parts/RovingFocus.d.ts +9 -0
  263. package/esm/overlays/floating-menu/parts/RovingFocus.js +86 -0
  264. package/esm/overlays/floating-menu/parts/RovingFocus.js.map +1 -0
  265. package/esm/overlays/floating-menu/parts/SlottedDivElement.d.ts +7 -0
  266. package/esm/overlays/floating-menu/parts/SlottedDivElement.js +20 -0
  267. package/esm/overlays/floating-menu/parts/SlottedDivElement.js.map +1 -0
  268. package/esm/stepper/context.d.ts +0 -1
  269. package/esm/table/context.d.ts +0 -1
  270. package/esm/tabs/Tabs.context.d.ts +1 -2
  271. package/esm/tabs/parts/tab/useTab.d.ts +1 -2
  272. package/esm/tabs/parts/tablist/useScrollButtons.d.ts +0 -1
  273. package/esm/tabs/parts/tablist/useTabList.js +2 -1
  274. package/esm/tabs/parts/tablist/useTabList.js.map +1 -1
  275. package/esm/tabs/useTabs.d.ts +0 -1
  276. package/esm/timeline/hooks/usePeriodContext.d.ts +0 -1
  277. package/esm/timeline/hooks/useRowContext.d.ts +0 -1
  278. package/esm/timeline/hooks/useTimelineContext.d.ts +0 -1
  279. package/esm/timeline/period/types.d.ts +0 -1
  280. package/esm/timeline/zoom/index.d.ts +1 -1
  281. package/esm/toggle-group/ToggleGroup.context.d.ts +1 -2
  282. package/esm/toggle-group/parts/useToggleItem.d.ts +1 -2
  283. package/esm/toggle-group/parts/useToggleItem.js +2 -1
  284. package/esm/toggle-group/parts/useToggleItem.js.map +1 -1
  285. package/esm/toggle-group/useToggleGroup.d.ts +0 -1
  286. package/esm/util/composeEventHandlers.d.ts +1 -2
  287. package/esm/util/hooks/descendants/useDescendant.d.ts +1 -1
  288. package/esm/util/hooks/useCallbackRef.d.ts +0 -1
  289. package/esm/util/hooks/useMergeRefs.d.ts +1 -1
  290. package/esm/util/i18n/i18n.context.d.ts +0 -1
  291. package/esm/util/types/AsChild.d.ts +0 -1
  292. package/esm/util/types/AsChildProps.d.ts +0 -1
  293. package/esm/util/virtualfocus/Context.d.ts +43 -0
  294. package/esm/util/virtualfocus/Context.js +5 -0
  295. package/esm/util/virtualfocus/Context.js.map +1 -0
  296. package/esm/util/virtualfocus/SlottedDivElement.d.ts +7 -0
  297. package/esm/util/virtualfocus/SlottedDivElement.js +20 -0
  298. package/esm/util/virtualfocus/SlottedDivElement.js.map +1 -0
  299. package/esm/util/virtualfocus/VirtualFocus.d.ts +62 -0
  300. package/esm/util/virtualfocus/VirtualFocus.js +63 -0
  301. package/esm/util/virtualfocus/VirtualFocus.js.map +1 -0
  302. package/esm/util/virtualfocus/parts/VirtualFocusAnchor.d.ts +33 -0
  303. package/esm/util/virtualfocus/parts/VirtualFocusAnchor.js +54 -0
  304. package/esm/util/virtualfocus/parts/VirtualFocusAnchor.js.map +1 -0
  305. package/esm/util/virtualfocus/parts/VirtualFocusContent.d.ts +4 -0
  306. package/esm/util/virtualfocus/parts/VirtualFocusContent.js +19 -0
  307. package/esm/util/virtualfocus/parts/VirtualFocusContent.js.map +1 -0
  308. package/esm/util/virtualfocus/parts/VirtualFocusItem.d.ts +18 -0
  309. package/esm/util/virtualfocus/parts/VirtualFocusItem.js +38 -0
  310. package/esm/util/virtualfocus/parts/VirtualFocusItem.js.map +1 -0
  311. package/package.json +3 -3
  312. package/src/form/combobox/Combobox.tsx +4 -1
  313. package/src/form/combobox/ComboboxProvider.tsx +3 -0
  314. package/src/form/combobox/FilteredOptions/AddNewOption.tsx +63 -0
  315. package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +11 -121
  316. package/src/form/combobox/FilteredOptions/FilteredOptionsItem.tsx +73 -0
  317. package/src/form/combobox/FilteredOptions/LoadingMessage.tsx +20 -0
  318. package/src/form/combobox/FilteredOptions/MaxSelectedMessage.tsx +27 -0
  319. package/src/form/combobox/FilteredOptions/NoSearchHitsMessage.tsx +19 -0
  320. package/src/form/combobox/FilteredOptions/filtered-options-util.ts +9 -1
  321. package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +8 -5
  322. package/src/form/combobox/Input/Input.context.tsx +27 -25
  323. package/src/form/combobox/Input/Input.tsx +60 -29
  324. package/src/form/combobox/Input/InputController.tsx +2 -0
  325. package/src/form/combobox/__tests__/combobox.test.tsx +174 -66
  326. package/src/form/combobox/types.ts +11 -7
  327. package/src/layout/grid/HGrid.tsx +4 -1
  328. package/src/layout/stack/Stack.tsx +6 -2
  329. package/src/overlays/floating-menu/Menu.tsx +1177 -0
  330. package/src/overlays/floating-menu/parts/FocusScope.tsx +84 -0
  331. package/src/overlays/floating-menu/parts/RovingFocus.tsx +121 -0
  332. package/src/overlays/floating-menu/parts/SlottedDivElement.tsx +17 -0
  333. package/src/tabs/parts/tablist/useTabList.ts +4 -1
  334. package/src/toggle-group/parts/useToggleItem.ts +4 -1
  335. package/src/util/composeEventHandlers.ts +1 -1
  336. package/src/util/virtualfocus/Context.tsx +27 -0
  337. package/src/util/virtualfocus/SlottedDivElement.tsx +17 -0
  338. package/src/util/virtualfocus/VirtualFocus.tsx +89 -0
  339. package/src/util/virtualfocus/parts/VirtualFocusAnchor.tsx +102 -0
  340. package/src/util/virtualfocus/parts/VirtualFocusContent.tsx +17 -0
  341. package/src/util/virtualfocus/parts/VirtualFocusItem.tsx +60 -0
@@ -0,0 +1,1177 @@
1
+ import React, {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+ import ReactDOM from "react-dom";
9
+ import { Portal } from "../../portal";
10
+ import { composeEventHandlers } from "../../util/composeEventHandlers";
11
+ import { createContext } from "../../util/create-context";
12
+ import { useCallbackRef, useId, useMergeRefs } from "../../util/hooks";
13
+ import { createDescendantContext } from "../../util/hooks/descendants/useDescendant";
14
+ import { DismissableLayer } from "../dismissablelayer/DismissableLayer";
15
+ import { Floating } from "../floating/Floating";
16
+ import { FocusScope } from "./parts/FocusScope";
17
+ import { RovingFocus, RovingFocusProps } from "./parts/RovingFocus";
18
+ import {
19
+ SlottedDivElement,
20
+ SlottedDivElementRef,
21
+ SlottedDivProps,
22
+ } from "./parts/SlottedDivElement";
23
+
24
+ /* -------------------------------------------------------------------------- */
25
+ /* Constants */
26
+ /* -------------------------------------------------------------------------- */
27
+ const SELECTION_KEYS = ["Enter", " "];
28
+ const SUB_OPEN_KEYS = [...SELECTION_KEYS, "ArrowRight"];
29
+ const SUB_CLOSE_KEYS = ["ArrowLeft"];
30
+ const FIRST_KEYS = ["ArrowDown", "PageUp", "Home"];
31
+ const LAST_KEYS = ["ArrowUp", "PageDown", "End"];
32
+ const FIRST_LAST_KEYS = [...FIRST_KEYS, ...LAST_KEYS];
33
+
34
+ type Point = { x: number; y: number };
35
+ type Polygon = Point[];
36
+ type SubMenuSide = "left" | "right";
37
+ type GraceIntent = { area: Polygon; side: SubMenuSide };
38
+ type CheckedState = boolean | "indeterminate";
39
+
40
+ /* -------------------------------------------------------------------------- */
41
+ /* Menu */
42
+ /* -------------------------------------------------------------------------- */
43
+ interface MenuProps {
44
+ children?: React.ReactNode;
45
+ open?: boolean;
46
+ onOpenChange?: (open: boolean) => void;
47
+ modal?: boolean;
48
+ }
49
+
50
+ interface MenuComponent extends React.FC<MenuProps> {
51
+ Anchor: typeof MenuAnchor;
52
+ Portal: typeof MenuPortal;
53
+ Content: typeof MenuContent;
54
+ Group: typeof MenuGroup;
55
+ Item: typeof MenuItem;
56
+ CheckboxItem: typeof MenuCheckboxItem;
57
+ RadioGroup: typeof MenuRadioGroup;
58
+ RadioItem: typeof MenuRadioItem;
59
+ Separator: typeof MenuSeparator;
60
+ Sub: typeof MenuSub;
61
+ SubTrigger: typeof MenuSubTrigger;
62
+ SubContent: typeof MenuSubContent;
63
+ ItemIndicator: typeof MenuItemIndicator;
64
+ }
65
+
66
+ const [
67
+ MenuDescendantsProvider,
68
+ useMenuDescendantsContext,
69
+ useMenuDescendants,
70
+ useMenuDescendant,
71
+ ] = createDescendantContext<SlottedDivElementRef>();
72
+
73
+ type MenuContentElementRef = React.ElementRef<typeof Floating.Content>;
74
+
75
+ type MenuContextValue = {
76
+ open: boolean;
77
+ onOpenChange: (open: boolean) => void;
78
+ content: MenuContentElementRef | null;
79
+ onContentChange: (content: MenuContentElementRef | null) => void;
80
+ };
81
+
82
+ const [MenuProvider, useMenuContext] = createContext<MenuContextValue>({
83
+ providerName: "MenuProvider",
84
+ hookName: "useMenuContext",
85
+ });
86
+
87
+ type MenuRootContextValue = {
88
+ onClose: () => void;
89
+ isUsingKeyboardRef: React.RefObject<boolean>;
90
+ modal: boolean;
91
+ };
92
+
93
+ const [MenuRootProvider, useMenuRootContext] =
94
+ createContext<MenuRootContextValue>({
95
+ providerName: "MenuRootProvider",
96
+ hookName: "useMenuRootContext",
97
+ });
98
+
99
+ const MenuRoot = ({
100
+ open = false,
101
+ children,
102
+ onOpenChange,
103
+ modal = true,
104
+ }: MenuProps) => {
105
+ const [content, setContent] = useState<MenuContentElement | null>(null);
106
+ const isUsingKeyboardRef = useRef(false);
107
+ const handleOpenChange = useCallbackRef(onOpenChange);
108
+
109
+ useEffect(() => {
110
+ const globalDocument = globalThis.document;
111
+ // Capturephase ensures we set the boolean before any side effects execute
112
+ // in response to the key or pointer event as they might depend on this value.
113
+ const handlePointer = () => {
114
+ isUsingKeyboardRef.current = false;
115
+ };
116
+ const handleKeyDown = () => {
117
+ isUsingKeyboardRef.current = true;
118
+ globalDocument.addEventListener("pointerdown", handlePointer, {
119
+ capture: true,
120
+ once: true,
121
+ });
122
+ globalDocument.addEventListener("pointermove", handlePointer, {
123
+ capture: true,
124
+ once: true,
125
+ });
126
+ };
127
+ globalDocument.addEventListener("keydown", handleKeyDown, {
128
+ capture: true,
129
+ });
130
+ return () => {
131
+ globalDocument.removeEventListener("keydown", handleKeyDown, {
132
+ capture: true,
133
+ });
134
+ globalDocument.removeEventListener("pointerdown", handlePointer, {
135
+ capture: true,
136
+ });
137
+ globalDocument.removeEventListener("pointermove", handlePointer, {
138
+ capture: true,
139
+ });
140
+ };
141
+ }, []);
142
+
143
+ return (
144
+ <Floating>
145
+ <MenuProvider
146
+ open={open}
147
+ onOpenChange={handleOpenChange}
148
+ content={content}
149
+ onContentChange={setContent}
150
+ >
151
+ <MenuRootProvider
152
+ onClose={React.useCallback(
153
+ () => handleOpenChange(false),
154
+ [handleOpenChange],
155
+ )}
156
+ isUsingKeyboardRef={isUsingKeyboardRef}
157
+ modal={modal}
158
+ >
159
+ {children}
160
+ </MenuRootProvider>
161
+ </MenuProvider>
162
+ </Floating>
163
+ );
164
+ };
165
+
166
+ const Menu = MenuRoot as MenuComponent;
167
+
168
+ /* -------------------------------------------------------------------------- */
169
+ /* Menu Anchor */
170
+ /* -------------------------------------------------------------------------- */
171
+ type MenuAnchorElement = React.ElementRef<typeof Floating.Anchor>;
172
+ type MenuAnchorProps = React.ComponentPropsWithoutRef<typeof Floating.Anchor>;
173
+
174
+ const MenuAnchor = forwardRef<MenuAnchorElement, MenuAnchorProps>(
175
+ (props: MenuAnchorProps, forwardedRef) => {
176
+ return <Floating.Anchor {...props} ref={forwardedRef} />;
177
+ },
178
+ );
179
+
180
+ /* -------------------------------------------------------------------------- */
181
+ /* Menu Content */
182
+ /* -------------------------------------------------------------------------- */
183
+ type MenuContentContextValue = {
184
+ onItemEnter: (event: React.PointerEvent) => void;
185
+ onItemLeave: (event: React.PointerEvent) => void;
186
+ onPointerLeaveTrigger: (event: React.PointerEvent) => void;
187
+ pointerGraceTimerRef: React.MutableRefObject<number>;
188
+ onPointerGraceIntentChange: (intent: GraceIntent | null) => void;
189
+ };
190
+
191
+ const [MenuContentProvider, useMenuContentContext] =
192
+ createContext<MenuContentContextValue>({
193
+ providerName: "MenuContentProvider",
194
+ hookName: "useMenuContentContext",
195
+ });
196
+
197
+ type MenuContentElement = MenuContentInternalElement;
198
+ interface MenuContentProps extends MenuContentInternalTypeProps {}
199
+
200
+ const MenuContent = React.forwardRef<
201
+ MenuContentInternalElement,
202
+ MenuContentProps
203
+ >((props: MenuContentProps, ref) => {
204
+ const descendants = useMenuDescendants();
205
+ const rootContext = useMenuRootContext();
206
+
207
+ return (
208
+ <MenuDescendantsProvider value={descendants}>
209
+ {rootContext.modal ? (
210
+ <MenuRootContentModal {...props} ref={ref} />
211
+ ) : (
212
+ <MenuRootContentNonModal {...props} ref={ref} />
213
+ )}
214
+ </MenuDescendantsProvider>
215
+ );
216
+ });
217
+
218
+ /* ---------------------------- Non-modal content --------------------------- */
219
+ const MenuRootContentNonModal = React.forwardRef<
220
+ MenuContentInternalElement,
221
+ MenuContentInternalTypeProps
222
+ >((props: MenuContentInternalTypeProps, ref) => {
223
+ const context = useMenuContext();
224
+ return (
225
+ <MenuContentInternal
226
+ {...props}
227
+ ref={ref}
228
+ disableOutsidePointerEvents={false}
229
+ onDismiss={() => context.onOpenChange(false)}
230
+ />
231
+ );
232
+ });
233
+
234
+ /* ------------------------------ Modal content ----------------------------- */
235
+ const MenuRootContentModal = forwardRef<
236
+ MenuContentInternalElement,
237
+ MenuContentInternalTypeProps
238
+ >((props: MenuContentInternalTypeProps, ref) => {
239
+ const context = useMenuContext();
240
+
241
+ return (
242
+ <MenuContentInternal
243
+ {...props}
244
+ ref={ref}
245
+ // make sure to only disable pointer events when open
246
+ // this avoids blocking interactions while animating out
247
+ disableOutsidePointerEvents={context.open}
248
+ // When focus is trapped, a `focusout` event may still happen.
249
+ // We make sure we don't trigger our `onDismiss` in such case.
250
+ onFocusOutside={composeEventHandlers(
251
+ props.onFocusOutside,
252
+ (event) => event.preventDefault(),
253
+ { checkForDefaultPrevented: false },
254
+ )}
255
+ onDismiss={() => context.onOpenChange(false)}
256
+ />
257
+ );
258
+ });
259
+
260
+ /* -------------------------- Menu content internals ------------------------- */
261
+ type MenuContentInternalElement = React.ElementRef<typeof Floating.Content>;
262
+ type FocusScopeProps = React.ComponentPropsWithoutRef<typeof FocusScope>;
263
+ type DismissableLayerProps = React.ComponentPropsWithoutRef<
264
+ typeof DismissableLayer
265
+ >;
266
+
267
+ type MenuContentInternalPrivateProps = {
268
+ onOpenAutoFocus?: FocusScopeProps["onMountHandler"];
269
+ onDismiss?: DismissableLayerProps["onDismiss"];
270
+ disableOutsidePointerEvents?: DismissableLayerProps["disableOutsidePointerEvents"];
271
+ };
272
+
273
+ interface MenuContentInternalProps
274
+ extends MenuContentInternalPrivateProps,
275
+ Omit<
276
+ React.ComponentPropsWithoutRef<typeof Floating.Content>,
277
+ "dir" | "onPlaced"
278
+ > {
279
+ /**
280
+ * Event handler called when auto-focusing after close.
281
+ * Can be prevented.
282
+ */
283
+ onCloseAutoFocus?: FocusScopeProps["onUnmountHandler"];
284
+ onEntryFocus?: RovingFocusProps["onEntryFocus"];
285
+ onEscapeKeyDown?: DismissableLayerProps["onEscapeKeyDown"];
286
+ onPointerDownOutside?: DismissableLayerProps["onPointerDownOutside"];
287
+ onFocusOutside?: DismissableLayerProps["onFocusOutside"];
288
+ onInteractOutside?: DismissableLayerProps["onInteractOutside"];
289
+ }
290
+
291
+ const MenuContentInternal = forwardRef<
292
+ MenuContentInternalElement,
293
+ MenuContentInternalProps
294
+ >(
295
+ (
296
+ {
297
+ onOpenAutoFocus,
298
+ onCloseAutoFocus,
299
+ disableOutsidePointerEvents,
300
+ onEntryFocus,
301
+ onEscapeKeyDown,
302
+ onPointerDownOutside,
303
+ onFocusOutside,
304
+ onInteractOutside,
305
+ onDismiss,
306
+ ...rest
307
+ }: MenuContentInternalProps,
308
+ forwardedRef,
309
+ ) => {
310
+ const descendants = useMenuDescendantsContext();
311
+
312
+ const context = useMenuContext();
313
+ const rootContext = useMenuRootContext();
314
+
315
+ const contentRef = useRef<HTMLDivElement>(null);
316
+ const composedRefs = useMergeRefs(
317
+ forwardedRef,
318
+ contentRef,
319
+ context.onContentChange,
320
+ );
321
+ const pointerGraceTimerRef = React.useRef(0);
322
+ const pointerGraceIntentRef = React.useRef<GraceIntent | null>(null);
323
+ const pointerDirRef = React.useRef<SubMenuSide>("right");
324
+ const lastPointerXRef = React.useRef(0);
325
+
326
+ const isPointerMovingToSubmenu = React.useCallback(
327
+ (event: React.PointerEvent) => {
328
+ const isMovingTowards =
329
+ pointerDirRef.current === pointerGraceIntentRef.current?.side;
330
+ return (
331
+ isMovingTowards &&
332
+ isPointerInGraceArea(event, pointerGraceIntentRef.current?.area)
333
+ );
334
+ },
335
+ [],
336
+ );
337
+
338
+ return (
339
+ <MenuContentProvider
340
+ onItemEnter={React.useCallback(
341
+ (event) => {
342
+ if (isPointerMovingToSubmenu(event)) event.preventDefault();
343
+ },
344
+ [isPointerMovingToSubmenu],
345
+ )}
346
+ onItemLeave={React.useCallback(
347
+ (event) => {
348
+ if (isPointerMovingToSubmenu(event)) return;
349
+
350
+ /**
351
+ * Resets focus from current active item to content area
352
+ * This is to prevent focus from being stuck on an item when we move pointer outside the menu or onto a disabled item
353
+ */
354
+ contentRef.current?.focus();
355
+ },
356
+ [isPointerMovingToSubmenu],
357
+ )}
358
+ onPointerLeaveTrigger={React.useCallback(
359
+ (event) => {
360
+ if (isPointerMovingToSubmenu(event)) event.preventDefault();
361
+ },
362
+ [isPointerMovingToSubmenu],
363
+ )}
364
+ pointerGraceTimerRef={pointerGraceTimerRef}
365
+ onPointerGraceIntentChange={React.useCallback((intent) => {
366
+ pointerGraceIntentRef.current = intent;
367
+ }, [])}
368
+ >
369
+ <FocusScope
370
+ onMountHandler={composeEventHandlers(onOpenAutoFocus, (event) => {
371
+ // when opening, explicitly focus the content area only and leave
372
+ // `onEntryFocus` in control of focusing first item
373
+ event.preventDefault();
374
+ contentRef.current?.focus({ preventScroll: true });
375
+ })}
376
+ onUnmountHandler={onCloseAutoFocus}
377
+ >
378
+ <DismissableLayer
379
+ asChild
380
+ disableOutsidePointerEvents={disableOutsidePointerEvents}
381
+ onEscapeKeyDown={onEscapeKeyDown}
382
+ onPointerDownOutside={onPointerDownOutside}
383
+ onFocusOutside={onFocusOutside}
384
+ onInteractOutside={onInteractOutside}
385
+ onDismiss={onDismiss}
386
+ >
387
+ <RovingFocus
388
+ asChild
389
+ descendants={descendants}
390
+ onEntryFocus={composeEventHandlers(onEntryFocus, (event) => {
391
+ // only focus first item when using keyboard
392
+ if (!rootContext.isUsingKeyboardRef.current)
393
+ event.preventDefault();
394
+ })}
395
+ >
396
+ <Floating.Content
397
+ role="menu"
398
+ aria-orientation="vertical"
399
+ data-state={getOpenState(context.open)}
400
+ data-aksel-menu-content=""
401
+ dir="ltr"
402
+ {...rest}
403
+ ref={composedRefs}
404
+ style={{ outline: "none", ...rest.style }}
405
+ onKeyDown={composeEventHandlers(rest.onKeyDown, (event) => {
406
+ // submenu key events bubble through portals. We only care about keys in this menu.
407
+ const target = event.target as HTMLElement;
408
+ const isKeyDownInside =
409
+ target.closest("[data-aksel-menu-content]") ===
410
+ event.currentTarget;
411
+ if (isKeyDownInside) {
412
+ // menus should not be navigated using tab key so we prevent it
413
+ if (event.key === "Tab") event.preventDefault();
414
+ }
415
+
416
+ // focus first/last item based on key pressed
417
+ const content = contentRef.current;
418
+ if (event.target !== content) return;
419
+ if (!FIRST_LAST_KEYS.includes(event.key)) return;
420
+ event.preventDefault();
421
+
422
+ if (LAST_KEYS.includes(event.key)) {
423
+ descendants.lastEnabled()?.node?.focus();
424
+ return;
425
+ }
426
+ descendants.firstEnabled()?.node?.focus();
427
+ })}
428
+ onPointerMove={composeEventHandlers(
429
+ rest.onPointerMove,
430
+ whenMouse((event) => {
431
+ const target = event.target as HTMLElement;
432
+ const pointerXHasChanged =
433
+ lastPointerXRef.current !== event.clientX;
434
+
435
+ // We don't use `event.movementX` for this check because Safari will
436
+ // always return `0` on a pointer event.
437
+ if (
438
+ event.currentTarget.contains(target) &&
439
+ pointerXHasChanged
440
+ ) {
441
+ const newDir =
442
+ event.clientX > lastPointerXRef.current
443
+ ? "right"
444
+ : "left";
445
+ pointerDirRef.current = newDir;
446
+ lastPointerXRef.current = event.clientX;
447
+ }
448
+ }),
449
+ )}
450
+ />
451
+ </RovingFocus>
452
+ </DismissableLayer>
453
+ </FocusScope>
454
+ </MenuContentProvider>
455
+ );
456
+ },
457
+ );
458
+
459
+ interface MenuContentInternalTypeProps
460
+ extends Omit<
461
+ MenuContentInternalProps,
462
+ keyof MenuContentInternalPrivateProps
463
+ > {}
464
+
465
+ /* -------------------------------------------------------------------------- */
466
+ /* Menu item */
467
+ /* -------------------------------------------------------------------------- */
468
+ const ITEM_SELECT_EVENT = "menu.itemSelect";
469
+
470
+ type MenuItemElement = MenuItemInternalElement;
471
+
472
+ interface MenuItemProps extends Omit<MenuItemInternalProps, "onSelect"> {
473
+ onSelect?: (event: Event) => void;
474
+ }
475
+
476
+ const MenuItem = forwardRef<MenuItemElement, MenuItemProps>(
477
+ (
478
+ {
479
+ disabled = false,
480
+ onSelect,
481
+ onClick,
482
+ onPointerUp,
483
+ onPointerDown,
484
+ onKeyDown,
485
+ ...rest
486
+ }: MenuItemProps,
487
+ forwardedRef,
488
+ ) => {
489
+ const ref = useRef<HTMLDivElement>(null);
490
+ const rootContext = useMenuRootContext();
491
+ const composedRefs = useMergeRefs(forwardedRef, ref);
492
+ const isPointerDownRef = useRef(false);
493
+
494
+ const handleSelect = () => {
495
+ const menuItem = ref.current;
496
+ if (!disabled && menuItem && onSelect) {
497
+ const itemSelectEvent = new CustomEvent(ITEM_SELECT_EVENT, {
498
+ bubbles: true,
499
+ cancelable: true,
500
+ });
501
+ menuItem.addEventListener(ITEM_SELECT_EVENT, onSelect, { once: true });
502
+
503
+ /**
504
+ * We flush the event synchronously to ensure that the event is dispatched before other events react to side-effect from event.
505
+ * This is necessary to prevent the menu from potentially closing before we are able to prevent it.
506
+ */
507
+ ReactDOM.flushSync(() => menuItem.dispatchEvent(itemSelectEvent));
508
+ if (itemSelectEvent.defaultPrevented) {
509
+ isPointerDownRef.current = false;
510
+ } else {
511
+ rootContext.onClose();
512
+ }
513
+ }
514
+ };
515
+
516
+ return (
517
+ <MenuItemInternal
518
+ {...rest}
519
+ tabIndex={disabled ? -1 : 0}
520
+ ref={composedRefs}
521
+ disabled={disabled}
522
+ onClick={composeEventHandlers(onClick, handleSelect)}
523
+ onPointerDown={composeEventHandlers(
524
+ onPointerDown,
525
+ () => {
526
+ isPointerDownRef.current = true;
527
+ },
528
+ { checkForDefaultPrevented: false },
529
+ )}
530
+ onPointerUp={composeEventHandlers(onPointerUp, (event) => {
531
+ // Pointer down can move to a different menu item which should activate it on pointer up.
532
+ // We dispatch a click for selection to allow composition with click based triggers and to
533
+ // prevent Firefox from getting stuck in text selection mode when the menu closes.
534
+ if (!isPointerDownRef.current) event.currentTarget?.click();
535
+ })}
536
+ onKeyDown={composeEventHandlers(onKeyDown, (event) => {
537
+ if (disabled) {
538
+ return;
539
+ }
540
+ if (SELECTION_KEYS.includes(event.key)) {
541
+ event.currentTarget.click();
542
+ /**
543
+ * We prevent default browser behaviour for selection keys as they should only trigger
544
+ * selection.
545
+ * - Prevents space from scrolling the page.
546
+ * - If keydown causes focus to move, prevents keydown from firing on the new target.
547
+ */
548
+ event.preventDefault();
549
+ }
550
+ })}
551
+ />
552
+ );
553
+ },
554
+ );
555
+
556
+ /* --------------------------- Menu Item internals --------------------------- */
557
+ type MenuItemInternalElement = SlottedDivElementRef;
558
+
559
+ interface MenuItemInternalProps extends SlottedDivProps {
560
+ disabled?: boolean;
561
+ }
562
+
563
+ const MenuItemInternal = forwardRef<
564
+ MenuItemInternalElement,
565
+ MenuItemInternalProps
566
+ >(
567
+ (
568
+ {
569
+ disabled = false,
570
+ onPointerMove,
571
+ onPointerLeave,
572
+ ...rest
573
+ }: MenuItemInternalProps,
574
+ forwardedRef,
575
+ ) => {
576
+ const { register } = useMenuDescendant({ disabled });
577
+
578
+ const contentContext = useMenuContentContext();
579
+ const ref = useRef<HTMLDivElement>(null);
580
+ const composedRefs = useMergeRefs(forwardedRef, ref, register);
581
+
582
+ return (
583
+ <SlottedDivElement
584
+ role="menuitem"
585
+ aria-disabled={disabled || undefined}
586
+ data-disabled={disabled ? "" : undefined}
587
+ tabIndex={-1}
588
+ {...rest}
589
+ style={{ userSelect: "none", ...rest?.style }}
590
+ ref={composedRefs}
591
+ /**
592
+ * We focus items on `pointerMove` make sure that the item is focused or re-focused
593
+ * when the mouse wiggles. If we used `mouseOver`/`mouseEnter` it would not re-focus.
594
+ * This is mostly to handle edgecases where the user uses mouse and keyboard together.
595
+ */
596
+ onPointerMove={composeEventHandlers(
597
+ onPointerMove,
598
+ whenMouse((event) => {
599
+ if (disabled) {
600
+ /**
601
+ * In the edgecase the focus is still stuck on a previous item, we make sure to reset it
602
+ * even when the disabled item can't be focused itself to reset it.
603
+ */
604
+ contentContext.onItemLeave(event);
605
+ } else {
606
+ contentContext.onItemEnter(event);
607
+ if (!event.defaultPrevented) {
608
+ event.currentTarget.focus();
609
+ }
610
+ }
611
+ }),
612
+ )}
613
+ onPointerLeave={composeEventHandlers(
614
+ onPointerLeave,
615
+ whenMouse(contentContext.onItemLeave),
616
+ )}
617
+ />
618
+ );
619
+ },
620
+ );
621
+
622
+ /* -------------------------------------------------------------------------- */
623
+ /* Menu Group */
624
+ /* -------------------------------------------------------------------------- */
625
+ interface MenuGroupProps extends SlottedDivProps {}
626
+
627
+ const MenuGroup = forwardRef<SlottedDivElementRef, MenuGroupProps>(
628
+ (props: MenuGroupProps, ref) => {
629
+ return <SlottedDivElement role="group" {...props} ref={ref} />;
630
+ },
631
+ );
632
+
633
+ /* -------------------------------------------------------------------------- */
634
+ /* Menu Portal */
635
+ /* -------------------------------------------------------------------------- */
636
+ type PortalProps = React.ComponentPropsWithoutRef<typeof Portal>;
637
+ type MenuPortalElement = React.ElementRef<typeof Portal>;
638
+
639
+ type MenuPortalProps = PortalProps & {
640
+ children: React.ReactElement;
641
+ };
642
+
643
+ const MenuPortal = forwardRef<MenuPortalElement, MenuPortalProps>(
644
+ ({ children, rootElement }: MenuPortalProps, ref) => {
645
+ const context = useMenuContext();
646
+
647
+ if (!context.open) {
648
+ return null;
649
+ }
650
+
651
+ return (
652
+ <Portal asChild rootElement={rootElement} ref={ref}>
653
+ {children}
654
+ </Portal>
655
+ );
656
+ },
657
+ );
658
+
659
+ /* -------------------------------------------------------------------------- */
660
+ /* Menu Radio */
661
+ /* -------------------------------------------------------------------------- */
662
+ const [RadioGroupProvider, useMenuRadioGroupContext] =
663
+ createContext<MenuRadioGroupProps>({
664
+ providerName: "MenuRadioGroupProvider",
665
+ hookName: "useMenuRadioGroupContext",
666
+ defaultValue: {
667
+ value: undefined,
668
+ onValueChange: () => {},
669
+ },
670
+ });
671
+
672
+ interface MenuRadioGroupProps extends MenuGroupProps {
673
+ value?: string;
674
+ onValueChange?: (value: string) => void;
675
+ }
676
+
677
+ const MenuRadioGroup = React.forwardRef<
678
+ React.ElementRef<typeof MenuGroup>,
679
+ MenuRadioGroupProps
680
+ >(({ value, onValueChange, ...rest }: MenuRadioGroupProps, ref) => {
681
+ const handleValueChange = useCallbackRef(onValueChange);
682
+ return (
683
+ <RadioGroupProvider value={value} onValueChange={handleValueChange}>
684
+ <MenuGroup {...rest} ref={ref} />
685
+ </RadioGroupProvider>
686
+ );
687
+ });
688
+
689
+ /* -------------------------------------------------------------------------- */
690
+ /* Menu Item Indicator */
691
+ /* -------------------------------------------------------------------------- */
692
+ const [MenuItemIndicatorProvider, useMenuItemIndicatorContext] = createContext<{
693
+ state: CheckedState;
694
+ }>({
695
+ providerName: "MenuItemIndicatorProvider",
696
+ hookName: "useMenuItemIndicatorContext",
697
+ });
698
+
699
+ interface MenuItemIndicatorProps extends SlottedDivProps {}
700
+
701
+ const MenuItemIndicator = forwardRef<
702
+ SlottedDivElementRef,
703
+ MenuItemIndicatorProps
704
+ >(({ asChild, ...rest }, ref) => {
705
+ const ctx = useMenuItemIndicatorContext();
706
+
707
+ return (
708
+ <SlottedDivElement
709
+ {...rest}
710
+ ref={ref}
711
+ data-state={getCheckedState(ctx.state)}
712
+ aria-hidden
713
+ asChild={asChild}
714
+ />
715
+ );
716
+ });
717
+
718
+ /* -------------------------------------------------------------------------- */
719
+ /* Menu Radio */
720
+ /* -------------------------------------------------------------------------- */
721
+ interface MenuRadioItemProps extends MenuItemProps {
722
+ value: string;
723
+ }
724
+
725
+ const MenuRadioItem = forwardRef<
726
+ React.ElementRef<typeof MenuItem>,
727
+ MenuRadioItemProps
728
+ >(({ value, onSelect, ...rest }: MenuRadioItemProps, forwardedRef) => {
729
+ const context = useMenuRadioGroupContext();
730
+ const checked = value === context.value;
731
+
732
+ return (
733
+ <MenuItemIndicatorProvider state={checked}>
734
+ <MenuItem
735
+ role="menuitemradio"
736
+ aria-checked={checked}
737
+ {...rest}
738
+ ref={forwardedRef}
739
+ data-state={getCheckedState(checked)}
740
+ onSelect={composeEventHandlers(
741
+ onSelect,
742
+ () => context.onValueChange?.(value),
743
+ { checkForDefaultPrevented: false },
744
+ )}
745
+ />
746
+ </MenuItemIndicatorProvider>
747
+ );
748
+ });
749
+
750
+ /* -------------------------------------------------------------------------- */
751
+ /* Menu Checkbox */
752
+ /* -------------------------------------------------------------------------- */
753
+ interface MenuCheckboxItemProps extends MenuItemProps {
754
+ checked?: CheckedState;
755
+ // `onCheckedChange` can never be called with `"indeterminate"` from the inside
756
+ onCheckedChange?: (checked: boolean) => void;
757
+ }
758
+
759
+ const MenuCheckboxItem = forwardRef<MenuItemElement, MenuCheckboxItemProps>(
760
+ (
761
+ {
762
+ checked = false,
763
+ onCheckedChange,
764
+ onSelect,
765
+ ...rest
766
+ }: MenuCheckboxItemProps,
767
+ forwardedRef,
768
+ ) => {
769
+ return (
770
+ <MenuItemIndicatorProvider state={checked}>
771
+ <MenuItem
772
+ role="menuitemcheckbox"
773
+ aria-checked={isIndeterminate(checked) ? "mixed" : checked}
774
+ {...rest}
775
+ ref={forwardedRef}
776
+ data-state={getCheckedState(checked)}
777
+ onSelect={composeEventHandlers(
778
+ onSelect,
779
+ () => onCheckedChange?.(isIndeterminate(checked) ? true : !checked),
780
+ { checkForDefaultPrevented: false },
781
+ )}
782
+ />
783
+ </MenuItemIndicatorProvider>
784
+ );
785
+ },
786
+ );
787
+
788
+ /* -------------------------------------------------------------------------- */
789
+ /* Menu Separator */
790
+ /* -------------------------------------------------------------------------- */
791
+ interface MenuSeparatorProps extends SlottedDivProps {}
792
+
793
+ const MenuSeparator = forwardRef<SlottedDivElementRef, MenuSeparatorProps>(
794
+ (props: MenuSeparatorProps, ref) => {
795
+ return (
796
+ <SlottedDivElement
797
+ role="separator"
798
+ aria-orientation="horizontal"
799
+ {...props}
800
+ ref={ref}
801
+ />
802
+ );
803
+ },
804
+ );
805
+
806
+ /* -------------------------------------------------------------------------- */
807
+ /* Menu SubMenu */
808
+ /* -------------------------------------------------------------------------- */
809
+ type MenuSubContextValue = {
810
+ contentId: string;
811
+ triggerId: string;
812
+ trigger: MenuItemElement | null;
813
+ onTriggerChange: (trigger: MenuItemElement | null) => void;
814
+ };
815
+
816
+ const [MenuSubProvider, useMenuSubContext] = createContext<MenuSubContextValue>(
817
+ {
818
+ providerName: "MenuSubProvider",
819
+ hookName: "useMenuSubContext",
820
+ },
821
+ );
822
+
823
+ interface MenuSubProps {
824
+ children?: React.ReactNode;
825
+ open?: boolean;
826
+ onOpenChange?: (open: boolean) => void;
827
+ }
828
+
829
+ const MenuSub: React.FC<MenuSubProps> = ({
830
+ children,
831
+ onOpenChange,
832
+ open = false,
833
+ }: MenuSubProps) => {
834
+ const parentMenuContext = useMenuContext();
835
+
836
+ const [trigger, setTrigger] = useState<MenuItemElement | null>(null);
837
+ const [content, setContent] = useState<MenuContentInternalElement | null>(
838
+ null,
839
+ );
840
+ const handleOpenChange = useCallbackRef(onOpenChange);
841
+
842
+ // Prevent the parent menu from reopening with open submenus.
843
+ useEffect(() => {
844
+ if (parentMenuContext.open === false) {
845
+ handleOpenChange(false);
846
+ }
847
+ return () => handleOpenChange(false);
848
+ }, [parentMenuContext.open, handleOpenChange]);
849
+
850
+ return (
851
+ <Floating>
852
+ <MenuProvider
853
+ open={open}
854
+ onOpenChange={handleOpenChange}
855
+ content={content}
856
+ onContentChange={setContent}
857
+ >
858
+ <MenuSubProvider
859
+ contentId={useId()}
860
+ triggerId={useId()}
861
+ trigger={trigger}
862
+ onTriggerChange={setTrigger}
863
+ >
864
+ {children}
865
+ </MenuSubProvider>
866
+ </MenuProvider>
867
+ </Floating>
868
+ );
869
+ };
870
+
871
+ /* -------------------------------------------------------------------------- */
872
+ /* Menu SubMenu Trigger */
873
+ /* -------------------------------------------------------------------------- */
874
+ interface MenuSubTriggerProps extends MenuItemInternalProps {}
875
+
876
+ const MenuSubTrigger = forwardRef<MenuItemElement, MenuSubTriggerProps>(
877
+ (props: MenuSubTriggerProps, forwardedRef) => {
878
+ const context = useMenuContext();
879
+ const subContext = useMenuSubContext();
880
+ const contentContext = useMenuContentContext();
881
+ const openTimerRef = useRef<number | null>(null);
882
+ const { pointerGraceTimerRef, onPointerGraceIntentChange } = contentContext;
883
+
884
+ const composedRefs = useMergeRefs(forwardedRef, subContext.onTriggerChange);
885
+
886
+ const clearOpenTimer = useCallback(() => {
887
+ if (openTimerRef.current) {
888
+ window.clearTimeout(openTimerRef.current);
889
+ }
890
+ openTimerRef.current = null;
891
+ }, []);
892
+
893
+ React.useEffect(() => clearOpenTimer, [clearOpenTimer]);
894
+
895
+ React.useEffect(() => {
896
+ const pointerGraceTimer = pointerGraceTimerRef.current;
897
+ return () => {
898
+ window.clearTimeout(pointerGraceTimer);
899
+ onPointerGraceIntentChange(null);
900
+ };
901
+ }, [pointerGraceTimerRef, onPointerGraceIntentChange]);
902
+
903
+ return (
904
+ <MenuAnchor asChild>
905
+ <MenuItemInternal
906
+ id={subContext.triggerId}
907
+ aria-haspopup="menu"
908
+ aria-expanded={context.open}
909
+ aria-controls={subContext.contentId}
910
+ data-state={getOpenState(context.open)}
911
+ {...props}
912
+ ref={composedRefs}
913
+ /**
914
+ * onClick is added to solve edgecase where the user clicks the trigger,
915
+ * but the focus is outside browser-window or viewport at first.
916
+ */
917
+ onClick={(event) => {
918
+ props.onClick?.(event);
919
+ if (props.disabled || event.defaultPrevented) return;
920
+
921
+ event.currentTarget.focus();
922
+ if (!context.open) context.onOpenChange(true);
923
+ }}
924
+ onPointerMove={composeEventHandlers(
925
+ props.onPointerMove,
926
+ whenMouse((event) => {
927
+ if (event.defaultPrevented) return;
928
+ if (!props.disabled && !context.open && !openTimerRef.current) {
929
+ contentContext.onPointerGraceIntentChange(null);
930
+ openTimerRef.current = window.setTimeout(() => {
931
+ context.onOpenChange(true);
932
+ clearOpenTimer();
933
+ }, 100);
934
+ }
935
+ }),
936
+ )}
937
+ onPointerLeave={composeEventHandlers(
938
+ props.onPointerLeave,
939
+ whenMouse((event) => {
940
+ clearOpenTimer();
941
+
942
+ const contentRect = context.content?.getBoundingClientRect();
943
+ if (contentRect) {
944
+ const side = context.content?.dataset.side as SubMenuSide;
945
+ const rightSide = side === "right";
946
+ const bleed = rightSide ? -5 : +5;
947
+ const contentNearEdge =
948
+ contentRect[rightSide ? "left" : "right"];
949
+ const contentFarEdge =
950
+ contentRect[rightSide ? "right" : "left"];
951
+
952
+ contentContext.onPointerGraceIntentChange({
953
+ area: [
954
+ // Apply a bleed on clientX to ensure that our exit point is
955
+ // consistently within polygon bounds
956
+ { x: event.clientX + bleed, y: event.clientY },
957
+ { x: contentNearEdge, y: contentRect.top },
958
+ { x: contentFarEdge, y: contentRect.top },
959
+ { x: contentFarEdge, y: contentRect.bottom },
960
+ { x: contentNearEdge, y: contentRect.bottom },
961
+ ],
962
+ side,
963
+ });
964
+
965
+ window.clearTimeout(pointerGraceTimerRef.current);
966
+ pointerGraceTimerRef.current = window.setTimeout(
967
+ () => contentContext.onPointerGraceIntentChange(null),
968
+ 300,
969
+ );
970
+ } else {
971
+ contentContext.onPointerLeaveTrigger(event);
972
+ if (event.defaultPrevented) return;
973
+
974
+ // There's 100ms where the user may leave an item before the submenu was opened.
975
+ contentContext.onPointerGraceIntentChange(null);
976
+ }
977
+ }),
978
+ )}
979
+ onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {
980
+ if (props.disabled) {
981
+ return;
982
+ }
983
+ if (SUB_OPEN_KEYS.includes(event.key)) {
984
+ context.onOpenChange(true);
985
+ // The trigger may hold focus if opened via pointer interaction
986
+ // so we ensure content is given focus again when switching to keyboard.
987
+ context.content?.focus();
988
+ // prevent window from scrolling
989
+ event.preventDefault();
990
+ }
991
+ })}
992
+ />
993
+ </MenuAnchor>
994
+ );
995
+ },
996
+ );
997
+
998
+ /* -------------------------------------------------------------------------- */
999
+ /* Menu SubMenu Content */
1000
+ /* -------------------------------------------------------------------------- */
1001
+ interface MenuSubContentProps
1002
+ extends Omit<
1003
+ MenuContentInternalProps,
1004
+ | keyof MenuContentInternalPrivateProps
1005
+ | "onCloseAutoFocus"
1006
+ | "onEntryFocus"
1007
+ | "side"
1008
+ | "align"
1009
+ > {}
1010
+
1011
+ const MenuSubContent = forwardRef<
1012
+ MenuContentInternalElement,
1013
+ MenuSubContentProps
1014
+ >((props: MenuSubContentProps, forwardedRef) => {
1015
+ const descendants = useMenuDescendants();
1016
+
1017
+ const context = useMenuContext();
1018
+ const rootContext = useMenuRootContext();
1019
+ const subContext = useMenuSubContext();
1020
+ const ref = useRef<MenuContentInternalElement>(null);
1021
+ const composedRefs = useMergeRefs(forwardedRef, ref);
1022
+
1023
+ return (
1024
+ <MenuDescendantsProvider value={descendants}>
1025
+ <MenuContentInternal
1026
+ id={subContext.contentId}
1027
+ aria-labelledby={subContext.triggerId}
1028
+ {...props}
1029
+ ref={composedRefs}
1030
+ align="start"
1031
+ side="right"
1032
+ disableOutsidePointerEvents={false}
1033
+ onOpenAutoFocus={(event) => {
1034
+ // when opening a submenu, focus content for keyboard users only
1035
+ if (rootContext.isUsingKeyboardRef.current) {
1036
+ ref.current?.focus();
1037
+ }
1038
+ event.preventDefault();
1039
+ }}
1040
+ // The menu might close because of focusing another menu item in the parent menu. We
1041
+ // don't want it to refocus the trigger in that case so we handle trigger focus ourselves.
1042
+ onCloseAutoFocus={(event) => event.preventDefault()}
1043
+ onFocusOutside={composeEventHandlers(props.onFocusOutside, (event) => {
1044
+ // We prevent closing when the trigger is focused to avoid triggering a re-open animation
1045
+ // on pointer interaction.
1046
+ if (event.target !== subContext.trigger) context.onOpenChange(false);
1047
+ })}
1048
+ onEscapeKeyDown={composeEventHandlers(
1049
+ props.onEscapeKeyDown,
1050
+ (event) => {
1051
+ rootContext.onClose();
1052
+ // Ensure pressing escape in submenu doesn't escape full screen mode
1053
+ event.preventDefault();
1054
+ },
1055
+ )}
1056
+ onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {
1057
+ // Submenu key events bubble through portals. We only care about keys in this menu.
1058
+ const isKeyDownInside = event.currentTarget.contains(
1059
+ event.target as HTMLElement,
1060
+ );
1061
+ let isCloseKey = SUB_CLOSE_KEYS.includes(event.key);
1062
+
1063
+ /* When submenu opens to the left, we allow closing it with ArrowRight */
1064
+ if (context.content?.dataset.side === "left") {
1065
+ isCloseKey = isCloseKey || event.key === "ArrowRight";
1066
+ }
1067
+
1068
+ if (isKeyDownInside && isCloseKey) {
1069
+ context.onOpenChange(false);
1070
+ // We focus manually because we prevented it in `onCloseAutoFocus`
1071
+ subContext.trigger?.focus();
1072
+ // Prevent window from scrolling
1073
+ event.preventDefault();
1074
+ }
1075
+ })}
1076
+ />
1077
+ </MenuDescendantsProvider>
1078
+ );
1079
+ });
1080
+
1081
+ /* -------------------------------------------------------------------------- */
1082
+ /* Utilities */
1083
+ /* -------------------------------------------------------------------------- */
1084
+ function getOpenState(open: boolean) {
1085
+ return open ? "open" : "closed";
1086
+ }
1087
+
1088
+ function isIndeterminate(checked?: CheckedState): checked is "indeterminate" {
1089
+ return checked === "indeterminate";
1090
+ }
1091
+
1092
+ function getCheckedState(checked: CheckedState) {
1093
+ return isIndeterminate(checked)
1094
+ ? "indeterminate"
1095
+ : checked
1096
+ ? "checked"
1097
+ : "unchecked";
1098
+ }
1099
+
1100
+ /**
1101
+ * Determine if a point is inside of a polygon.
1102
+ */
1103
+ function isPointInPolygon(point: Point, polygon: Polygon) {
1104
+ const { x, y } = point;
1105
+ let inside = false;
1106
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
1107
+ const xi = polygon[i].x;
1108
+ const yi = polygon[i].y;
1109
+ const xj = polygon[j].x;
1110
+ const yj = polygon[j].y;
1111
+
1112
+ // prettier-ignore
1113
+ const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
1114
+ if (intersect) inside = !inside;
1115
+ }
1116
+
1117
+ return inside;
1118
+ }
1119
+
1120
+ function isPointerInGraceArea(event: React.PointerEvent, area?: Polygon) {
1121
+ if (!area) return false;
1122
+ const cursorPos = { x: event.clientX, y: event.clientY };
1123
+ return isPointInPolygon(cursorPos, area);
1124
+ }
1125
+
1126
+ function whenMouse<E>(
1127
+ handler: React.PointerEventHandler<E>,
1128
+ ): React.PointerEventHandler<E> {
1129
+ return (event) =>
1130
+ event.pointerType === "mouse" ? handler(event) : undefined;
1131
+ }
1132
+
1133
+ /* -------------------------------------------------------------------------- */
1134
+ Menu.Anchor = MenuAnchor;
1135
+ Menu.Portal = MenuPortal;
1136
+ Menu.Content = MenuContent;
1137
+ Menu.Group = MenuGroup;
1138
+ Menu.Item = MenuItem;
1139
+ Menu.CheckboxItem = MenuCheckboxItem;
1140
+ Menu.RadioGroup = MenuRadioGroup;
1141
+ Menu.RadioItem = MenuRadioItem;
1142
+ Menu.Separator = MenuSeparator;
1143
+ Menu.Sub = MenuSub;
1144
+ Menu.SubTrigger = MenuSubTrigger;
1145
+ Menu.SubContent = MenuSubContent;
1146
+ Menu.ItemIndicator = MenuItemIndicator;
1147
+
1148
+ export {
1149
+ Menu,
1150
+ MenuAnchor,
1151
+ MenuCheckboxItem,
1152
+ MenuContent,
1153
+ MenuGroup,
1154
+ MenuItem,
1155
+ MenuItemIndicator,
1156
+ MenuPortal,
1157
+ MenuRadioGroup,
1158
+ MenuRadioItem,
1159
+ MenuSeparator,
1160
+ MenuSub,
1161
+ MenuSubContent,
1162
+ MenuSubTrigger,
1163
+ type MenuAnchorProps,
1164
+ type MenuCheckboxItemProps,
1165
+ type MenuContentProps,
1166
+ type MenuGroupProps,
1167
+ type MenuItemElement,
1168
+ type MenuItemIndicatorProps,
1169
+ type MenuPortalProps,
1170
+ type MenuProps,
1171
+ type MenuRadioGroupProps,
1172
+ type MenuRadioItemProps,
1173
+ type MenuSeparatorProps,
1174
+ type MenuSubContentProps,
1175
+ type MenuSubProps,
1176
+ type MenuSubTriggerProps,
1177
+ };