@navikt/ds-react 6.13.0 → 6.14.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 (270) 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/filtered-options-util.d.ts +1 -0
  29. package/cjs/form/combobox/FilteredOptions/filtered-options-util.js +6 -1
  30. package/cjs/form/combobox/FilteredOptions/filtered-options-util.js.map +1 -1
  31. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +5 -5
  32. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  33. package/cjs/form/combobox/Input/Input.context.d.ts +20 -7
  34. package/cjs/form/combobox/Input/Input.context.js +6 -12
  35. package/cjs/form/combobox/Input/Input.context.js.map +1 -1
  36. package/cjs/form/combobox/Input/Input.d.ts +1 -1
  37. package/cjs/form/combobox/Input/Input.js +42 -18
  38. package/cjs/form/combobox/Input/Input.js.map +1 -1
  39. package/cjs/form/combobox/Input/InputController.d.ts +1 -1
  40. package/cjs/form/combobox/Input/InputController.js.map +1 -1
  41. package/cjs/form/combobox/types.d.ts +8 -4
  42. package/cjs/form/fieldset/context.d.ts +0 -1
  43. package/cjs/form/fieldset/useFieldset.d.ts +1 -1
  44. package/cjs/form/file-upload/FileUpload.context.d.ts +0 -1
  45. package/cjs/form/file-upload/parts/dropzone/dropzone.types.d.ts +0 -1
  46. package/cjs/form/file-upload/parts/item/utils/format-file-size.js +1 -2
  47. package/cjs/form/file-upload/parts/item/utils/format-file-size.js.map +1 -1
  48. package/cjs/form/file-upload/useFileUpload.d.ts +1 -2
  49. package/cjs/form/file-upload/utils/is-accepted-file-type.js +1 -2
  50. package/cjs/form/file-upload/utils/is-accepted-file-type.js.map +1 -1
  51. package/cjs/form/file-upload/utils/is-accepted-size.js +1 -2
  52. package/cjs/form/file-upload/utils/is-accepted-size.js.map +1 -1
  53. package/cjs/form/radio/useRadio.d.ts +3 -3
  54. package/cjs/form/search/context.d.ts +0 -1
  55. package/cjs/layout/base/PrimitiveAsChildProps.d.ts +0 -1
  56. package/cjs/layout/grid/HGrid.js +4 -1
  57. package/cjs/layout/grid/HGrid.js.map +1 -1
  58. package/cjs/layout/stack/Stack.js +7 -2
  59. package/cjs/layout/stack/Stack.js.map +1 -1
  60. package/cjs/layout/utilities/css.js +2 -3
  61. package/cjs/layout/utilities/css.js.map +1 -1
  62. package/cjs/list/context.d.ts +0 -1
  63. package/cjs/list/types.d.ts +0 -1
  64. package/cjs/modal/ModalUtils.js +3 -3
  65. package/cjs/modal/ModalUtils.js.map +1 -1
  66. package/cjs/modal/types.d.ts +0 -1
  67. package/cjs/overlays/dismissablelayer/DismissableLayer.d.ts +1 -1
  68. package/cjs/overlays/dismissablelayer/util/dispatchCustomEvent.js +2 -2
  69. package/cjs/overlays/dismissablelayer/util/dispatchCustomEvent.js.map +1 -1
  70. package/cjs/overlays/dismissablelayer/util/useEscapeKeydown.js +1 -2
  71. package/cjs/overlays/dismissablelayer/util/useEscapeKeydown.js.map +1 -1
  72. package/cjs/overlays/dismissablelayer/util/useFocusOutside.js +1 -2
  73. package/cjs/overlays/dismissablelayer/util/useFocusOutside.js.map +1 -1
  74. package/cjs/overlays/dismissablelayer/util/usePointerDownOutside.js +1 -2
  75. package/cjs/overlays/dismissablelayer/util/usePointerDownOutside.js.map +1 -1
  76. package/cjs/overlays/floating/Floating.utils.js +2 -3
  77. package/cjs/overlays/floating/Floating.utils.js.map +1 -1
  78. package/cjs/slot/merge-props.js +1 -2
  79. package/cjs/slot/merge-props.js.map +1 -1
  80. package/cjs/stepper/context.d.ts +0 -1
  81. package/cjs/table/context.d.ts +0 -1
  82. package/cjs/tabs/Tabs.context.d.ts +1 -2
  83. package/cjs/tabs/parts/tab/useTab.d.ts +1 -2
  84. package/cjs/tabs/parts/tab/useTab.js +1 -2
  85. package/cjs/tabs/parts/tab/useTab.js.map +1 -1
  86. package/cjs/tabs/parts/tablist/useScrollButtons.d.ts +0 -1
  87. package/cjs/tabs/parts/tablist/useScrollButtons.js +1 -2
  88. package/cjs/tabs/parts/tablist/useScrollButtons.js.map +1 -1
  89. package/cjs/tabs/parts/tablist/useTabList.js +3 -3
  90. package/cjs/tabs/parts/tablist/useTabList.js.map +1 -1
  91. package/cjs/tabs/parts/tabpanel/useTabPanel.js +1 -2
  92. package/cjs/tabs/parts/tabpanel/useTabPanel.js.map +1 -1
  93. package/cjs/tabs/useTabs.d.ts +0 -1
  94. package/cjs/tabs/useTabs.js +1 -2
  95. package/cjs/tabs/useTabs.js.map +1 -1
  96. package/cjs/timeline/hooks/usePeriodContext.d.ts +0 -1
  97. package/cjs/timeline/hooks/useRowContext.d.ts +0 -1
  98. package/cjs/timeline/hooks/useTimelineContext.d.ts +0 -1
  99. package/cjs/timeline/period/types.d.ts +0 -1
  100. package/cjs/timeline/zoom/index.d.ts +1 -1
  101. package/cjs/toggle-group/ToggleGroup.context.d.ts +1 -2
  102. package/cjs/toggle-group/parts/useToggleItem.d.ts +1 -2
  103. package/cjs/toggle-group/parts/useToggleItem.js +3 -3
  104. package/cjs/toggle-group/parts/useToggleItem.js.map +1 -1
  105. package/cjs/toggle-group/useToggleGroup.d.ts +0 -1
  106. package/cjs/toggle-group/useToggleGroup.js +1 -2
  107. package/cjs/toggle-group/useToggleGroup.js.map +1 -1
  108. package/cjs/util/composeEventHandlers.d.ts +0 -1
  109. package/cjs/util/composeEventHandlers.js +1 -2
  110. package/cjs/util/composeEventHandlers.js.map +1 -1
  111. package/cjs/util/copy.js +1 -1
  112. package/cjs/util/copy.js.map +1 -1
  113. package/cjs/util/create-context.js +1 -2
  114. package/cjs/util/create-context.js.map +1 -1
  115. package/cjs/util/debounce.js +1 -1
  116. package/cjs/util/debounce.js.map +1 -1
  117. package/cjs/util/hooks/descendants/useDescendant.d.ts +1 -1
  118. package/cjs/util/hooks/descendants/useDescendant.js +1 -2
  119. package/cjs/util/hooks/descendants/useDescendant.js.map +1 -1
  120. package/cjs/util/hooks/descendants/utils.js +4 -4
  121. package/cjs/util/hooks/descendants/utils.js.map +1 -1
  122. package/cjs/util/hooks/useCallbackRef.d.ts +0 -1
  123. package/cjs/util/hooks/useCallbackRef.js +1 -2
  124. package/cjs/util/hooks/useCallbackRef.js.map +1 -1
  125. package/cjs/util/hooks/useControllableState.js +1 -2
  126. package/cjs/util/hooks/useControllableState.js.map +1 -1
  127. package/cjs/util/hooks/useId.js +1 -2
  128. package/cjs/util/hooks/useId.js.map +1 -1
  129. package/cjs/util/hooks/useMergeRefs.d.ts +1 -1
  130. package/cjs/util/hooks/useMergeRefs.js +2 -3
  131. package/cjs/util/hooks/useMergeRefs.js.map +1 -1
  132. package/cjs/util/i18n/get.js +1 -2
  133. package/cjs/util/i18n/get.js.map +1 -1
  134. package/cjs/util/i18n/i18n.context.d.ts +0 -1
  135. package/cjs/util/i18n/i18n.context.js +2 -2
  136. package/cjs/util/i18n/i18n.context.js.map +1 -1
  137. package/cjs/util/i18n/merge.js +1 -2
  138. package/cjs/util/i18n/merge.js.map +1 -1
  139. package/cjs/util/omit.js +1 -2
  140. package/cjs/util/omit.js.map +1 -1
  141. package/cjs/util/types/AsChild.d.ts +0 -1
  142. package/cjs/util/types/AsChildProps.d.ts +0 -1
  143. package/cjs/util/virtualfocus/Context.d.ts +43 -0
  144. package/cjs/util/virtualfocus/Context.js +9 -0
  145. package/cjs/util/virtualfocus/Context.js.map +1 -0
  146. package/cjs/util/virtualfocus/SlottedDivElement.d.ts +7 -0
  147. package/cjs/util/virtualfocus/SlottedDivElement.js +46 -0
  148. package/cjs/util/virtualfocus/SlottedDivElement.js.map +1 -0
  149. package/cjs/util/virtualfocus/VirtualFocus.d.ts +62 -0
  150. package/cjs/util/virtualfocus/VirtualFocus.js +90 -0
  151. package/cjs/util/virtualfocus/VirtualFocus.js.map +1 -0
  152. package/cjs/util/virtualfocus/parts/VirtualFocusAnchor.d.ts +33 -0
  153. package/cjs/util/virtualfocus/parts/VirtualFocusAnchor.js +80 -0
  154. package/cjs/util/virtualfocus/parts/VirtualFocusAnchor.js.map +1 -0
  155. package/cjs/util/virtualfocus/parts/VirtualFocusContent.d.ts +4 -0
  156. package/cjs/util/virtualfocus/parts/VirtualFocusContent.js +45 -0
  157. package/cjs/util/virtualfocus/parts/VirtualFocusContent.js.map +1 -0
  158. package/cjs/util/virtualfocus/parts/VirtualFocusItem.d.ts +18 -0
  159. package/cjs/util/virtualfocus/parts/VirtualFocusItem.js +64 -0
  160. package/cjs/util/virtualfocus/parts/VirtualFocusItem.js.map +1 -0
  161. package/esm/accordion/AccordionContext.d.ts +0 -1
  162. package/esm/collapsible/Collapsible.context.d.ts +0 -1
  163. package/esm/date/context/useDateInputContext.d.ts +0 -1
  164. package/esm/date/datepicker/parts/HeadRow.d.ts +0 -1
  165. package/esm/date/datepicker/parts/Row.d.ts +0 -1
  166. package/esm/date/datepicker/parts/TableHead.d.ts +0 -1
  167. package/esm/date/datepicker/parts/WeekNumber.d.ts +0 -1
  168. package/esm/date/datepicker/types.d.ts +0 -1
  169. package/esm/date/monthpicker/types.d.ts +0 -1
  170. package/esm/dropdown/Menu/index.d.ts +1 -1
  171. package/esm/dropdown/context.d.ts +0 -1
  172. package/esm/expansion-card/context.d.ts +0 -1
  173. package/esm/form/checkbox/useCheckbox.d.ts +3 -3
  174. package/esm/form/combobox/Combobox.d.ts +1 -1
  175. package/esm/form/combobox/Combobox.js.map +1 -1
  176. package/esm/form/combobox/ComboboxProvider.js +3 -1
  177. package/esm/form/combobox/ComboboxProvider.js.map +1 -1
  178. package/esm/form/combobox/FilteredOptions/filtered-options-util.d.ts +1 -0
  179. package/esm/form/combobox/FilteredOptions/filtered-options-util.js +6 -1
  180. package/esm/form/combobox/FilteredOptions/filtered-options-util.js.map +1 -1
  181. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +5 -5
  182. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  183. package/esm/form/combobox/Input/Input.context.d.ts +20 -7
  184. package/esm/form/combobox/Input/Input.context.js +7 -13
  185. package/esm/form/combobox/Input/Input.context.js.map +1 -1
  186. package/esm/form/combobox/Input/Input.d.ts +1 -1
  187. package/esm/form/combobox/Input/Input.js +43 -19
  188. package/esm/form/combobox/Input/Input.js.map +1 -1
  189. package/esm/form/combobox/Input/InputController.d.ts +1 -1
  190. package/esm/form/combobox/Input/InputController.js.map +1 -1
  191. package/esm/form/combobox/types.d.ts +8 -4
  192. package/esm/form/fieldset/context.d.ts +0 -1
  193. package/esm/form/fieldset/useFieldset.d.ts +1 -1
  194. package/esm/form/file-upload/FileUpload.context.d.ts +0 -1
  195. package/esm/form/file-upload/parts/dropzone/dropzone.types.d.ts +0 -1
  196. package/esm/form/file-upload/useFileUpload.d.ts +1 -2
  197. package/esm/form/radio/useRadio.d.ts +3 -3
  198. package/esm/form/search/context.d.ts +0 -1
  199. package/esm/layout/base/PrimitiveAsChildProps.d.ts +0 -1
  200. package/esm/layout/grid/HGrid.js +4 -1
  201. package/esm/layout/grid/HGrid.js.map +1 -1
  202. package/esm/layout/stack/Stack.js +7 -2
  203. package/esm/layout/stack/Stack.js.map +1 -1
  204. package/esm/list/context.d.ts +0 -1
  205. package/esm/list/types.d.ts +0 -1
  206. package/esm/modal/types.d.ts +0 -1
  207. package/esm/overlays/dismissablelayer/DismissableLayer.d.ts +1 -1
  208. package/esm/stepper/context.d.ts +0 -1
  209. package/esm/table/context.d.ts +0 -1
  210. package/esm/tabs/Tabs.context.d.ts +1 -2
  211. package/esm/tabs/parts/tab/useTab.d.ts +1 -2
  212. package/esm/tabs/parts/tablist/useScrollButtons.d.ts +0 -1
  213. package/esm/tabs/parts/tablist/useTabList.js +2 -1
  214. package/esm/tabs/parts/tablist/useTabList.js.map +1 -1
  215. package/esm/tabs/useTabs.d.ts +0 -1
  216. package/esm/timeline/hooks/usePeriodContext.d.ts +0 -1
  217. package/esm/timeline/hooks/useRowContext.d.ts +0 -1
  218. package/esm/timeline/hooks/useTimelineContext.d.ts +0 -1
  219. package/esm/timeline/period/types.d.ts +0 -1
  220. package/esm/timeline/zoom/index.d.ts +1 -1
  221. package/esm/toggle-group/ToggleGroup.context.d.ts +1 -2
  222. package/esm/toggle-group/parts/useToggleItem.d.ts +1 -2
  223. package/esm/toggle-group/parts/useToggleItem.js +2 -1
  224. package/esm/toggle-group/parts/useToggleItem.js.map +1 -1
  225. package/esm/toggle-group/useToggleGroup.d.ts +0 -1
  226. package/esm/util/composeEventHandlers.d.ts +0 -1
  227. package/esm/util/hooks/descendants/useDescendant.d.ts +1 -1
  228. package/esm/util/hooks/useCallbackRef.d.ts +0 -1
  229. package/esm/util/hooks/useMergeRefs.d.ts +1 -1
  230. package/esm/util/i18n/i18n.context.d.ts +0 -1
  231. package/esm/util/types/AsChild.d.ts +0 -1
  232. package/esm/util/types/AsChildProps.d.ts +0 -1
  233. package/esm/util/virtualfocus/Context.d.ts +43 -0
  234. package/esm/util/virtualfocus/Context.js +5 -0
  235. package/esm/util/virtualfocus/Context.js.map +1 -0
  236. package/esm/util/virtualfocus/SlottedDivElement.d.ts +7 -0
  237. package/esm/util/virtualfocus/SlottedDivElement.js +20 -0
  238. package/esm/util/virtualfocus/SlottedDivElement.js.map +1 -0
  239. package/esm/util/virtualfocus/VirtualFocus.d.ts +62 -0
  240. package/esm/util/virtualfocus/VirtualFocus.js +63 -0
  241. package/esm/util/virtualfocus/VirtualFocus.js.map +1 -0
  242. package/esm/util/virtualfocus/parts/VirtualFocusAnchor.d.ts +33 -0
  243. package/esm/util/virtualfocus/parts/VirtualFocusAnchor.js +54 -0
  244. package/esm/util/virtualfocus/parts/VirtualFocusAnchor.js.map +1 -0
  245. package/esm/util/virtualfocus/parts/VirtualFocusContent.d.ts +4 -0
  246. package/esm/util/virtualfocus/parts/VirtualFocusContent.js +19 -0
  247. package/esm/util/virtualfocus/parts/VirtualFocusContent.js.map +1 -0
  248. package/esm/util/virtualfocus/parts/VirtualFocusItem.d.ts +18 -0
  249. package/esm/util/virtualfocus/parts/VirtualFocusItem.js +38 -0
  250. package/esm/util/virtualfocus/parts/VirtualFocusItem.js.map +1 -0
  251. package/package.json +3 -3
  252. package/src/form/combobox/Combobox.tsx +4 -1
  253. package/src/form/combobox/ComboboxProvider.tsx +3 -0
  254. package/src/form/combobox/FilteredOptions/filtered-options-util.ts +9 -1
  255. package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +8 -5
  256. package/src/form/combobox/Input/Input.context.tsx +27 -25
  257. package/src/form/combobox/Input/Input.tsx +56 -27
  258. package/src/form/combobox/Input/InputController.tsx +1 -0
  259. package/src/form/combobox/__tests__/combobox.test.tsx +174 -66
  260. package/src/form/combobox/types.ts +11 -7
  261. package/src/layout/grid/HGrid.tsx +4 -1
  262. package/src/layout/stack/Stack.tsx +6 -2
  263. package/src/tabs/parts/tablist/useTabList.ts +4 -1
  264. package/src/toggle-group/parts/useToggleItem.ts +4 -1
  265. package/src/util/virtualfocus/Context.tsx +27 -0
  266. package/src/util/virtualfocus/SlottedDivElement.tsx +17 -0
  267. package/src/util/virtualfocus/VirtualFocus.tsx +89 -0
  268. package/src/util/virtualfocus/parts/VirtualFocusAnchor.tsx +102 -0
  269. package/src/util/virtualfocus/parts/VirtualFocusContent.tsx +17 -0
  270. package/src/util/virtualfocus/parts/VirtualFocusItem.tsx +60 -0
