@navikt/ds-react 6.6.1 → 6.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/cjs/chat/Chat.d.ts +2 -1
  2. package/cjs/chat/Chat.js +2 -1
  3. package/cjs/chat/Chat.js.map +1 -1
  4. package/cjs/date/datepicker/parts/DropdownCaption.js +1 -1
  5. package/cjs/date/datepicker/parts/DropdownCaption.js.map +1 -1
  6. package/cjs/date/monthpicker/MonthCaption.js +1 -1
  7. package/cjs/date/utils/labels.d.ts +2 -2
  8. package/cjs/form/ReadOnlyIcon.d.ts +2 -2
  9. package/cjs/form/combobox/Combobox.js +7 -22
  10. package/cjs/form/combobox/Combobox.js.map +1 -1
  11. package/cjs/form/combobox/ComboboxProvider.js +2 -2
  12. package/cjs/form/combobox/ComboboxProvider.js.map +1 -1
  13. package/cjs/form/combobox/ComboboxWrapper.d.ts +1 -2
  14. package/cjs/form/combobox/ComboboxWrapper.js +4 -2
  15. package/cjs/form/combobox/ComboboxWrapper.js.map +1 -1
  16. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +4 -4
  17. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  18. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.d.ts +4 -4
  19. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +13 -15
  20. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  21. package/cjs/form/combobox/Input/{inputContext.d.ts → Input.context.d.ts} +7 -5
  22. package/cjs/form/combobox/Input/{inputContext.js → Input.context.js} +22 -22
  23. package/cjs/form/combobox/Input/Input.context.js.map +1 -0
  24. package/cjs/form/combobox/Input/Input.js +2 -2
  25. package/cjs/form/combobox/Input/Input.js.map +1 -1
  26. package/cjs/form/combobox/Input/InputController.d.ts +3 -0
  27. package/cjs/form/combobox/Input/InputController.js +70 -0
  28. package/cjs/form/combobox/Input/InputController.js.map +1 -0
  29. package/cjs/form/combobox/{ToggleListButton.js → Input/ToggleListButton.js} +1 -1
  30. package/cjs/form/combobox/Input/ToggleListButton.js.map +1 -0
  31. package/cjs/form/combobox/SelectedOptions/SelectedOptions.js +2 -2
  32. package/cjs/form/combobox/SelectedOptions/SelectedOptions.js.map +1 -1
  33. package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.d.ts +4 -4
  34. package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.js +7 -13
  35. package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.js.map +1 -1
  36. package/cjs/form/combobox/customOptionsContext.d.ts +4 -4
  37. package/cjs/form/combobox/customOptionsContext.js +10 -13
  38. package/cjs/form/combobox/customOptionsContext.js.map +1 -1
  39. package/cjs/form/combobox/types.d.ts +1 -1
  40. package/cjs/help-text/HelpTextIcon.d.ts +1 -1
  41. package/cjs/index.d.ts +1 -0
  42. package/cjs/index.js +4 -2
  43. package/cjs/index.js.map +1 -1
  44. package/cjs/overlay/dismiss/DismissableLayer.d.ts +70 -0
  45. package/cjs/overlay/dismiss/DismissableLayer.js +253 -0
  46. package/cjs/overlay/dismiss/DismissableLayer.js.map +1 -0
  47. package/cjs/overlay/dismiss/util/dispatchCustomEvent.d.ts +50 -0
  48. package/cjs/overlay/dismiss/util/dispatchCustomEvent.js +65 -0
  49. package/cjs/overlay/dismiss/util/dispatchCustomEvent.js.map +1 -0
  50. package/cjs/overlay/dismiss/util/useEscapeKeydown.d.ts +1 -0
  51. package/cjs/overlay/dismiss/util/useEscapeKeydown.js +19 -0
  52. package/cjs/overlay/dismiss/util/useEscapeKeydown.js.map +1 -0
  53. package/cjs/overlay/dismiss/util/useFocusOutside.d.ts +8 -0
  54. package/cjs/overlay/dismiss/util/useFocusOutside.js +42 -0
  55. package/cjs/overlay/dismiss/util/useFocusOutside.js.map +1 -0
  56. package/cjs/overlay/dismiss/util/usePointerDownOutside.d.ts +10 -0
  57. package/cjs/overlay/dismiss/util/usePointerDownOutside.js +84 -0
  58. package/cjs/overlay/dismiss/util/usePointerDownOutside.js.map +1 -0
  59. package/cjs/overlays/floating/Floating.d.ts +53 -0
  60. package/cjs/overlays/floating/Floating.js +215 -0
  61. package/cjs/overlays/floating/Floating.js.map +1 -0
  62. package/cjs/overlays/floating/Floating.utils.d.ts +18 -0
  63. package/cjs/overlays/floating/Floating.utils.js +52 -0
  64. package/cjs/overlays/floating/Floating.utils.js.map +1 -0
  65. package/cjs/popover/Popover.js +13 -28
  66. package/cjs/popover/Popover.js.map +1 -1
  67. package/cjs/progress-bar/ProgressBar.d.ts +72 -0
  68. package/cjs/progress-bar/ProgressBar.js +86 -0
  69. package/cjs/progress-bar/ProgressBar.js.map +1 -0
  70. package/cjs/progress-bar/index.d.ts +1 -0
  71. package/cjs/progress-bar/index.js +10 -0
  72. package/cjs/progress-bar/index.js.map +1 -0
  73. package/cjs/tabs/Tabs.context.d.ts +7 -3
  74. package/cjs/tabs/Tabs.context.js +1 -0
  75. package/cjs/tabs/Tabs.context.js.map +1 -1
  76. package/cjs/timeline/AxisLabels.d.ts +1 -1
  77. package/cjs/toggle-group/ToggleGroup.context.d.ts +7 -3
  78. package/cjs/toggle-group/ToggleGroup.context.js +1 -0
  79. package/cjs/toggle-group/ToggleGroup.context.js.map +1 -1
  80. package/cjs/typography/BodyLong.d.ts +7 -7
  81. package/cjs/typography/BodyLong.js +5 -5
  82. package/cjs/typography/BodyShort.d.ts +5 -5
  83. package/cjs/typography/BodyShort.js +3 -3
  84. package/cjs/typography/Detail.d.ts +5 -5
  85. package/cjs/typography/Detail.js +3 -3
  86. package/cjs/typography/ErrorMessage.d.ts +5 -5
  87. package/cjs/typography/ErrorMessage.js +3 -3
  88. package/cjs/typography/Heading.d.ts +6 -6
  89. package/cjs/typography/Heading.js +3 -3
  90. package/cjs/typography/Label.d.ts +5 -5
  91. package/cjs/typography/Label.js +3 -3
  92. package/cjs/typography/types.d.ts +3 -3
  93. package/cjs/util/hooks/descendants/useDescendant.d.ts +2 -2
  94. package/cjs/util/hooks/descendants/useDescendant.js +49 -52
  95. package/cjs/util/hooks/descendants/useDescendant.js.map +1 -1
  96. package/cjs/util/types/AsChild.d.ts +14 -0
  97. package/cjs/util/types/AsChild.js +3 -0
  98. package/cjs/util/types/AsChild.js.map +1 -0
  99. package/esm/chat/Chat.d.ts +2 -1
  100. package/esm/chat/Chat.js +1 -0
  101. package/esm/chat/Chat.js.map +1 -1
  102. package/esm/date/datepicker/parts/DropdownCaption.js +1 -1
  103. package/esm/date/datepicker/parts/DropdownCaption.js.map +1 -1
  104. package/esm/date/monthpicker/MonthCaption.js +1 -1
  105. package/esm/date/utils/labels.d.ts +2 -2
  106. package/esm/form/ReadOnlyIcon.d.ts +2 -2
  107. package/esm/form/combobox/Combobox.js +8 -23
  108. package/esm/form/combobox/Combobox.js.map +1 -1
  109. package/esm/form/combobox/ComboboxProvider.js +1 -1
  110. package/esm/form/combobox/ComboboxProvider.js.map +1 -1
  111. package/esm/form/combobox/ComboboxWrapper.d.ts +1 -2
  112. package/esm/form/combobox/ComboboxWrapper.js +4 -2
  113. package/esm/form/combobox/ComboboxWrapper.js.map +1 -1
  114. package/esm/form/combobox/FilteredOptions/FilteredOptions.js +3 -3
  115. package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  116. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.d.ts +4 -4
  117. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +15 -16
  118. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  119. package/esm/form/combobox/Input/{inputContext.d.ts → Input.context.d.ts} +7 -5
  120. package/esm/form/combobox/Input/{inputContext.js → Input.context.js} +22 -21
  121. package/esm/form/combobox/Input/Input.context.js.map +1 -0
  122. package/esm/form/combobox/Input/Input.js +1 -1
  123. package/esm/form/combobox/Input/Input.js.map +1 -1
  124. package/esm/form/combobox/Input/InputController.d.ts +3 -0
  125. package/esm/form/combobox/Input/InputController.js +41 -0
  126. package/esm/form/combobox/Input/InputController.js.map +1 -0
  127. package/esm/form/combobox/{ToggleListButton.js → Input/ToggleListButton.js} +1 -1
  128. package/esm/form/combobox/Input/ToggleListButton.js.map +1 -0
  129. package/esm/form/combobox/SelectedOptions/SelectedOptions.js +1 -1
  130. package/esm/form/combobox/SelectedOptions/SelectedOptions.js.map +1 -1
  131. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.d.ts +4 -4
  132. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js +9 -14
  133. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js.map +1 -1
  134. package/esm/form/combobox/customOptionsContext.d.ts +4 -4
  135. package/esm/form/combobox/customOptionsContext.js +10 -12
  136. package/esm/form/combobox/customOptionsContext.js.map +1 -1
  137. package/esm/form/combobox/types.d.ts +1 -1
  138. package/esm/help-text/HelpTextIcon.d.ts +1 -1
  139. package/esm/index.d.ts +1 -0
  140. package/esm/index.js +1 -0
  141. package/esm/index.js.map +1 -1
  142. package/esm/overlay/dismiss/DismissableLayer.d.ts +70 -0
  143. package/esm/overlay/dismiss/DismissableLayer.js +226 -0
  144. package/esm/overlay/dismiss/DismissableLayer.js.map +1 -0
  145. package/esm/overlay/dismiss/util/dispatchCustomEvent.d.ts +50 -0
  146. package/esm/overlay/dismiss/util/dispatchCustomEvent.js +58 -0
  147. package/esm/overlay/dismiss/util/dispatchCustomEvent.js.map +1 -0
  148. package/esm/overlay/dismiss/util/useEscapeKeydown.d.ts +1 -0
  149. package/esm/overlay/dismiss/util/useEscapeKeydown.js +15 -0
  150. package/esm/overlay/dismiss/util/useEscapeKeydown.js.map +1 -0
  151. package/esm/overlay/dismiss/util/useFocusOutside.d.ts +8 -0
  152. package/esm/overlay/dismiss/util/useFocusOutside.js +38 -0
  153. package/esm/overlay/dismiss/util/useFocusOutside.js.map +1 -0
  154. package/esm/overlay/dismiss/util/usePointerDownOutside.d.ts +10 -0
  155. package/esm/overlay/dismiss/util/usePointerDownOutside.js +80 -0
  156. package/esm/overlay/dismiss/util/usePointerDownOutside.js.map +1 -0
  157. package/esm/overlays/floating/Floating.d.ts +53 -0
  158. package/esm/overlays/floating/Floating.js +188 -0
  159. package/esm/overlays/floating/Floating.js.map +1 -0
  160. package/esm/overlays/floating/Floating.utils.d.ts +18 -0
  161. package/esm/overlays/floating/Floating.utils.js +48 -0
  162. package/esm/overlays/floating/Floating.utils.js.map +1 -0
  163. package/esm/popover/Popover.js +16 -31
  164. package/esm/popover/Popover.js.map +1 -1
  165. package/esm/progress-bar/ProgressBar.d.ts +72 -0
  166. package/esm/progress-bar/ProgressBar.js +57 -0
  167. package/esm/progress-bar/ProgressBar.js.map +1 -0
  168. package/esm/progress-bar/index.d.ts +1 -0
  169. package/esm/progress-bar/index.js +3 -0
  170. package/esm/progress-bar/index.js.map +1 -0
  171. package/esm/tabs/Tabs.context.d.ts +7 -3
  172. package/esm/tabs/Tabs.context.js +1 -0
  173. package/esm/tabs/Tabs.context.js.map +1 -1
  174. package/esm/timeline/AxisLabels.d.ts +1 -1
  175. package/esm/toggle-group/ToggleGroup.context.d.ts +7 -3
  176. package/esm/toggle-group/ToggleGroup.context.js +1 -0
  177. package/esm/toggle-group/ToggleGroup.context.js.map +1 -1
  178. package/esm/typography/BodyLong.d.ts +7 -7
  179. package/esm/typography/BodyLong.js +5 -5
  180. package/esm/typography/BodyShort.d.ts +5 -5
  181. package/esm/typography/BodyShort.js +3 -3
  182. package/esm/typography/Detail.d.ts +5 -5
  183. package/esm/typography/Detail.js +3 -3
  184. package/esm/typography/ErrorMessage.d.ts +5 -5
  185. package/esm/typography/ErrorMessage.js +3 -3
  186. package/esm/typography/Heading.d.ts +6 -6
  187. package/esm/typography/Heading.js +3 -3
  188. package/esm/typography/Label.d.ts +5 -5
  189. package/esm/typography/Label.js +3 -3
  190. package/esm/typography/types.d.ts +3 -3
  191. package/esm/util/hooks/descendants/useDescendant.d.ts +2 -2
  192. package/esm/util/hooks/descendants/useDescendant.js +49 -52
  193. package/esm/util/hooks/descendants/useDescendant.js.map +1 -1
  194. package/esm/util/types/AsChild.d.ts +14 -0
  195. package/esm/util/types/AsChild.js +2 -0
  196. package/esm/util/types/AsChild.js.map +1 -0
  197. package/package.json +16 -5
  198. package/src/chat/Chat.tsx +2 -1
  199. package/src/date/datepicker/parts/DropdownCaption.tsx +5 -1
  200. package/src/date/monthpicker/MonthCaption.tsx +1 -1
  201. package/src/form/combobox/Combobox.tsx +6 -76
  202. package/src/form/combobox/ComboboxProvider.tsx +1 -1
  203. package/src/form/combobox/ComboboxWrapper.tsx +4 -3
  204. package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +3 -3
  205. package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +19 -29
  206. package/src/form/combobox/Input/{inputContext.tsx → Input.context.tsx} +30 -33
  207. package/src/form/combobox/Input/Input.tsx +1 -1
  208. package/src/form/combobox/Input/InputController.tsx +102 -0
  209. package/src/form/combobox/{ToggleListButton.tsx → Input/ToggleListButton.tsx} +1 -1
  210. package/src/form/combobox/SelectedOptions/SelectedOptions.tsx +1 -1
  211. package/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx +12 -26
  212. package/src/form/combobox/{combobox-utils.test.ts → __tests__/combobox-utils.test.ts} +1 -1
  213. package/src/form/combobox/{combobox.test.tsx → __tests__/combobox.test.tsx} +2 -3
  214. package/src/form/combobox/customOptionsContext.tsx +14 -18
  215. package/src/form/combobox/types.ts +3 -1
  216. package/src/index.ts +1 -0
  217. package/src/overlay/README.md +5 -0
  218. package/src/overlay/dismiss/DismissableLayer.tsx +368 -0
  219. package/src/overlay/dismiss/util/dispatchCustomEvent.ts +77 -0
  220. package/src/overlay/dismiss/util/useEscapeKeydown.ts +21 -0
  221. package/src/overlay/dismiss/util/useFocusOutside.ts +52 -0
  222. package/src/overlay/dismiss/util/usePointerDownOutside.ts +95 -0
  223. package/src/overlays/floating/Floating.tsx +399 -0
  224. package/src/overlays/floating/Floating.utils.ts +63 -0
  225. package/src/popover/Popover.tsx +38 -70
  226. package/src/progress-bar/ProgressBar.tsx +149 -0
  227. package/src/progress-bar/index.ts +2 -0
  228. package/src/tabs/Tabs.context.ts +2 -0
  229. package/src/toggle-group/ToggleGroup.context.ts +1 -0
  230. package/src/typography/BodyLong.tsx +7 -7
  231. package/src/typography/BodyShort.tsx +5 -5
  232. package/src/typography/Detail.tsx +5 -5
  233. package/src/typography/ErrorMessage.tsx +5 -5
  234. package/src/typography/Heading.tsx +6 -6
  235. package/src/typography/Label.tsx +5 -5
  236. package/src/typography/types.ts +3 -3
  237. package/src/util/hooks/descendants/useDescendant.tsx +55 -68
  238. package/src/util/types/AsChild.ts +15 -0
  239. package/cjs/form/combobox/ClearButton.d.ts +0 -7
  240. package/cjs/form/combobox/ClearButton.js +0 -28
  241. package/cjs/form/combobox/ClearButton.js.map +0 -1
  242. package/cjs/form/combobox/FilteredOptions/CheckIcon.d.ts +0 -3
  243. package/cjs/form/combobox/FilteredOptions/CheckIcon.js +0 -12
  244. package/cjs/form/combobox/FilteredOptions/CheckIcon.js.map +0 -1
  245. package/cjs/form/combobox/Input/inputContext.js.map +0 -1
  246. package/cjs/form/combobox/ToggleListButton.js.map +0 -1
  247. package/esm/form/combobox/ClearButton.d.ts +0 -7
  248. package/esm/form/combobox/ClearButton.js +0 -21
  249. package/esm/form/combobox/ClearButton.js.map +0 -1
  250. package/esm/form/combobox/FilteredOptions/CheckIcon.d.ts +0 -3
  251. package/esm/form/combobox/FilteredOptions/CheckIcon.js +0 -7
  252. package/esm/form/combobox/FilteredOptions/CheckIcon.js.map +0 -1
  253. package/esm/form/combobox/Input/inputContext.js.map +0 -1
  254. package/esm/form/combobox/ToggleListButton.js.map +0 -1
  255. package/src/form/combobox/ClearButton.tsx +0 -29
  256. package/src/form/combobox/FilteredOptions/CheckIcon.tsx +0 -23
  257. /package/cjs/form/combobox/{ToggleListButton.d.ts → Input/ToggleListButton.d.ts} +0 -0
  258. /package/esm/form/combobox/{ToggleListButton.d.ts → Input/ToggleListButton.d.ts} +0 -0
