@navikt/ds-react 6.2.0 → 6.3.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 (271) hide show
  1. package/cjs/form/combobox/ComboboxProvider.js +5 -1
  2. package/cjs/form/combobox/ComboboxProvider.js.map +1 -1
  3. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +14 -12
  4. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  5. package/cjs/form/combobox/FilteredOptions/filtered-options-util.d.ts +3 -3
  6. package/cjs/form/combobox/FilteredOptions/filtered-options-util.js +1 -3
  7. package/cjs/form/combobox/FilteredOptions/filtered-options-util.js.map +1 -1
  8. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.d.ts +8 -5
  9. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +8 -13
  10. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  11. package/cjs/form/combobox/Input/Input.js +9 -7
  12. package/cjs/form/combobox/Input/Input.js.map +1 -1
  13. package/cjs/form/combobox/SelectedOptions/SelectedOptions.d.ts +2 -1
  14. package/cjs/form/combobox/SelectedOptions/SelectedOptions.js +3 -3
  15. package/cjs/form/combobox/SelectedOptions/SelectedOptions.js.map +1 -1
  16. package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.d.ts +10 -7
  17. package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.js +6 -8
  18. package/cjs/form/combobox/SelectedOptions/selectedOptionsContext.js.map +1 -1
  19. package/cjs/form/combobox/combobox-utils.d.ts +10 -0
  20. package/cjs/form/combobox/combobox-utils.js +27 -0
  21. package/cjs/form/combobox/combobox-utils.js.map +1 -0
  22. package/cjs/form/combobox/customOptionsContext.d.ts +5 -4
  23. package/cjs/form/combobox/customOptionsContext.js +1 -1
  24. package/cjs/form/combobox/customOptionsContext.js.map +1 -1
  25. package/cjs/form/combobox/types.d.ts +22 -11
  26. package/cjs/form/file-upload/FileUpload.context.d.ts +8 -0
  27. package/cjs/form/file-upload/FileUpload.context.js +7 -0
  28. package/cjs/form/file-upload/FileUpload.context.js.map +1 -0
  29. package/cjs/form/file-upload/FileUpload.d.ts +118 -0
  30. package/cjs/form/file-upload/FileUpload.js +73 -0
  31. package/cjs/form/file-upload/FileUpload.js.map +1 -0
  32. package/cjs/form/file-upload/FileUpload.types.d.ts +55 -0
  33. package/cjs/form/file-upload/FileUpload.types.js +8 -0
  34. package/cjs/form/file-upload/FileUpload.types.js.map +1 -0
  35. package/cjs/form/file-upload/i18n/get.d.ts +2 -0
  36. package/cjs/form/file-upload/i18n/get.js +38 -0
  37. package/cjs/form/file-upload/i18n/get.js.map +1 -0
  38. package/cjs/form/file-upload/i18n/i18n.context.d.ts +11 -0
  39. package/cjs/form/file-upload/i18n/i18n.context.js +39 -0
  40. package/cjs/form/file-upload/i18n/i18n.context.js.map +1 -0
  41. package/cjs/form/file-upload/i18n/i18n.types.d.ts +13 -0
  42. package/cjs/form/file-upload/i18n/i18n.types.js +3 -0
  43. package/cjs/form/file-upload/i18n/i18n.types.js.map +1 -0
  44. package/cjs/form/file-upload/i18n/locales/nb.json +20 -0
  45. package/cjs/form/file-upload/i18n/merge.d.ts +2 -0
  46. package/cjs/form/file-upload/i18n/merge.js +29 -0
  47. package/cjs/form/file-upload/i18n/merge.js.map +1 -0
  48. package/cjs/form/file-upload/index.d.ts +7 -0
  49. package/cjs/form/file-upload/index.js +16 -0
  50. package/cjs/form/file-upload/index.js.map +1 -0
  51. package/cjs/form/file-upload/parts/Trigger.d.ts +7 -0
  52. package/cjs/form/file-upload/parts/Trigger.js +43 -0
  53. package/cjs/form/file-upload/parts/Trigger.js.map +1 -0
  54. package/cjs/form/file-upload/parts/dropzone/Dropzone.d.ts +4 -0
  55. package/cjs/form/file-upload/parts/dropzone/Dropzone.js +106 -0
  56. package/cjs/form/file-upload/parts/dropzone/Dropzone.js.map +1 -0
  57. package/cjs/form/file-upload/parts/dropzone/dropzone.types.d.ts +18 -0
  58. package/cjs/form/file-upload/parts/dropzone/dropzone.types.js +3 -0
  59. package/cjs/form/file-upload/parts/dropzone/dropzone.types.js.map +1 -0
  60. package/cjs/form/file-upload/parts/dropzone/useDropzone.d.ts +13 -0
  61. package/cjs/form/file-upload/parts/dropzone/useDropzone.js +34 -0
  62. package/cjs/form/file-upload/parts/dropzone/useDropzone.js.map +1 -0
  63. package/cjs/form/file-upload/parts/item/Item.d.ts +55 -0
  64. package/cjs/form/file-upload/parts/item/Item.js +79 -0
  65. package/cjs/form/file-upload/parts/item/Item.js.map +1 -0
  66. package/cjs/form/file-upload/parts/item/Item.types.d.ts +5 -0
  67. package/cjs/form/file-upload/parts/item/Item.types.js +3 -0
  68. package/cjs/form/file-upload/parts/item/Item.types.js.map +1 -0
  69. package/cjs/form/file-upload/parts/item/ItemButton.d.ts +12 -0
  70. package/cjs/form/file-upload/parts/item/ItemButton.js +22 -0
  71. package/cjs/form/file-upload/parts/item/ItemButton.js.map +1 -0
  72. package/cjs/form/file-upload/parts/item/ItemIcon.d.ts +9 -0
  73. package/cjs/form/file-upload/parts/item/ItemIcon.js +51 -0
  74. package/cjs/form/file-upload/parts/item/ItemIcon.js.map +1 -0
  75. package/cjs/form/file-upload/parts/item/ItemName.d.ts +9 -0
  76. package/cjs/form/file-upload/parts/item/ItemName.js +32 -0
  77. package/cjs/form/file-upload/parts/item/ItemName.js.map +1 -0
  78. package/cjs/form/file-upload/parts/item/utils/download-file.d.ts +1 -0
  79. package/cjs/form/file-upload/parts/item/utils/download-file.js +13 -0
  80. package/cjs/form/file-upload/parts/item/utils/download-file.js.map +1 -0
  81. package/cjs/form/file-upload/parts/item/utils/file-type-checker.d.ts +2 -0
  82. package/cjs/form/file-upload/parts/item/utils/file-type-checker.js +6 -0
  83. package/cjs/form/file-upload/parts/item/utils/file-type-checker.js.map +1 -0
  84. package/cjs/form/file-upload/parts/item/utils/format-file-size.d.ts +2 -0
  85. package/cjs/form/file-upload/parts/item/utils/format-file-size.js +24 -0
  86. package/cjs/form/file-upload/parts/item/utils/format-file-size.js.map +1 -0
  87. package/cjs/form/file-upload/useFileUpload.d.ts +12 -0
  88. package/cjs/form/file-upload/useFileUpload.js +33 -0
  89. package/cjs/form/file-upload/useFileUpload.js.map +1 -0
  90. package/cjs/form/file-upload/utils/is-accepted-file-type.d.ts +1 -0
  91. package/cjs/form/file-upload/utils/is-accepted-file-type.js +26 -0
  92. package/cjs/form/file-upload/utils/is-accepted-file-type.js.map +1 -0
  93. package/cjs/form/file-upload/utils/is-accepted-size.d.ts +1 -0
  94. package/cjs/form/file-upload/utils/is-accepted-size.js +11 -0
  95. package/cjs/form/file-upload/utils/is-accepted-size.js.map +1 -0
  96. package/cjs/form/file-upload/utils/validate-files.d.ts +8 -0
  97. package/cjs/form/file-upload/utils/validate-files.js +48 -0
  98. package/cjs/form/file-upload/utils/validate-files.js.map +1 -0
  99. package/cjs/index.d.ts +1 -0
  100. package/cjs/index.js +3 -1
  101. package/cjs/index.js.map +1 -1
  102. package/cjs/loader/Loader.d.ts +0 -7
  103. package/cjs/loader/Loader.js.map +1 -1
  104. package/cjs/table/DataCell.d.ts +1 -3
  105. package/cjs/table/DataCell.js.map +1 -1
  106. package/cjs/util/create-context.d.ts +1 -0
  107. package/cjs/util/create-context.js.map +1 -1
  108. package/cjs/util/hooks/useEventListener.d.ts +1 -1
  109. package/cjs/util/hooks/useMergeRefs.d.ts +1 -1
  110. package/esm/form/combobox/ComboboxProvider.js +5 -1
  111. package/esm/form/combobox/ComboboxProvider.js.map +1 -1
  112. package/esm/form/combobox/FilteredOptions/FilteredOptions.js +14 -12
  113. package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  114. package/esm/form/combobox/FilteredOptions/filtered-options-util.d.ts +3 -3
  115. package/esm/form/combobox/FilteredOptions/filtered-options-util.js +1 -3
  116. package/esm/form/combobox/FilteredOptions/filtered-options-util.js.map +1 -1
  117. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.d.ts +8 -5
  118. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +8 -13
  119. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  120. package/esm/form/combobox/Input/Input.js +9 -7
  121. package/esm/form/combobox/Input/Input.js.map +1 -1
  122. package/esm/form/combobox/SelectedOptions/SelectedOptions.d.ts +2 -1
  123. package/esm/form/combobox/SelectedOptions/SelectedOptions.js +3 -3
  124. package/esm/form/combobox/SelectedOptions/SelectedOptions.js.map +1 -1
  125. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.d.ts +10 -7
  126. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js +6 -8
  127. package/esm/form/combobox/SelectedOptions/selectedOptionsContext.js.map +1 -1
  128. package/esm/form/combobox/combobox-utils.d.ts +10 -0
  129. package/esm/form/combobox/combobox-utils.js +22 -0
  130. package/esm/form/combobox/combobox-utils.js.map +1 -0
  131. package/esm/form/combobox/customOptionsContext.d.ts +5 -4
  132. package/esm/form/combobox/customOptionsContext.js +1 -1
  133. package/esm/form/combobox/customOptionsContext.js.map +1 -1
  134. package/esm/form/combobox/types.d.ts +22 -11
  135. package/esm/form/file-upload/FileUpload.context.d.ts +8 -0
  136. package/esm/form/file-upload/FileUpload.context.js +3 -0
  137. package/esm/form/file-upload/FileUpload.context.js.map +1 -0
  138. package/esm/form/file-upload/FileUpload.d.ts +118 -0
  139. package/esm/form/file-upload/FileUpload.js +44 -0
  140. package/esm/form/file-upload/FileUpload.js.map +1 -0
  141. package/esm/form/file-upload/FileUpload.types.d.ts +55 -0
  142. package/esm/form/file-upload/FileUpload.types.js +5 -0
  143. package/esm/form/file-upload/FileUpload.types.js.map +1 -0
  144. package/esm/form/file-upload/i18n/get.d.ts +2 -0
  145. package/esm/form/file-upload/i18n/get.js +34 -0
  146. package/esm/form/file-upload/i18n/get.js.map +1 -0
  147. package/esm/form/file-upload/i18n/i18n.context.d.ts +11 -0
  148. package/esm/form/file-upload/i18n/i18n.context.js +32 -0
  149. package/esm/form/file-upload/i18n/i18n.context.js.map +1 -0
  150. package/esm/form/file-upload/i18n/i18n.types.d.ts +13 -0
  151. package/esm/form/file-upload/i18n/i18n.types.js +2 -0
  152. package/esm/form/file-upload/i18n/i18n.types.js.map +1 -0
  153. package/esm/form/file-upload/i18n/locales/nb.json +20 -0
  154. package/esm/form/file-upload/i18n/merge.d.ts +2 -0
  155. package/esm/form/file-upload/i18n/merge.js +25 -0
  156. package/esm/form/file-upload/i18n/merge.js.map +1 -0
  157. package/esm/form/file-upload/index.d.ts +7 -0
  158. package/esm/form/file-upload/index.js +6 -0
  159. package/esm/form/file-upload/index.js.map +1 -0
  160. package/esm/form/file-upload/parts/Trigger.d.ts +7 -0
  161. package/esm/form/file-upload/parts/Trigger.js +18 -0
  162. package/esm/form/file-upload/parts/Trigger.js.map +1 -0
  163. package/esm/form/file-upload/parts/dropzone/Dropzone.d.ts +4 -0
  164. package/esm/form/file-upload/parts/dropzone/Dropzone.js +78 -0
  165. package/esm/form/file-upload/parts/dropzone/Dropzone.js.map +1 -0
  166. package/esm/form/file-upload/parts/dropzone/dropzone.types.d.ts +18 -0
  167. package/esm/form/file-upload/parts/dropzone/dropzone.types.js +2 -0
  168. package/esm/form/file-upload/parts/dropzone/dropzone.types.js.map +1 -0
  169. package/esm/form/file-upload/parts/dropzone/useDropzone.d.ts +13 -0
  170. package/esm/form/file-upload/parts/dropzone/useDropzone.js +30 -0
  171. package/esm/form/file-upload/parts/dropzone/useDropzone.js.map +1 -0
  172. package/esm/form/file-upload/parts/item/Item.d.ts +55 -0
  173. package/esm/form/file-upload/parts/item/Item.js +50 -0
  174. package/esm/form/file-upload/parts/item/Item.js.map +1 -0
  175. package/esm/form/file-upload/parts/item/Item.types.d.ts +5 -0
  176. package/esm/form/file-upload/parts/item/Item.types.js +2 -0
  177. package/esm/form/file-upload/parts/item/Item.types.js.map +1 -0
  178. package/esm/form/file-upload/parts/item/ItemButton.d.ts +12 -0
  179. package/esm/form/file-upload/parts/item/ItemButton.js +17 -0
  180. package/esm/form/file-upload/parts/item/ItemButton.js.map +1 -0
  181. package/esm/form/file-upload/parts/item/ItemIcon.d.ts +9 -0
  182. package/esm/form/file-upload/parts/item/ItemIcon.js +46 -0
  183. package/esm/form/file-upload/parts/item/ItemIcon.js.map +1 -0
  184. package/esm/form/file-upload/parts/item/ItemName.d.ts +9 -0
  185. package/esm/form/file-upload/parts/item/ItemName.js +27 -0
  186. package/esm/form/file-upload/parts/item/ItemName.js.map +1 -0
  187. package/esm/form/file-upload/parts/item/utils/download-file.d.ts +1 -0
  188. package/esm/form/file-upload/parts/item/utils/download-file.js +9 -0
  189. package/esm/form/file-upload/parts/item/utils/download-file.js.map +1 -0
  190. package/esm/form/file-upload/parts/item/utils/file-type-checker.d.ts +2 -0
  191. package/esm/form/file-upload/parts/item/utils/file-type-checker.js +2 -0
  192. package/esm/form/file-upload/parts/item/utils/file-type-checker.js.map +1 -0
  193. package/esm/form/file-upload/parts/item/utils/format-file-size.d.ts +2 -0
  194. package/esm/form/file-upload/parts/item/utils/format-file-size.js +20 -0
  195. package/esm/form/file-upload/parts/item/utils/format-file-size.js.map +1 -0
  196. package/esm/form/file-upload/useFileUpload.d.ts +12 -0
  197. package/esm/form/file-upload/useFileUpload.js +29 -0
  198. package/esm/form/file-upload/useFileUpload.js.map +1 -0
  199. package/esm/form/file-upload/utils/is-accepted-file-type.d.ts +1 -0
  200. package/esm/form/file-upload/utils/is-accepted-file-type.js +22 -0
  201. package/esm/form/file-upload/utils/is-accepted-file-type.js.map +1 -0
  202. package/esm/form/file-upload/utils/is-accepted-size.d.ts +1 -0
  203. package/esm/form/file-upload/utils/is-accepted-size.js +7 -0
  204. package/esm/form/file-upload/utils/is-accepted-size.js.map +1 -0
  205. package/esm/form/file-upload/utils/validate-files.d.ts +8 -0
  206. package/esm/form/file-upload/utils/validate-files.js +44 -0
  207. package/esm/form/file-upload/utils/validate-files.js.map +1 -0
  208. package/esm/index.d.ts +1 -0
  209. package/esm/index.js +1 -0
  210. package/esm/index.js.map +1 -1
  211. package/esm/loader/Loader.d.ts +0 -7
  212. package/esm/loader/Loader.js.map +1 -1
  213. package/esm/table/DataCell.d.ts +1 -3
  214. package/esm/table/DataCell.js.map +1 -1
  215. package/esm/util/create-context.d.ts +1 -0
  216. package/esm/util/create-context.js.map +1 -1
  217. package/esm/util/hooks/useEventListener.d.ts +1 -1
  218. package/esm/util/hooks/useMergeRefs.d.ts +1 -1
  219. package/package.json +13 -3
  220. package/src/form/combobox/ComboboxProvider.tsx +7 -3
  221. package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +22 -15
  222. package/src/form/combobox/FilteredOptions/filtered-options-util.ts +5 -10
  223. package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +19 -29
  224. package/src/form/combobox/Input/Input.tsx +14 -8
  225. package/src/form/combobox/SelectedOptions/SelectedOptions.tsx +8 -5
  226. package/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx +24 -25
  227. package/src/form/combobox/combobox-utils.test.ts +67 -0
  228. package/src/form/combobox/combobox-utils.ts +32 -0
  229. package/src/form/combobox/combobox.stories.tsx +67 -32
  230. package/src/form/combobox/combobox.test.tsx +32 -1
  231. package/src/form/combobox/customOptionsContext.tsx +9 -8
  232. package/src/form/combobox/types.ts +23 -11
  233. package/src/form/file-upload/FileUpload.context.tsx +9 -0
  234. package/src/form/file-upload/FileUpload.tsx +142 -0
  235. package/src/form/file-upload/FileUpload.types.ts +57 -0
  236. package/src/form/file-upload/file-upload-dropzone.stories.tsx +123 -0
  237. package/src/form/file-upload/file-upload-item.stories.tsx +136 -0
  238. package/src/form/file-upload/file-upload.stories.tsx +236 -0
  239. package/src/form/file-upload/i18n/get.ts +48 -0
  240. package/src/form/file-upload/i18n/i18n.context.test.tsx +92 -0
  241. package/src/form/file-upload/i18n/i18n.context.ts +67 -0
  242. package/src/form/file-upload/i18n/i18n.types.ts +20 -0
  243. package/src/form/file-upload/i18n/locales/nb.json +20 -0
  244. package/src/form/file-upload/i18n/merge.ts +35 -0
  245. package/src/form/file-upload/index.ts +21 -0
  246. package/src/form/file-upload/parts/Trigger.tsx +48 -0
  247. package/src/form/file-upload/parts/dropzone/Dropzone.tsx +181 -0
  248. package/src/form/file-upload/parts/dropzone/dropzone.types.ts +22 -0
  249. package/src/form/file-upload/parts/dropzone/useDropzone.ts +43 -0
  250. package/src/form/file-upload/parts/item/Item.tsx +165 -0
  251. package/src/form/file-upload/parts/item/Item.types.ts +6 -0
  252. package/src/form/file-upload/parts/item/ItemButton.tsx +52 -0
  253. package/src/form/file-upload/parts/item/ItemIcon.tsx +74 -0
  254. package/src/form/file-upload/parts/item/ItemName.tsx +58 -0
  255. package/src/form/file-upload/parts/item/utils/download-file.ts +9 -0
  256. package/src/form/file-upload/parts/item/utils/file-type-checker.ts +4 -0
  257. package/src/form/file-upload/parts/item/utils/format-file-size.test.ts +76 -0
  258. package/src/form/file-upload/parts/item/utils/format-file-size.ts +25 -0
  259. package/src/form/file-upload/useFileUpload.ts +54 -0
  260. package/src/form/file-upload/utils/is-accepted-file-type.test.ts +69 -0
  261. package/src/form/file-upload/utils/is-accepted-file-type.ts +25 -0
  262. package/src/form/file-upload/utils/is-accepted-size.test.ts +26 -0
  263. package/src/form/file-upload/utils/is-accepted-size.ts +7 -0
  264. package/src/form/file-upload/utils/validate-files.test.ts +132 -0
  265. package/src/form/file-upload/utils/validate-files.ts +62 -0
  266. package/src/index.ts +14 -0
  267. package/src/internal-header/header.stories.tsx +8 -5
  268. package/src/loader/Loader.tsx +0 -7
  269. package/src/table/DataCell.tsx +1 -6
  270. package/src/util/create-context.tsx +1 -0
  271. package/src/util/hooks/useMergeRefs.ts +1 -1