@@ -8,6 +8,7 @@ import { UNSAFE_Combobox } from "../index";
8
8
  const options = [
9
9
  "banana",
10
10
  "apple",
11
+ "apple pie",
11
12
  "tangerine",
12
13
  "pear",
13
14
  "grape",
@@ -71,86 +72,193 @@ describe("Render combobox", () => {
71
72
  });
72
73
  });
73
74
 
74
- test("Should show loading icon when loading (used for async search)", async () => {
75
- render(<App options={[]} isListOpen isLoading />);
75
+ describe("Combobox state-handling", () => {
76
+ test("Should show loading icon when loading (used for async search)", async () => {
77
+ render(<App options={[]} isListOpen isLoading />);
76
78
 
77
- expect(await screen.findByText("Søker...")).toBeInTheDocument();
78
- });
79
- });
79
+ expect(await screen.findByText("Søker...")).toBeInTheDocument();
80
+ });
80
81
 
81
- describe("Combobox state-handling", () => {
82
- test("Should not select previous focused element when closes", async () => {
83
- render(<App options={options} />);
82
+ test("Should not select previous focused element when closes", async () => {
83
+ render(<App options={options} />);
84
84
 
85
- await act(async () => {
86
- await userEvent.click(
87
- screen.getByRole("combobox", { name: "Hva er dine favorittfrukter?" }),
88
- );
89
- });
90
- await act(async () => {
91
- await userEvent.type(
92
- screen.getByRole("combobox", { name: "Hva er dine favorittfrukter?" }),
93
- "ban",
94
- );
95
- await userEvent.keyboard("{ArrowDown}");
96
- await userEvent.keyboard("{ArrowUp}");
97
- await userEvent.keyboard("{Enter}");
85
+ await act(async () => {
86
+ await userEvent.click(
87
+ screen.getByRole("combobox", {
88
+ name: "Hva er dine favorittfrukter?",
89
+ }),
90
+ );
91
+ });
92
+ await act(async () => {
93
+ await userEvent.type(
94
+ screen.getByRole("combobox", {
95
+ name: "Hva er dine favorittfrukter?",
96
+ }),
97
+ "ban",
98
+ );
99
+ await userEvent.keyboard("{ArrowDown}");
100
+ await userEvent.keyboard("{ArrowUp}");
101
+ await userEvent.keyboard("{Enter}");
102
+ });
103
+
104
+ expect(screen.queryByRole("button", { name: "banana slett" })).toBeNull();
98
105
  });
99
106
 
100
- expect(screen.queryByRole("button", { name: "banana slett" })).toBeNull();
101
- });
107
+ test("Should reset list when resetting input (ESC)", async () => {
108
+ render(<App options={options} />);
102
109
 
103
- test("Should reset list when resetting input (ESC)", async () => {
104
- render(<App options={options} />);
110
+ await act(async () => {
111
+ await userEvent.click(
112
+ screen.getByRole("combobox", {
113
+ name: "Hva er dine favorittfrukter?",
114
+ }),
115
+ );
116
+ });
117
+ await act(async () => {
118
+ await userEvent.type(
119
+ screen.getByRole("combobox", {
120
+ name: "Hva er dine favorittfrukter?",
121
+ }),
122
+ "apple",
123
+ );
124
+ await userEvent.keyboard("{ArrowDown}");
125
+ await userEvent.keyboard("{Escape}");
126
+ await userEvent.keyboard("{ArrowDown}");
127
+ });
128
+
129
+ expect(
130
+ await screen.findByRole("option", { name: "banana" }),
131
+ ).toBeInTheDocument();
132
+ });
105
133
 
106
- await act(async () => {
107
- await userEvent.click(
108
- screen.getByRole("combobox", { name: "Hva er dine favorittfrukter?" }),
134
+ test("Should handle complex options with label and value", async () => {
135
+ const onToggleSelected = vi.fn();
136
+ render(
137
+ <App
138
+ options={[
139
+ { label: "Hjelpemidler [HJE]", value: "HJE" },
140
+ { label: "Oppfølging [OPP]", value: "OPP" },
141
+ { label: "Sykepenger [SYK]", value: "SYK" },
142
+ { label: "Sykemelding [SYM]", value: "SYM" },
143
+ ]}
144
+ onToggleSelected={onToggleSelected}
145
+ />,
109
146
  );
147
+
148
+ expect(screen.getByRole("combobox")).toBeInTheDocument();
149
+ const bananaOption = screen.getByRole("option", {
150
+ name: "Hjelpemidler [HJE]",
151
+ selected: false,
152
+ });
153
+ await act(async () => {
154
+ await userEvent.click(bananaOption);
155
+ });
156
+ expect(onToggleSelected).toHaveBeenCalledWith("HJE", true, false);
157
+ expect(
158
+ screen.getByRole("option", {
159
+ name: "Hjelpemidler [HJE]",
160
+ selected: true,
161
+ }),
162
+ ).toBeInTheDocument();
110
163
  });
111
- await act(async () => {
112
- await userEvent.type(
113
- screen.getByRole("combobox", { name: "Hva er dine favorittfrukter?" }),
114
- "apple",
164
+
165
+ test("should trigger onChange for every character typed or removed", async () => {
166
+ const onChange = vi.fn();
167
+ const onToggleSelected = vi.fn();
168
+ render(
169
+ <App
170
+ options={["Apple", "Orange", "Banana", "Lemon"]}
171
+ onChange={onChange}
172
+ onToggleSelected={onToggleSelected}
173
+ shouldAutocomplete
174
+ />,
115
175
  );
116
- await userEvent.keyboard("{ArrowDown}");
117
- await userEvent.keyboard("{Escape}");
118
- await userEvent.keyboard("{ArrowDown}");
176
+ const combobox = screen.getByRole("combobox");
177
+ expect(combobox).toBeInTheDocument();
178
+
179
+ await act(async () => {
180
+ await userEvent.click(combobox);
181
+ await userEvent.type(combobox, "Lemon");
182
+ });
183
+ expect(onChange).toHaveBeenNthCalledWith(1, "L");
184
+ expect(onChange).toHaveBeenNthCalledWith(2, "Le");
185
+ expect(onChange).toHaveBeenNthCalledWith(3, "Lem");
186
+ expect(onChange).toHaveBeenNthCalledWith(4, "Lemo");
187
+ expect(onChange).toHaveBeenNthCalledWith(5, "Lemon");
119
188
  });
120
189
 
121
- expect(
122
- await screen.findByRole("option", { name: "banana" }),
123
- ).toBeInTheDocument();
124
- });
190
+ test("should trigger onChange while typing and on accepting autocomplete suggestions", async () => {
191
+ const onChange = vi.fn();
192
+ const onToggleSelected = vi.fn();
193
+ render(
194
+ <App
195
+ options={[
196
+ "Hjelpemidler [HJE]",
197
+ "Oppfølging [OPP]",
198
+ "Sykepenger [SYK]",
199
+ "Sykemelding [SYM]",
200
+ ]}
201
+ onChange={onChange}
202
+ onToggleSelected={onToggleSelected}
203
+ shouldAutocomplete
204
+ />,
205
+ );
206
+ const combobox = screen.getByRole("combobox");
207
+ expect(combobox).toBeInTheDocument();
125
208
 
126
- test("Should handle complex options with label and value", async () => {
127
- const onToggleSelected = vi.fn();
128
- render(
129
- <App
130
- options={[
131
- { label: "Hjelpemidler [HJE]", value: "HJE" },
132
- { label: "Oppfølging [OPP]", value: "OPP" },
133
- { label: "Sykepenger [SYK]", value: "SYK" },
134
- { label: "Sykemelding [SYM]", value: "SYM" },
135
- ]}
136
- onToggleSelected={onToggleSelected}
137
- />,
138
- );
139
-
140
- expect(screen.getByRole("combobox")).toBeInTheDocument();
141
- const bananaOption = screen.getByRole("option", {
142
- name: "Hjelpemidler [HJE]",
143
- selected: false,
209
+ await act(async () => {
210
+ await userEvent.click(combobox);
211
+ await userEvent.type(combobox, "Syke");
212
+ await userEvent.keyboard("{ArrowRight}");
213
+ await userEvent.keyboard("{ArrowDown}");
214
+ await userEvent.keyboard("{Enter}");
215
+ });
216
+ expect(onChange).toHaveBeenNthCalledWith(1, "S");
217
+ expect(onChange).toHaveBeenNthCalledWith(2, "Sy");
218
+ expect(onChange).toHaveBeenNthCalledWith(3, "Syk");
219
+ expect(onChange).toHaveBeenNthCalledWith(4, "Syke");
220
+ expect(onChange).toHaveBeenNthCalledWith(5, "Sykepenger [SYK]");
221
+ expect(onChange).toHaveBeenCalledWith("");
222
+ expect(onToggleSelected).toHaveBeenCalledOnce();
223
+ expect(onToggleSelected).toHaveBeenCalledWith(
224
+ "Sykepenger [SYK]",
225
+ true,
226
+ false,
227
+ );
144
228
  });
145
- await act(async () => {
146
- await userEvent.click(bananaOption);
229
+ });
230
+
231
+ describe("search", () => {
232
+ test("should find matched anywhere in the label", async () => {
233
+ render(<App options={options} />);
234
+
235
+ const combobox = screen.getByRole("combobox", {
236
+ name: "Hva er dine favorittfrukter?",
237
+ });
238
+
239
+ await act(async () => {
240
+ await userEvent.click(combobox);
241
+
242
+ await userEvent.type(combobox, "p");
243
+ });
244
+
245
+ const searchHits = [
246
+ "apple",
247
+ "apple pie",
248
+ "pear",
249
+ "grape",
250
+ "passion fruit",
251
+ "pineapple",
252
+ "grape fruit",
253
+ ];
254
+ searchHits.forEach((label) => {
255
+ expect(screen.getByRole("option", { name: label })).toBeInTheDocument();
256
+ });
257
+ screen.getAllByRole("option").forEach((option) => {
258
+ expect(
259
+ option.textContent && searchHits.includes(option.textContent),
260
+ ).toBe(true);
261
+ });
147
262
  });
148
- expect(onToggleSelected).toHaveBeenCalledWith("HJE", true, false);
149
- expect(
150
- screen.getByRole("option", {
151
- name: "Hjelpemidler [HJE]",
152
- selected: true,
153
- }),
154
- ).toBeInTheDocument();
155
263
  });
156
264
  });
@@ -1,4 +1,4 @@
1
- import React, { ChangeEvent, InputHTMLAttributes } from "react";
1
+ import React, { InputHTMLAttributes } from "react";
2
2
  import { FormFieldProps } from "../useFormField";
3
3
 
4
4
  /**
@@ -29,7 +29,10 @@ export type MaxSelected = {
29
29
 
30
30
  export interface ComboboxProps
31
31
  extends FormFieldProps,
32
- Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "onChange" | "value"> {
32
+ Omit<
33
+ InputHTMLAttributes<HTMLInputElement>,
34
+ "size" | "onChange" | "value" | "defaultValue"
35
+ > {
33
36
  /**
34
37
  * Combobox label.
35
38
  */
@@ -89,12 +92,9 @@ export interface ComboboxProps
89
92
  /**
90
93
  * Callback function triggered whenever the value of the input field is triggered.
91
94
  *
92
- * @param event
95
+ * @param value The value after change
93
96
  */
94
- onChange?: (
95
- event: ChangeEvent<HTMLInputElement> | null,
96
- value?: string,
97
- ) => void;
97
+ onChange?: (value: string) => void;
98
98
  /**
99
99
  * Callback function triggered whenever the input field is cleared.
100
100
  *
@@ -156,4 +156,8 @@ export interface ComboboxProps
156
156
  * This converts the input to a controlled input, so you have to use onChange to update the value.
157
157
  */
158
158
  value?: string;
159
+ /**
160
+ * Initial value of the input field. Only works when the input is uncontrolled.
161
+ */
162
+ defaultValue?: string;
159
163
  }
@@ -92,7 +92,10 @@ export const HGrid: OverridableComponent<HGridProps, HTMLDivElement> =
92
92
  <Comp
93
93
  {...omit(rest, PRIMITIVE_PROPS)}
94
94
  ref={ref}
95
- className={cl("navds-hgrid", className)}
95
+ className={cl("navds-hgrid", className, {
96
+ "navds-hgrid-gap": gap,
97
+ "navds-hgrid-align": align,
98
+ })}
96
99
  style={styles}
97
100
  >
98
101
  {children}
@@ -73,7 +73,7 @@ export const Stack: OverridableComponent<StackProps, HTMLDivElement> =
73
73
  children,
74
74
  className,
75
75
  as: Component = "div",
76
- align = "stretch",
76
+ align,
77
77
  justify,
78
78
  wrap = true,
79
79
  gap,
@@ -86,7 +86,6 @@ export const Stack: OverridableComponent<StackProps, HTMLDivElement> =
86
86
  ) => {
87
87
  const style: React.CSSProperties = {
88
88
  ..._style,
89
- "--__ac-stack-wrap": wrap ? "wrap" : "nowrap",
90
89
  ...getResponsiveProps(`stack`, "gap", "spacing", gap),
91
90
  ...getResponsiveValue(`stack`, "direction", direction),
92
91
  ...getResponsiveValue(`stack`, "align", align),
@@ -104,6 +103,11 @@ export const Stack: OverridableComponent<StackProps, HTMLDivElement> =
104
103
  className={cl("navds-stack", className, {
105
104
  "navds-vstack": direction === "column",
106
105
  "navds-hstack": direction === "row",
106
+ "navds-stack-gap": gap,
107
+ "navds-stack-align": align,
108
+ "navds-stack-justify": justify,
109
+ "navds-stack-direction": direction,
110
+ "navds-stack-wrap": wrap,
107
111
  })}
108
112
  >
109
113
  {children}
@@ -48,9 +48,12 @@ export function useTabList() {
48
48
  End: lastTab,
49
49
  };
50
50
 
51
+ const hasModifiers =
52
+ event.shiftKey || event.ctrlKey || event.altKey || event.metaKey;
53
+
51
54
  const action = keyMap[event.key];
52
55
 
53
- if (action) {
56
+ if (action && !hasModifiers) {
54
57
  event.preventDefault();
55
58
  action(event);
56
59
  } else if (event.key === "Tab") {
@@ -77,9 +77,12 @@ export function useToggleItem<P extends UseToggleItemProps>(
77
77
  End: lastTab,
78
78
  };
79
79
 
80
+ const hasModifiers =
81
+ event.shiftKey || event.ctrlKey || event.altKey || event.metaKey;
82
+
80
83
  const action = keyMap[event.key];
81
84
 
82
- if (action) {
85
+ if (action && !hasModifiers) {
83
86
  event.preventDefault();
84
87
  action(event);
85
88
  } else if (event.key === "Tab") {
@@ -0,0 +1,27 @@
1
+ import { Dispatch, SetStateAction } from "react";
2
+ import { createContext } from "../create-context";
3
+ import { createDescendantContext } from "../hooks/descendants/useDescendant";
4
+ import { SlottedDivElementRef } from "./SlottedDivElement";
5
+
6
+ export const [
7
+ VirtualFocusDescendantsProvider,
8
+ useVirtualFocusDescendantsContext,
9
+ useVirtualFocusDescendantInitializer,
10
+ useVirtualFocusDescendant,
11
+ ] = createDescendantContext<
12
+ SlottedDivElementRef,
13
+ {
14
+ handleOnSelect: () => void;
15
+ handleOnActive: () => void;
16
+ }
17
+ >();
18
+
19
+ export const [
20
+ VirtualFocusInternalContextProvider,
21
+ useVirtualFocusInternalContext,
22
+ ] = createContext<{
23
+ virtualFocusIdx: number;
24
+ setVirtualFocusIdx: Dispatch<SetStateAction<number>>;
25
+ loop: boolean;
26
+ uniqueId: string;
27
+ }>();
@@ -0,0 +1,17 @@
1
+ import React, { forwardRef } from "react";
2
+ import { Slot } from "../../slot/Slot";
3
+
4
+ interface SlottedDivProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ asChild?: boolean;
6
+ }
7
+
8
+ const SlottedDivElement = forwardRef<HTMLDivElement, SlottedDivProps>(
9
+ ({ asChild, ...rest }, forwardedRef) => {
10
+ const Comp = asChild ? Slot : "div";
11
+ return <Comp {...rest} ref={forwardedRef} />;
12
+ },
13
+ );
14
+
15
+ type SlottedDivElementRef = React.ElementRef<typeof SlottedDivElement>;
16
+
17
+ export { SlottedDivElement, type SlottedDivElementRef, type SlottedDivProps };
@@ -0,0 +1,89 @@
1
+ import React, { useState } from "react";
2
+ import { useId } from "../../util/hooks";
3
+ import {
4
+ VirtualFocusDescendantsProvider,
5
+ VirtualFocusInternalContextProvider,
6
+ useVirtualFocusDescendantInitializer,
7
+ } from "./Context";
8
+ import { VirtualFocusAnchor } from "./parts/VirtualFocusAnchor";
9
+ import { VirtualFocusContent } from "./parts/VirtualFocusContent";
10
+ import { VirtualFocusItem } from "./parts/VirtualFocusItem";
11
+
12
+ type VirtualFocusProps = {
13
+ children: React.ReactNode;
14
+ /**
15
+ * Whether to cause focus to loop around when it hits the first or last element
16
+ * @default false
17
+ **/
18
+ loop?: boolean;
19
+ };
20
+
21
+ /**
22
+ * A component that manages a virtual focus using the 'up' and 'down'
23
+ * arrow keys as well as selection with 'Return'.
24
+ *
25
+ * @see [📝 Documentation](https://aksel.nav.no/komponenter/core/virtualfocus)
26
+ * @see 🏷️ {@link AccordionProps}
27
+ *
28
+ * @example
29
+ * ```jsx
30
+ * <VirtualFocus>
31
+ * <VirtualFocus.Anchor
32
+ * role="combobox"
33
+ * onSelect={() => {
34
+ * console.log("you selected the anchor");
35
+ * }}
36
+ * onActive={() => {
37
+ * console.log("the anchor is now virtually focused");
38
+ * }}
39
+ * >
40
+ * <input type="text" />
41
+ * </VirtualFocus.Anchor>
42
+ * <VirtualFocus.Content>
43
+ * <VirtualFocus.Item
44
+ * onSelect={() => {
45
+ * console.log("you selected the item");
46
+ * }}
47
+ * onActive={() => {
48
+ * console.log("the item is now virtually focused");
49
+ * }}
50
+ * >
51
+ * <p>item 1</p>
52
+ * </VirtualFocus.Item>
53
+ * <VirtualFocus.Item
54
+ * onSelect={() => {
55
+ * console.log("you selected the item");
56
+ * }}
57
+ * onActive={() => {
58
+ * console.log("the item is now virtually focused");
59
+ * }}
60
+ * >
61
+ * <p>item 2</p>
62
+ * </VirtualFocus.Item>
63
+ * </VirtualFocus.Content>
64
+ * </VirtualFocus>
65
+ * ```
66
+ */
67
+ export const VirtualFocus = ({ children, loop = false }: VirtualFocusProps) => {
68
+ const descendants = useVirtualFocusDescendantInitializer();
69
+ const [virtualFocusIdx, setVirtualFocusIdx] = useState(0);
70
+
71
+ return (
72
+ <VirtualFocusInternalContextProvider
73
+ virtualFocusIdx={virtualFocusIdx}
74
+ setVirtualFocusIdx={setVirtualFocusIdx}
75
+ loop={loop}
76
+ uniqueId={useId()}
77
+ >
78
+ <VirtualFocusDescendantsProvider value={descendants}>
79
+ {children}
80
+ </VirtualFocusDescendantsProvider>
81
+ </VirtualFocusInternalContextProvider>
82
+ );
83
+ };
84
+
85
+ VirtualFocus.Anchor = VirtualFocusAnchor;
86
+ VirtualFocus.Item = VirtualFocusItem;
87
+ VirtualFocus.Content = VirtualFocusContent;
88
+
89
+ export default VirtualFocus;
@@ -0,0 +1,102 @@
1
+ import React, { forwardRef } from "react";
2
+ import { Slot } from "../../../slot/Slot";
3
+ import { composeEventHandlers } from "../../composeEventHandlers";
4
+ import { useMergeRefs } from "../../hooks";
5
+ import {
6
+ useVirtualFocusDescendant,
7
+ useVirtualFocusInternalContext,
8
+ } from "../Context";
9
+
10
+ export interface VirtualFocusAnchorProps
11
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "id"> {
12
+ /**
13
+ * The role of the container. This is a limited subset of roles that
14
+ * require manual focus management.
15
+ *
16
+ * Children that are to get focus inside this container element shall be
17
+ * pointed to by `aria-activedescendant`.
18
+ **/
19
+ role:
20
+ | "combobox"
21
+ | "grid"
22
+ | "listbox"
23
+ | "menu"
24
+ | "menubar"
25
+ | "radiogroup"
26
+ | "tree"
27
+ | "treegrid"
28
+ | "tablist";
29
+ /**
30
+ * The function that is run when the focused element
31
+ * is to be selected (eg. do an actual search, change route... etc)
32
+ */
33
+ onSelect: () => void;
34
+ /**
35
+ * The function that is run when the element gets
36
+ * virtual focus set to it.
37
+ */
38
+ onActive: () => void;
39
+ children: React.ReactElement;
40
+ /**
41
+ * Set this to `0` if you want the Anchor itself
42
+ * to be focusable. Since this Anchor is hoisted & merged with
43
+ * its first child, you most likely want to keep this as `0`.
44
+ * @default 0
45
+ */
46
+ tabIndex?: number;
47
+ }
48
+
49
+ /**
50
+ * Must have a single child that is an input element.
51
+ */
52
+ export const VirtualFocusAnchor = forwardRef<
53
+ HTMLInputElement,
54
+ VirtualFocusAnchorProps
55
+ >(({ onSelect, onActive, children, ...rest }, ref) => {
56
+ const { virtualFocusIdx, setVirtualFocusIdx, loop, uniqueId } =
57
+ useVirtualFocusInternalContext();
58
+
59
+ const { register, descendants, index } = useVirtualFocusDescendant({
60
+ handleOnActive: () => {
61
+ setVirtualFocusIdx(0);
62
+ onActive();
63
+ },
64
+ handleOnSelect: onSelect,
65
+ });
66
+
67
+ const mergedRefs = useMergeRefs(ref, register);
68
+
69
+ return (
70
+ <Slot
71
+ ref={mergedRefs}
72
+ tabIndex={0}
73
+ {...rest}
74
+ id={`virtualfocus-${uniqueId}-${index}`}
75
+ aria-owns={`virtualfocus-${uniqueId}-content`}
76
+ aria-controls={`virtualfocus-${uniqueId}-content`}
77
+ aria-activedescendant={`virtualfocus-${uniqueId}-${virtualFocusIdx}`}
78
+ onKeyDown={composeEventHandlers(rest.onKeyDown, (event) => {
79
+ if (event.key === "ArrowDown") {
80
+ event.preventDefault();
81
+ const to_focus_descendant = descendants.next(virtualFocusIdx, loop);
82
+ if (to_focus_descendant) {
83
+ to_focus_descendant.handleOnActive();
84
+ }
85
+ } else if (event.key === "ArrowUp") {
86
+ event.preventDefault();
87
+ const to_focus_descendant = descendants.prev(virtualFocusIdx, loop);
88
+ if (to_focus_descendant) {
89
+ to_focus_descendant.handleOnActive();
90
+ }
91
+ } else if (event.key === "Enter") {
92
+ const curr = descendants.item(virtualFocusIdx);
93
+ if (curr?.handleOnSelect) {
94
+ curr.handleOnSelect();
95
+ }
96
+ }
97
+ })}
98
+ >
99
+ {children}
100
+ </Slot>
101
+ );
102
+ });
@@ -0,0 +1,17 @@
1
+ import React, { forwardRef } from "react";
2
+ import { useVirtualFocusInternalContext } from "../Context";
3
+
4
+ export interface VirtualFocusContentProps
5
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "id"> {}
6
+
7
+ export const VirtualFocusContent = forwardRef<
8
+ HTMLDivElement,
9
+ VirtualFocusContentProps
10
+ >(({ children, ...rest }, ref) => {
11
+ const { uniqueId } = useVirtualFocusInternalContext();
12
+ return (
13
+ <div ref={ref} {...rest} id={`virtualfocus-${uniqueId}-content`}>
14
+ {children}
15
+ </div>
16
+ );
17
+ });