@@ -1,17 +1,12 @@
1
- import React, {
2
- createContext,
3
- useCallback,
4
- useContext,
5
- useMemo,
6
- useState,
7
- } from "react";
1
+ import React, { useCallback, useMemo, useState } from "react";
2
+ import { createContext } from "../../../util/create-context";
8
3
  import { usePrevious } from "../../../util/hooks";
9
- import { useInputContext } from "../Input/inputContext";
4
+ import { useInputContext } from "../Input/Input.context";
10
5
  import { isInList } from "../combobox-utils";
11
- import { useCustomOptionsContext } from "../customOptionsContext";
6
+ import { useComboboxCustomOptions } from "../customOptionsContext";
12
7
  import { ComboboxOption, ComboboxProps, MaxSelected } from "../types";
13
8
 
14
- type SelectedOptionsContextType = {
9
+ type SelectedOptionsContextValue = {
15
10
  addSelectedOption: (option: ComboboxOption) => void;
16
11
  isMultiSelect?: boolean;
17
12
  removeSelectedOption: (option: ComboboxOption) => void;
@@ -25,11 +20,10 @@ type SelectedOptionsContextType = {
25
20
  ) => void;
26
21
  };
27
22
 
28
- const SelectedOptionsContext = createContext<SelectedOptionsContextType>(
29
- {} as SelectedOptionsContextType,
30
- );
23
+ const [SelectedOptionsContextProvider, useSelectedOptionsContext] =
24
+ createContext<SelectedOptionsContextValue>();
31
25
 
32
- export const SelectedOptionsProvider = ({
26
+ const SelectedOptionsProvider = ({
33
27
  children,
34
28
  value,
35
29
  }: {
@@ -45,7 +39,7 @@ export const SelectedOptionsProvider = ({
45
39
  removeCustomOption,
46
40
  addCustomOption,
47
41
  setCustomOptions,
48
- } = useCustomOptionsContext();
42
+ } = useComboboxCustomOptions();
49
43
  const {
50
44
  allowNewValues,
51
45
  isMultiSelect,
@@ -149,18 +143,10 @@ export const SelectedOptionsProvider = ({
149
143
  };
150
144
 
151
145
  return (
152
- <SelectedOptionsContext.Provider value={selectedOptionsState}>
146
+ <SelectedOptionsContextProvider {...selectedOptionsState}>
153
147
  {children}
154
- </SelectedOptionsContext.Provider>
148
+ </SelectedOptionsContextProvider>
155
149
  );
156
150
  };
157
151
 
158
- export const useSelectedOptionsContext = () => {
159
- const context = useContext(SelectedOptionsContext);
160
- if (!context) {
161
- throw new Error(
162
- "useSelectedOptionsContext must be used within a SelectedOptionsProvider",
163
- );
164
- }
165
- return context;
166
- };
152
+ export { SelectedOptionsProvider, useSelectedOptionsContext };
@@ -2,7 +2,7 @@ import {
2
2
  isInList,
3
3
  mapToComboboxOptionArray,
4
4
  toComboboxOption,
5
- } from "./combobox-utils";
5
+ } from "../combobox-utils";
6
6
 
7
7
  const list = [
8
8
  { label: "Hjelpemidler", value: "HJE" },
@@ -1,10 +1,9 @@
1
1
  /* eslint-disable testing-library/no-unnecessary-act -- https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning */
2
- import { render, screen } from "@testing-library/react";
2
+ import { act, render, screen } from "@testing-library/react";
3
3
  import userEvent from "@testing-library/user-event";
4
4
  import React, { useId } from "react";
5
- import { act } from "react-dom/test-utils";
6
5
  import { describe, expect, test, vi } from "vitest";
7
- import { UNSAFE_Combobox } from "./index";
6
+ import { UNSAFE_Combobox } from "../index";
8
7
 
9
8
  const options = [
10
9
  "banana",
@@ -1,19 +1,23 @@
1
- import React, { createContext, useCallback, useContext, useState } from "react";
2
- import { useInputContext } from "./Input/inputContext";
1
+ import React, { useCallback, useState } from "react";
2
+ import { createContext } from "../../util/create-context";
3
+ import { useInputContext } from "./Input/Input.context";
3
4
  import { ComboboxOption } from "./types";
4
5
 
5
- type CustomOptionsContextType = {
6
+ type CustomOptionsContextValue = {
6
7
  customOptions: ComboboxOption[];
7
8
  removeCustomOption: (option: ComboboxOption) => void;
8
9
  addCustomOption: (option: ComboboxOption) => void;
9
10
  setCustomOptions: React.Dispatch<React.SetStateAction<ComboboxOption[]>>;
10
11
  };
11
12
 
12
- const CustomOptionsContext = createContext<CustomOptionsContextType>(
13
- {} as CustomOptionsContextType,
14
- );
13
+ const [ComboboxCustomOptionsProvider, useComboboxCustomOptions] =
14
+ createContext<CustomOptionsContextValue>({
15
+ name: "ComboboxCustomOptions",
16
+ errorMessage:
17
+ "useComboboxCustomOptions must be used within a ComboboxCustomOptionsProvider",
18
+ });
15
19
 
16
- export const CustomOptionsProvider = ({
20
+ const CustomOptionsProvider = ({
17
21
  children,
18
22
  value,
19
23
  }: {
@@ -54,18 +58,10 @@ export const CustomOptionsProvider = ({
54
58
  };
55
59
 
56
60
  return (
57
- <CustomOptionsContext.Provider value={customOptionsState}>
61
+ <ComboboxCustomOptionsProvider {...customOptionsState}>
58
62
  {children}
59
- </CustomOptionsContext.Provider>
63
+ </ComboboxCustomOptionsProvider>
60
64
  );
61
65
  };
62
66
 
63
- export const useCustomOptionsContext = () => {
64
- const context = useContext(CustomOptionsContext);
65
- if (!context) {
66
- throw new Error(
67
- "useCustomOptionsContext must be used within a CustomOptionsProvider",
68
- );
69
- }
70
- return context;
71
- };
67
+ export { CustomOptionsProvider, useComboboxCustomOptions };
@@ -100,7 +100,9 @@ export interface ComboboxProps
100
100
  *
101
101
  * @param event
102
102
  */
103
- onClear?: (event: React.PointerEvent | React.KeyboardEvent) => void;
103
+ onClear?: (
104
+ event: React.PointerEvent | React.KeyboardEvent | React.MouseEvent,
105
+ ) => void;
104
106
  /**
105
107
  * Callback function triggered whenever an option is selected or de-selected.
106
108
  *
package/src/index.ts CHANGED
@@ -59,6 +59,7 @@ export { Modal, type ModalProps } from "./modal";
59
59
  export { Pagination, type PaginationProps } from "./pagination";
60
60
  export { Popover, type PopoverProps } from "./popover";
61
61
  export { Portal, type PortalProps } from "./portal";
62
+ export { ProgressBar, type ProgressBarProps } from "./progress-bar";
62
63
  export { Provider, type ProviderProps } from "./provider";
63
64
  export { ReadMore, type ReadMoreProps } from "./read-more";
64
65
  export { Skeleton, type SkeletonProps } from "./skeleton";
@@ -0,0 +1,5 @@
1
+ # DismissableLayer API
2
+
3
+ `DismissableLayer`-API provides a robust handler for layers that can be dismissed by interacting outside of it.
4
+
5
+ One of the benefits is its ability to handle nested elements, relevant when creating nested dropdowns and similar solutions.
@@ -0,0 +1,368 @@
1
+ import React, {
2
+ CSSProperties,
3
+ forwardRef,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+ import { Slot } from "../../util/Slot";
10
+ import { useMergeRefs } from "../../util/hooks";
11
+ import { createDescendantContext } from "../../util/hooks/descendants/useDescendant";
12
+ import { AsChild } from "../../util/types/AsChild";
13
+ import {
14
+ CustomFocusEvent,
15
+ CustomPointerDownEvent,
16
+ } from "./util/dispatchCustomEvent";
17
+ import { useEscapeKeydown } from "./util/useEscapeKeydown";
18
+ import { useFocusOutside } from "./util/useFocusOutside";
19
+ import { usePointerDownOutside } from "./util/usePointerDownOutside";
20
+
21
+ interface DismissableLayerBaseProps {
22
+ /**
23
+ * When `true`, hover/focus/click interactions will be disabled on elements outside
24
+ * the `DismissableLayer`. Users will need to click twice on outside elements to
25
+ * interact with them: once to close the `DismissableLayer`, and again to trigger the element.
26
+ */
27
+ disableOutsidePointerEvents?: boolean;
28
+ /**
29
+ * Event handler called when the escape key is down.
30
+ * Can be prevented.
31
+ */
32
+ onEscapeKeyDown?: (event: KeyboardEvent) => void;
33
+ /**
34
+ * Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`.
35
+ * Can be prevented.
36
+ */
37
+ onPointerDownOutside?: (event: CustomPointerDownEvent) => void;
38
+ /**
39
+ * Event handler called when the focus moves outside of the `DismissableLayer`.
40
+ * Can be prevented.
41
+ */
42
+ onFocusOutside?: (event: CustomFocusEvent) => void;
43
+ /**
44
+ * Event handler called when an interaction happens outside the `DismissableLayer`.
45
+ * Specifically, when a `pointerdown` event happens outside or focus moves outside of it.
46
+ * Can be prevented.
47
+ */
48
+ onInteractOutside?: (
49
+ event: CustomPointerDownEvent | CustomFocusEvent,
50
+ ) => void;
51
+ /**
52
+ * Handler called when the `DismissableLayer` should be dismissed
53
+ */
54
+ onDismiss?: () => void;
55
+ /**
56
+ * Stops `onDismiss` from beeing called when interacting with the `safeZone` elements.
57
+ * `safeZone.dismissable` is only needed when its element does not have a `tabIndex` since it will not receive focus-events.
58
+ */
59
+ safeZone?: {
60
+ anchor?: Element | null;
61
+ dismissable?: Element | null;
62
+ };
63
+
64
+ style?: CSSProperties;
65
+ /**
66
+ * Disables layer from beeing counted in context for nested `DismissableLayer`.
67
+ */
68
+ enabled?: boolean;
69
+ }
70
+
71
+ type DismissableLayerProps = DismissableLayerBaseProps & AsChild;
72
+
73
+ export const [
74
+ DismissableDescendantsProvider,
75
+ useDismissableDescendantsContext,
76
+ useDismissableDescendants,
77
+ useDismissableDescendant,
78
+ ] = createDescendantContext<
79
+ HTMLDivElement,
80
+ { disableOutsidePointerEvents: boolean }
81
+ >();
82
+
83
+ let originalBodyPointerEvents: string;
84
+
85
+ const DismissableLayer = forwardRef<HTMLDivElement, DismissableLayerProps>(
86
+ (props: DismissableLayerProps, ref) => {
87
+ const context = useDismissableDescendantsContext(false);
88
+
89
+ /**
90
+ * To correctly handle nested DismissableLayer,
91
+ * we only initialize the `Descendants`-API for the root layer to aboid resetting context
92
+ */
93
+ return context ? (
94
+ <DismissableLayerNode ref={ref} {...props} />
95
+ ) : (
96
+ <DismissableRoot>
97
+ <DismissableLayerNode ref={ref} {...props} />
98
+ </DismissableRoot>
99
+ );
100
+ },
101
+ );
102
+
103
+ /**
104
+ * DismissableRoot
105
+ *
106
+ * Used to initialize the `Descendants`-API at the root layer.
107
+ * All subsequent layers will use the same context.
108
+ */
109
+ const DismissableRoot = ({ children }: { children: React.ReactNode }) => {
110
+ const descendants = useDismissableDescendants();
111
+
112
+ return (
113
+ <DismissableDescendantsProvider value={descendants}>
114
+ {children}
115
+ </DismissableDescendantsProvider>
116
+ );
117
+ };
118
+
119
+ const DismissableLayerNode = forwardRef<HTMLDivElement, DismissableLayerProps>(
120
+ (
121
+ {
122
+ children,
123
+ asChild,
124
+ onEscapeKeyDown,
125
+ onPointerDownOutside,
126
+ onFocusOutside,
127
+ onInteractOutside,
128
+ onDismiss,
129
+ safeZone,
130
+ disableOutsidePointerEvents = false,
131
+ enabled = true,
132
+ ...rest
133
+ }: DismissableLayerProps,
134
+ ref,
135
+ ) => {
136
+ const { register, index, descendants } = useDismissableDescendant({
137
+ disableOutsidePointerEvents,
138
+ disabled: !enabled,
139
+ });
140
+
141
+ /**
142
+ * `node` will be set to the ref of the component or nested component
143
+ * Ex: If
144
+ * ```
145
+ * <DismissableLayer asChild>
146
+ * <Popover />
147
+ * </DismissableLayer>
148
+ * ```
149
+ * `node` will in this case be the Popover-element.
150
+ * We use State her and not ref since we want to trigger a rerender when the node changes.
151
+ */
152
+ const [node, setNode] = useState<HTMLDivElement | null>(null);
153
+
154
+ const mergedRefs = useMergeRefs(setNode, register, ref);
155
+
156
+ /**
157
+ * In some cases the `node.ownerDocument` can differ from global document.
158
+ * This can happend when portaling elements or using web-components
159
+ */
160
+ const ownerDocument = node?.ownerDocument ?? globalThis?.document;
161
+
162
+ const hasInteractedOutsideRef = useRef(false);
163
+ const hasPointerDownOutsideRef = useRef(false);
164
+
165
+ const pointerEnabled = useMemo(() => {
166
+ let lastIndex = -1;
167
+
168
+ const descendantNodes = descendants.enabledValues();
169
+ descendantNodes.forEach((obj, _index) => {
170
+ if (obj.disableOutsidePointerEvents) {
171
+ lastIndex = _index;
172
+ }
173
+ });
174
+
175
+ return {
176
+ /**
177
+ * Makes sure we stop events at the highest layer with pointer events disabled.
178
+ * If not checked, we risk closing every layer when clicking outside the layer.
179
+ */
180
+ isPointerEventsEnabled: index >= lastIndex,
181
+ /**
182
+ * If we find a node with `disableOutsidePointerEvents` we want to disable pointer events on the body.
183
+ */
184
+ isBodyPointerEventsDisabled: lastIndex > -1,
185
+ };
186
+ }, [descendants, index]);
187
+
188
+ /**
189
+ * We want to prevent the Layer from closing when the trigger, anchor element, or its child elements are interacted with.
190
+ *
191
+ * To achieve this, we check if the event target is the trigger, anchor or a child. If it is, we prevent default event behavior.
192
+ *
193
+ * The `pointerDownOutside` and `focusOutside` handlers already check if the event target is within the DismissableLayer (`node`).
194
+ * However, since we don't add a `tabIndex` to the Popover/Tooltip, the `focusOutside` handler doesn't correctly handle focus events.
195
+ * Therefore, we also need to check that neither the trigger (`anchor`) nor the DismissableLayer (`dismissable`) are the event targets.
196
+ */
197
+ function handleOutsideEvent(
198
+ event: CustomFocusEvent | CustomPointerDownEvent,
199
+ ) {
200
+ if ((!safeZone?.anchor && !safeZone?.dismissable) || !enabled) {
201
+ return;
202
+ }
203
+
204
+ if (!event.defaultPrevented) {
205
+ hasInteractedOutsideRef.current = true;
206
+ if (event.detail.originalEvent.type === "pointerdown") {
207
+ hasPointerDownOutsideRef.current = true;
208
+ }
209
+ }
210
+
211
+ const target = event.target as HTMLElement;
212
+
213
+ /**
214
+ * pointerdown-events works as expected, but focus-events does not.
215
+ * For focus-event we need to also check `safeZone.dismissable` (the Popover/Tooltip itself) since it does not have a tabIndex.
216
+ */
217
+ if (event.detail.originalEvent.type === "pointerdown") {
218
+ const targetIsTrigger =
219
+ safeZone?.anchor?.contains(target) || target === safeZone?.anchor;
220
+ targetIsTrigger && event.preventDefault();
221
+ } else {
222
+ const targetIsNotTrigger =
223
+ target instanceof HTMLElement &&
224
+ ![safeZone?.anchor, safeZone?.dismissable].some(
225
+ (element) => element?.contains(target as Node),
226
+ ) &&
227
+ !target.contains(safeZone?.dismissable ?? null);
228
+
229
+ !targetIsNotTrigger && event.preventDefault();
230
+ }
231
+
232
+ /**
233
+ * In Safari, if the trigger element is inside a container with tabIndex={0}, a click on the trigger
234
+ * will first fire a 'pointerdownoutside' event on the trigger itself. However, it will then fire a
235
+ * 'focusoutside' event on the container.
236
+ *
237
+ * To handle this, we ignore any 'focusoutside' events if a 'pointerdownoutside' event has already occurred.
238
+ * 'pointerdownoutside' event is sufficient to indicate interaction outside the DismissableLayer.
239
+ */
240
+ if (
241
+ event.detail.originalEvent.type === "focusin" &&
242
+ hasPointerDownOutsideRef.current
243
+ ) {
244
+ event.preventDefault();
245
+ }
246
+ hasPointerDownOutsideRef.current = false;
247
+ hasInteractedOutsideRef.current = false;
248
+ }
249
+
250
+ const pointerDownOutside = usePointerDownOutside((event) => {
251
+ if (!pointerEnabled.isPointerEventsEnabled || !enabled) {
252
+ return;
253
+ }
254
+
255
+ /**
256
+ * We call these before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault based certain cases.
257
+ */
258
+ onPointerDownOutside?.(event);
259
+ onInteractOutside?.(event);
260
+
261
+ /**
262
+ * Add safeZone to prevent closing when interacting with trigger/anchor or its children.
263
+ */
264
+ safeZone && handleOutsideEvent(event);
265
+
266
+ /**
267
+ * Both `onPointerDownOutside` and `onInteractOutside` are able to preventDefault the event, thus stopping call for `onDismiss`.
268
+ */
269
+ if (!event.defaultPrevented && onDismiss) {
270
+ onDismiss();
271
+ }
272
+ }, ownerDocument);
273
+
274
+ const focusOutside = useFocusOutside((event) => {
275
+ if (!enabled) {
276
+ return;
277
+ }
278
+
279
+ /**
280
+ * We call these before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault based certain cases.
281
+ */
282
+ onFocusOutside?.(event);
283
+ onInteractOutside?.(event);
284
+
285
+ /**
286
+ * Add safeZone to prevent closing when interacting with trigger/anchor or its children.
287
+ */
288
+ safeZone && handleOutsideEvent(event);
289
+
290
+ /**
291
+ * Both `onFocusOutside` and `onInteractOutside` are able to preventDefault the event, thus stopping call for `onDismiss`.
292
+ */
293
+ if (!event.defaultPrevented && onDismiss) {
294
+ onDismiss();
295
+ }
296
+ }, ownerDocument);
297
+
298
+ useEscapeKeydown((event) => {
299
+ if (!enabled) {
300
+ return;
301
+ }
302
+ /**
303
+ * The deepest nested element will always be last in the descendants list.
304
+ * This allows us to only close the highest layer when pressing escape.
305
+ *
306
+ * In some cases a layer might still exist, but be disabled. We want to ignore these layers.
307
+ */
308
+ const isHighestLayer = index === descendants.enabledCount() - 1;
309
+ if (!isHighestLayer) {
310
+ return;
311
+ }
312
+
313
+ /**
314
+ * We call this before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault based certain cases.
315
+ */
316
+ onEscapeKeyDown?.(event);
317
+ /**
318
+ * `onEscapeKeyDown` is able to preventDefault the event, thus stopping call for `onDismiss`.
319
+ * We want to `preventDefault` the escape-event to avoid sideeffect from other elements on screen
320
+ */
321
+ if (!event.defaultPrevented && onDismiss) {
322
+ event.preventDefault();
323
+ onDismiss();
324
+ }
325
+ }, ownerDocument);
326
+
327
+ /**
328
+ * If `disableOutsidePointerEvents` is true,
329
+ * we want to disable pointer events on the body when the first layer is opened.
330
+ */
331
+ useEffect(() => {
332
+ if (!node || !disableOutsidePointerEvents || index !== 0 || !enabled) {
333
+ return;
334
+ }
335
+
336
+ originalBodyPointerEvents = ownerDocument.body.style.pointerEvents;
337
+ ownerDocument.body.style.pointerEvents = "none";
338
+
339
+ return () => {
340
+ ownerDocument.body.style.pointerEvents = originalBodyPointerEvents;
341
+ };
342
+ }, [node, ownerDocument, disableOutsidePointerEvents, index, enabled]);
343
+
344
+ const Comp = asChild ? Slot : "div";
345
+
346
+ return (
347
+ <Comp
348
+ ref={mergedRefs}
349
+ {...rest}
350
+ onFocusCapture={focusOutside.onFocusCapture}
351
+ onBlurCapture={focusOutside.onBlurCapture}
352
+ onPointerDownCapture={pointerDownOutside.onPointerDownCapture}
353
+ style={{
354
+ pointerEvents: pointerEnabled.isBodyPointerEventsDisabled
355
+ ? pointerEnabled.isPointerEventsEnabled
356
+ ? "auto"
357
+ : "none"
358
+ : undefined,
359
+ ...rest.style,
360
+ }}
361
+ >
362
+ {children}
363
+ </Comp>
364
+ );
365
+ },
366
+ );
367
+
368
+ export { DismissableLayer, type DismissableLayerProps };
@@ -0,0 +1,77 @@
1
+ import ReactDOM from "react-dom";
2
+
3
+ type CustomFocusEvent = CustomEvent<{ originalEvent: FocusEvent }>;
4
+ type CustomPointerDownEvent = CustomEvent<{
5
+ originalEvent: PointerEvent;
6
+ }>;
7
+
8
+ export { CustomFocusEvent, CustomPointerDownEvent };
9
+
10
+ export const CUSTOM_EVENTS = {
11
+ FOCUS_OUTSIDE: "AKSEL_FOCUS_OUTSIDE",
12
+ POINTER_DOWN_OUTSIDE: "AKSEL_POINTER_DOWN_OUTSIDE",
13
+ };
14
+
15
+ /**
16
+ * Use of `discrete` flushes custom event dispatch. This is to mimic the behavior React has for `discrete` events.
17
+ * https://github.com/facebook/react/blob/a8a4742f1c54493df00da648a3f9d26e3db9c8b5/packages/react-dom/src/events/ReactDOMEventListener.js#L318
18
+ *
19
+ * React batches *all* event handlers since version 18, this introduces certain considerations when using custom event types.
20
+ *
21
+ * Internally, React prioritises events in the following order:
22
+ * - discrete
23
+ * - continuous
24
+ * - default
25
+ *
26
+ * `discrete` is an important distinction as updates within these events are applied immediately.
27
+ * React however, is not able to infer the priority of custom event types due to how they are detected internally.
28
+ * Because of this, it's possible for updates from custom events to be unexpectedly batched when
29
+ * dispatched by another `discrete` event.
30
+ *
31
+ * In order to ensure that updates from custom events are applied predictably, we need to manually flush the batch.
32
+ * This utility should be used when dispatching a custom event from within another `discrete` event, this utility
33
+ * is not nessesary when dispatching known event types, or if dispatching a custom type inside a non-discrete event.
34
+ * For example:
35
+ *
36
+ * dispatching a known click 👎
37
+ * target.dispatchEvent(new Event(‘click’))
38
+ *
39
+ * dispatching a custom type within a non-discrete event 👎
40
+ * onScroll={(event) => event.target.dispatchEvent(new CustomEvent(‘customType’))}
41
+ *
42
+ * dispatching a custom type within a `discrete` event 👍
43
+ * onPointerDown={(event) => dispatchDiscreteCustomEvent(event.target, new CustomEvent(‘customType’))}
44
+ *
45
+ * Note: though React classifies `focus`, `focusin` and `focusout` events as `discrete`, it's not recommended to use
46
+ * this utility with them. This is because it's possible for those handlers to be called implicitly during render
47
+ * e.g. when focus is within a component as it is unmounted, or when managing focus on mount.
48
+ */
49
+ export function dispatchCustomEvent<
50
+ E extends CustomEvent,
51
+ OriginalEvent extends Event,
52
+ >(
53
+ name: string,
54
+ handler: ((event: E) => void) | undefined,
55
+ detail: { originalEvent: OriginalEvent } & (E extends CustomEvent<infer D>
56
+ ? D
57
+ : never),
58
+ { discrete }: { discrete: boolean } = { discrete: false },
59
+ ) {
60
+ if (!handler) {
61
+ return;
62
+ }
63
+ const target = detail.originalEvent.target;
64
+ const event = new CustomEvent(name, {
65
+ bubbles: false,
66
+ cancelable: true,
67
+ detail,
68
+ });
69
+
70
+ target.addEventListener(name, handler as EventListener, { once: true });
71
+
72
+ if (discrete && target) {
73
+ ReactDOM.flushSync(() => target.dispatchEvent(event));
74
+ } else {
75
+ target.dispatchEvent(event);
76
+ }
77
+ }
@@ -0,0 +1,21 @@
1
+ import { useEffect } from "react";
2
+ import { useCallbackRef } from "../../../util/hooks";
3
+
4
+ export function useEscapeKeydown(
5
+ callback?: (event: KeyboardEvent) => void,
6
+ ownerDocument: Document = globalThis?.document,
7
+ ) {
8
+ const onEscapeKeyDown = useCallbackRef(callback);
9
+
10
+ useEffect(() => {
11
+ const handleKeyDown = (event: KeyboardEvent) => {
12
+ if (event.key === "Escape") {
13
+ onEscapeKeyDown(event);
14
+ }
15
+ };
16
+
17
+ ownerDocument.addEventListener("keydown", handleKeyDown, true);
18
+ return () =>
19
+ ownerDocument.removeEventListener("keydown", handleKeyDown, true);
20
+ }, [onEscapeKeyDown, ownerDocument]);
21
+ }