@@ -0,0 +1,48 @@
1
+ import { TranslationObject } from "./i18n.types";
2
+
3
+ /**
4
+ * https://github.com/Shopify/polaris/blob/main/polaris-react/src/utilities/get.ts#L3
5
+ */
6
+ const OBJECT_NOTATION_MATCHER = /(\w+)/g;
7
+
8
+ export function get(
9
+ keypath: string | string[],
10
+ ...objs: (TranslationObject | undefined)[]
11
+ ) {
12
+ const keys = Array.isArray(keypath) ? keypath : getKeypath(keypath);
13
+
14
+ for (const obj of objs) {
15
+ if (!obj) {
16
+ continue;
17
+ }
18
+
19
+ let acc: string | TranslationObject = obj;
20
+
21
+ for (let i = 0; i < keys.length; i++) {
22
+ const val = acc[keys[i]];
23
+ if (val === undefined) {
24
+ continue;
25
+ }
26
+ acc = val;
27
+ }
28
+
29
+ if (typeof acc === "string") {
30
+ return acc;
31
+ }
32
+ }
33
+
34
+ throw new Error(
35
+ "Error translating key. The keypath does not resolve to a string.",
36
+ );
37
+ }
38
+
39
+ function getKeypath(str: string) {
40
+ const path: string[] = [];
41
+ let result: RegExpExecArray | null;
42
+ while ((result = OBJECT_NOTATION_MATCHER.exec(str))) {
43
+ const [, first, second] = result;
44
+ path.push(first || second);
45
+ }
46
+
47
+ return path;
48
+ }
@@ -0,0 +1,92 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import React from "react";
3
+ import { describe, expect, test } from "vitest";
4
+ import { I18nContext, useI18n } from "./i18n.context";
5
+
6
+ describe("useI18n", () => {
7
+ test("should throw error if key is not found", () => {
8
+ const { result } = renderHook(() => useI18n("FileUpload"));
9
+ const translate = result.current;
10
+ // @ts-expect-error - Testing nonexistent key
11
+ expect(() => translate("item.nonexistentKey")).toThrowError();
12
+ });
13
+
14
+ test("should return the translated text from I18nContext", () => {
15
+ const i18n = { FileUpload: { item: { uploading: "Test translation" } } };
16
+ const { result } = renderHook(() => useI18n("FileUpload"), {
17
+ wrapper: ({ children }) => (
18
+ <I18nContext.Provider value={i18n}>{children}</I18nContext.Provider>
19
+ ),
20
+ });
21
+ const translate = result.current;
22
+ expect(translate("item.uploading")).toBe("Test translation");
23
+ });
24
+
25
+ test("should return the translated text from first context object", () => {
26
+ const i18n1 = {
27
+ FileUpload: { item: { uploading: "Correct translation" } },
28
+ };
29
+ const i18n2 = { FileUpload: { item: { uploading: "Wrong translation" } } };
30
+ const { result } = renderHook(() => useI18n("FileUpload"), {
31
+ wrapper: ({ children }) => (
32
+ <I18nContext.Provider value={[i18n1, i18n2]}>
33
+ {children}
34
+ </I18nContext.Provider>
35
+ ),
36
+ });
37
+ const translate = result.current;
38
+ expect(translate("item.uploading")).toBe("Correct translation");
39
+ });
40
+
41
+ test("should return the translated text from second context object", () => {
42
+ const i18n1 = { FileUpload: { item: { uploading: "Foo" } } };
43
+ const i18n2 = {
44
+ FileUpload: { item: { downloading: "Correct translation" } },
45
+ };
46
+ const { result } = renderHook(() => useI18n("FileUpload"), {
47
+ wrapper: ({ children }) => (
48
+ <I18nContext.Provider value={[i18n1, i18n2]}>
49
+ {children}
50
+ </I18nContext.Provider>
51
+ ),
52
+ });
53
+ const translate = result.current;
54
+ expect(translate("item.downloading")).toBe("Correct translation");
55
+ });
56
+
57
+ test("should return the translated text from first local object", () => {
58
+ const i18n1 = { item: { uploading: "Correct translation" } };
59
+ const i18n2 = { item: { uploading: "Wrong translation" } };
60
+ const { result } = renderHook(() => useI18n("FileUpload", i18n1, i18n2));
61
+ const translate = result.current;
62
+ expect(translate("item.uploading")).toBe("Correct translation");
63
+ });
64
+
65
+ test("should return the translated text from second local object", () => {
66
+ const i18n1 = { item: { uploading: "Foo" } };
67
+ const i18n2 = { item: { downloading: "Correct translation" } };
68
+ const { result } = renderHook(() => useI18n("FileUpload", i18n1, i18n2));
69
+ const translate = result.current;
70
+ expect(translate("item.downloading")).toBe("Correct translation");
71
+ });
72
+
73
+ test("should replace placeholders in the translated text", () => {
74
+ const i18n = {
75
+ item: { uploading: "Hello, {name}. You have {cnt} messages." },
76
+ };
77
+ const { result } = renderHook(() => useI18n("FileUpload", i18n));
78
+ const translate = result.current;
79
+ expect(
80
+ translate("item.uploading", { replacements: { name: "John", cnt: 3 } }),
81
+ ).toBe("Hello, John. You have 3 messages.");
82
+ });
83
+
84
+ test("should throw an error if replacement key is not found", () => {
85
+ const i18n = { item: { uploading: "Hello, {name}" } };
86
+ const { result } = renderHook(() => useI18n("FileUpload", i18n));
87
+ const translate = result.current;
88
+ expect(() =>
89
+ translate("item.uploading", { replacements: { other: "John" } }),
90
+ ).toThrowError();
91
+ });
92
+ });
@@ -0,0 +1,67 @@
1
+ import { createContext, useContext } from "react";
2
+ import { get } from "./get";
3
+ import {
4
+ Component,
5
+ ComponentTranslation,
6
+ TranslationDictionary,
7
+ } from "./i18n.types";
8
+ import nb from "./locales/nb.json";
9
+
10
+ /**
11
+ * https://regex101.com/r/LYKWi3/1
12
+ */
13
+ const REPLACE_REGEX = /{[^}]*}/g;
14
+
15
+ export const I18nContext = createContext<
16
+ TranslationDictionary | TranslationDictionary[]
17
+ >(nb);
18
+
19
+ /* https://dev.to/pffigueiredo/typescript-utility-keyof-nested-object-2pa3 */
20
+ type NestedKeyOf<ObjectType extends object> = {
21
+ [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
22
+ ? `${Key}.${NestedKeyOf<ObjectType[Key]>}`
23
+ : `${Key}`;
24
+ }[keyof ObjectType & (string | number)];
25
+
26
+ export function useI18n<T extends Component>(
27
+ componentName: T,
28
+ ...local: (ComponentTranslation<T> | undefined)[]
29
+ ) {
30
+ const i18n = useContext(I18nContext);
31
+
32
+ /**
33
+ * https://github.com/Shopify/polaris/blob/2115f9ba2f5bcbf2ad15745233501bff2db81ecf/polaris-react/src/utilities/i18n/I18n.ts#L24
34
+ */
35
+ const translate = (
36
+ keypath: NestedKeyOf<(typeof nb)[T]>,
37
+ options?: { replacements: Record<string, string | number> },
38
+ ) => {
39
+ const text = get(
40
+ keypath,
41
+ ...local,
42
+ ...(Array.isArray(i18n)
43
+ ? i18n.map((t) => t[componentName])
44
+ : [i18n[componentName]]),
45
+ );
46
+
47
+ if (options?.replacements) {
48
+ return text.replace(REPLACE_REGEX, (match) => {
49
+ const replacement = match.substring(1, match.length - 1);
50
+
51
+ if (options.replacements[replacement] === undefined) {
52
+ const replacementData = JSON.stringify(options.replacements);
53
+
54
+ throw new Error(
55
+ `Error translating key '${keypath}'. No replacement syntax ({}) found for key '${replacement}'. The following replacements were passed: '${replacementData}'`,
56
+ );
57
+ }
58
+
59
+ return options.replacements[replacement] as string; // can also be a number, but JS doesn't mind...
60
+ });
61
+ }
62
+
63
+ return text;
64
+ };
65
+
66
+ return translate;
67
+ }
@@ -0,0 +1,20 @@
1
+ import nb from "./locales/nb.json";
2
+
3
+ export interface TranslationObject {
4
+ [key: string]: string | TranslationObject | undefined;
5
+ }
6
+
7
+ export interface TranslationDictionary {
8
+ [key: string]: TranslationObject | undefined;
9
+ }
10
+
11
+ /* https://stackoverflow.com/questions/47914536/use-partial-in-nested-property-with-typescripts */
12
+ type RecursivePartial<T> = {
13
+ [P in keyof T]?: RecursivePartial<T[P]>;
14
+ };
15
+
16
+ export type Component = keyof typeof nb;
17
+
18
+ export type ComponentTranslation<T extends Component> = RecursivePartial<
19
+ (typeof nb)[T]
20
+ >;
@@ -0,0 +1,20 @@
1
+ {
2
+ "FileUpload": {
3
+ "dropzone": {
4
+ "button": "Velg fil",
5
+ "buttonMultiple": "Velg filer",
6
+ "dragAndDrop": "Dra og slipp filen her",
7
+ "dragAndDropMultiple": "Dra og slipp filer her",
8
+ "drop": "Slipp",
9
+ "or": "eller",
10
+ "disabled": "Filopplasting er deaktivert",
11
+ "disabledFilelimit": "Du kan ikke laste opp flere filer"
12
+ },
13
+ "item": {
14
+ "retryButtonTitle": "Prøv å laste opp filen på nytt",
15
+ "deleteButtonTitle": "Slett filen",
16
+ "uploading": "Laster opp…",
17
+ "downloading": "Laster ned…"
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,35 @@
1
+ import { TranslationDictionary, TranslationObject } from "./i18n.types";
2
+
3
+ export function merge(...objs: TranslationDictionary[]) {
4
+ let final: TranslationDictionary = {};
5
+
6
+ for (const obj of objs) {
7
+ final = mergeRecursively(final, obj);
8
+ }
9
+
10
+ return final;
11
+ }
12
+
13
+ function mergeRecursively<T>(
14
+ objA: T,
15
+ objB: TranslationDictionary | TranslationObject,
16
+ ) {
17
+ const objARes = { ...objA };
18
+
19
+ for (const key in objB) {
20
+ if (!(key in objB)) {
21
+ continue;
22
+ }
23
+
24
+ const a = objARes[key];
25
+ const b = objB[key];
26
+
27
+ if (b && typeof b !== "string" && typeof a !== "string") {
28
+ objARes[key] = mergeRecursively(a, b);
29
+ } else {
30
+ objARes[key] = b;
31
+ }
32
+ }
33
+
34
+ return objARes;
35
+ }
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ export { default as UNSAFE_FileUpload } from "./FileUpload";
3
+ export { default as UNSAFE_FileUploadDropzone } from "./parts/dropzone/Dropzone";
4
+ export { type FileUploadDropzoneProps } from "./parts/dropzone/dropzone.types";
5
+ export {
6
+ default as UNSAFE_FileUploadTrigger,
7
+ type FileUploadTriggerProps,
8
+ } from "./parts/Trigger";
9
+ export {
10
+ type FileObject,
11
+ type FileRejected,
12
+ type FileAccepted,
13
+ type FileRejectedPartitioned,
14
+ type FilesPartitioned,
15
+ type FileRejectionReason,
16
+ } from "./FileUpload.types";
17
+ export {
18
+ default as UNSAFE_FileUploadItem,
19
+ type FileUploadItemProps,
20
+ } from "./parts/item/Item";
21
+ export { type FileItem, type FileMetadata } from "./parts/item/Item.types";
@@ -0,0 +1,48 @@
1
+ import React, { forwardRef } from "react";
2
+ import { Slot } from "../../../util/Slot";
3
+ import { FileUploadBaseProps } from "../FileUpload.types";
4
+ import { useFileUpload } from "../useFileUpload";
5
+
6
+ export interface FileUploadTriggerProps
7
+ extends Omit<FileUploadBaseProps, "fileLimit"> {
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ const Trigger = forwardRef<HTMLInputElement, FileUploadTriggerProps>(
12
+ (
13
+ {
14
+ children,
15
+ multiple = true,
16
+ accept,
17
+ onSelect,
18
+ validator,
19
+ maxSizeInBytes,
20
+ }: FileUploadTriggerProps,
21
+ ref,
22
+ ) => {
23
+ const { onChange, inputRef, mergedRef } = useFileUpload({
24
+ ref,
25
+ onSelect,
26
+ validator,
27
+ accept,
28
+ maxSizeInBytes,
29
+ disabled: false,
30
+ });
31
+
32
+ return (
33
+ <>
34
+ <Slot onClick={() => inputRef.current?.click()}>{children}</Slot>
35
+ <input
36
+ ref={mergedRef}
37
+ type="file"
38
+ style={{ display: "none" }}
39
+ multiple={multiple}
40
+ accept={accept}
41
+ onChange={onChange}
42
+ />
43
+ </>
44
+ );
45
+ },
46
+ );
47
+
48
+ export default Trigger;
@@ -0,0 +1,181 @@
1
+ import cl from "clsx";
2
+ import React, { forwardRef } from "react";
3
+ import { CircleSlashIcon, CloudUpIcon } from "@navikt/aksel-icons";
4
+ import { Button } from "../../../../button";
5
+ import { BodyShort, ErrorMessage, Label } from "../../../../typography";
6
+ import { composeEventHandlers } from "../../../../util/composeEventHandlers";
7
+ import { useId } from "../../../../util/hooks";
8
+ import { omit } from "../../../../util/omit";
9
+ import { useFormField } from "../../../useFormField";
10
+ import { useFileUploadTranslation } from "../../FileUpload.context";
11
+ import { useI18n } from "../../i18n/i18n.context";
12
+ import { useFileUpload } from "../../useFileUpload";
13
+ import { FileUploadDropzoneProps } from "./dropzone.types";
14
+ import { useDropzone } from "./useDropzone";
15
+
16
+ const Dropzone = forwardRef<HTMLInputElement, FileUploadDropzoneProps>(
17
+ (props: FileUploadDropzoneProps, ref) => {
18
+ const {
19
+ onSelect,
20
+ error,
21
+ label,
22
+ description,
23
+ className,
24
+ multiple = true,
25
+ accept,
26
+ validator,
27
+ maxSizeInBytes,
28
+ fileLimit,
29
+ icon: DropzoneIcon = CloudUpIcon,
30
+ disabled,
31
+ translations,
32
+ onClick,
33
+ ...rest
34
+ } = props;
35
+
36
+ const context = useFileUploadTranslation(false);
37
+ const translate = useI18n(
38
+ "FileUpload",
39
+ { dropzone: translations },
40
+ context?.translations,
41
+ );
42
+
43
+ const fileLimitReached =
44
+ fileLimit && fileLimit?.current >= fileLimit?.max && fileLimit?.max > 0;
45
+
46
+ const _disabled = disabled ?? fileLimitReached;
47
+
48
+ const { inputProps, errorId, showErrorMsg, hasError, inputDescriptionId } =
49
+ useFormField({ ...props, disabled: _disabled }, "fileUpload");
50
+ const {
51
+ id: inputId,
52
+ "aria-describedby": ariaDescribedby,
53
+ ...inputPropsRest
54
+ } = inputProps;
55
+ const labelId = useId();
56
+
57
+ const { upload, onChange, inputRef, mergedRef } = useFileUpload({
58
+ ref,
59
+ onSelect,
60
+ validator,
61
+ accept,
62
+ maxSizeInBytes,
63
+ disabled: inputProps.disabled,
64
+ });
65
+
66
+ const dropzoneCtx = useDropzone({
67
+ upload,
68
+ disabled: inputProps.disabled,
69
+ });
70
+
71
+ return (
72
+ <div
73
+ className={cl("navds-form-field", "navds-dropzone", className, {
74
+ "navds-dropzone--error": hasError,
75
+ "navds-dropzone--dragging": dropzoneCtx.isDraggingOver,
76
+ "navds-dropzone--disabled": inputProps.disabled,
77
+ })}
78
+ >
79
+ <Label
80
+ htmlFor={inputId}
81
+ id={labelId}
82
+ className="navds-form-field__label"
83
+ >
84
+ {label}
85
+ </Label>
86
+ {!!description && (
87
+ <BodyShort
88
+ id={inputDescriptionId}
89
+ className="navds-form-field__description"
90
+ as="div"
91
+ >
92
+ {description}
93
+ </BodyShort>
94
+ )}
95
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
96
+ <div
97
+ className="navds-dropzone__area"
98
+ onDragEnter={dropzoneCtx.onDragEnter}
99
+ onDragOver={dropzoneCtx.onDragOver}
100
+ onDragLeave={dropzoneCtx.onDragLeave}
101
+ onDrop={dropzoneCtx.onDrop}
102
+ onClick={composeEventHandlers(
103
+ onClick,
104
+ () => inputRef.current?.click(),
105
+ )}
106
+ >
107
+ {!inputProps.disabled && (
108
+ <>
109
+ <div className="navds-dropzone__area-icon">
110
+ <DropzoneIcon fontSize="1.5rem" aria-hidden />
111
+ </div>
112
+ <div className="navds-dropzone__area-release">
113
+ <div className="navds-dropzone__area-release__icon">
114
+ <DropzoneIcon aria-hidden />
115
+ </div>
116
+ <span
117
+ aria-hidden={!dropzoneCtx.isDraggingOver}
118
+ className="navds-dropzone__area-release__text"
119
+ >
120
+ {translate("dropzone.drop")}
121
+ </span>
122
+ </div>
123
+ <div aria-hidden>
124
+ <BodyShort as="div" spacing>
125
+ {multiple
126
+ ? translate("dropzone.dragAndDropMultiple")
127
+ : translate("dropzone.dragAndDrop")}
128
+ </BodyShort>
129
+ <BodyShort as="div">{translate("dropzone.or")}</BodyShort>
130
+ </div>
131
+ <Button
132
+ {...omit(rest, ["errorId"])}
133
+ {...inputPropsRest}
134
+ aria-describedby={cl(labelId, ariaDescribedby)}
135
+ className="navds-dropzone__area-button"
136
+ type="button"
137
+ variant="secondary"
138
+ >
139
+ {multiple
140
+ ? translate("dropzone.buttonMultiple")
141
+ : translate("dropzone.button")}
142
+ </Button>
143
+ </>
144
+ )}
145
+
146
+ {inputProps.disabled && (
147
+ <div className="navds-dropzone__area-disabled">
148
+ <CircleSlashIcon aria-hidden fontSize="1.75rem" />
149
+ <BodyShort as="div">
150
+ {fileLimitReached
151
+ ? translate("dropzone.disabledFilelimit")
152
+ : translate("dropzone.disabled")}
153
+ </BodyShort>
154
+ </div>
155
+ )}
156
+
157
+ <input
158
+ id={inputId}
159
+ type="file"
160
+ style={{ display: "none" }}
161
+ multiple={multiple}
162
+ accept={accept}
163
+ onChange={onChange}
164
+ ref={mergedRef}
165
+ disabled={inputProps.disabled}
166
+ />
167
+ </div>
168
+ <div
169
+ className="navds-form-field__error"
170
+ id={errorId}
171
+ aria-relevant="additions removals"
172
+ aria-live="polite"
173
+ >
174
+ {showErrorMsg && <ErrorMessage>{error}</ErrorMessage>}
175
+ </div>
176
+ </div>
177
+ );
178
+ },
179
+ );
180
+
181
+ export default Dropzone;
@@ -0,0 +1,22 @@
1
+ import { FormFieldProps } from "../../../useFormField";
2
+ import { FileUploadBaseProps } from "../../FileUpload.types";
3
+ import { ComponentTranslation } from "../../i18n/i18n.types";
4
+
5
+ export interface FileUploadDropzoneProps
6
+ extends FileUploadBaseProps,
7
+ Omit<FormFieldProps, "size" | "readOnly">,
8
+ Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect" | "onClick">,
9
+ Pick<React.HTMLAttributes<HTMLDivElement>, "onClick"> {
10
+ /**
11
+ * Text shown to the user.
12
+ */
13
+ label: string;
14
+ /**
15
+ * @default CloudUpIcon
16
+ */
17
+ icon?: React.ComponentType<any>;
18
+ /**
19
+ * i18n-API for customizing texts and labels
20
+ */
21
+ translations?: ComponentTranslation<"FileUpload">["dropzone"];
22
+ }
@@ -0,0 +1,43 @@
1
+ import { useState } from "react";
2
+ import { UseFileUploadProps } from "../../useFileUpload";
3
+
4
+ interface Props {
5
+ upload: (fileList: FileList) => void;
6
+ disabled: UseFileUploadProps["disabled"];
7
+ }
8
+
9
+ export const useDropzone = ({ upload, disabled }: Props) => {
10
+ const [isDraggingOver, setIsDraggingOver] = useState(false);
11
+
12
+ const onDragEnter = () => {
13
+ setIsDraggingOver(true);
14
+ };
15
+
16
+ const onDragOver = (event: React.DragEvent<HTMLDivElement>) => {
17
+ event.preventDefault(); // Prevents the browser from opening the file in a new tab
18
+ };
19
+
20
+ const onDragLeave = () => {
21
+ setIsDraggingOver(false);
22
+ };
23
+
24
+ const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
25
+ event.preventDefault(); // Prevents the browser from opening the file in a new tab
26
+ setIsDraggingOver(false);
27
+
28
+ const fileList = event.dataTransfer.files;
29
+ if (!fileList) {
30
+ return;
31
+ }
32
+
33
+ upload(fileList);
34
+ };
35
+
36
+ return {
37
+ isDraggingOver,
38
+ onDragEnter: disabled ? undefined : onDragEnter,
39
+ onDragOver: disabled ? undefined : onDragOver,
40
+ onDragLeave: disabled ? undefined : onDragLeave,
41
+ onDrop: disabled ? undefined : onDrop,
42
+ };
43
+ };