@navikt/ds-react 6.2.0 → 6.3.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 (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 +180 -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,165 @@
1
+ import cl from "clsx";
2
+ import React, { MouseEvent, forwardRef } from "react";
3
+ import { ExclamationmarkTriangleIcon } from "@navikt/aksel-icons";
4
+ import { BodyShort } from "../../../../typography";
5
+ import { OverridableComponent } from "../../../../util";
6
+ import { useFileUploadTranslation } from "../../FileUpload.context";
7
+ import { useI18n } from "../../i18n/i18n.context";
8
+ import { ComponentTranslation } from "../../i18n/i18n.types";
9
+ import { FileItem } from "./Item.types";
10
+ import ItemButton from "./ItemButton";
11
+ import ItemIcon from "./ItemIcon";
12
+ import ItemName from "./ItemName";
13
+ import { formatFileSize } from "./utils/format-file-size";
14
+
15
+ export interface FileItemBaseProps {
16
+ /**
17
+ * Overrides html-tag
18
+ * @default "div"
19
+ */
20
+ as?: "div" | "li";
21
+ /**
22
+ * Either a native File or file metadata.
23
+ */
24
+ file: FileItem;
25
+ /**
26
+ * onClick on the file name.
27
+ * @note If this and `href` is not set and the `file` prop is a native file, onClick will download the file.
28
+ */
29
+ onFileClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
30
+ /**
31
+ * href on the file name.
32
+ * @note If this and `onFileClick` is not set and the `file` prop is a native file, onClick will download the file.
33
+ */
34
+ href?: string;
35
+ /**
36
+ * Error message relating to the item.
37
+ */
38
+ error?: string;
39
+ /**
40
+ * Status "downloading" and "uploading" displays a loading indicator.
41
+ * @default "idle"
42
+ */
43
+ status?: "downloading" | "uploading" | "idle";
44
+ /**
45
+ * i18n-API for customizing texts and labels
46
+ */
47
+ translations?: ComponentTranslation<"FileUpload">["item"];
48
+ onRetry?: (event: MouseEvent<HTMLButtonElement>) => void;
49
+ onDelete?: (event: MouseEvent<HTMLButtonElement>) => void;
50
+ }
51
+
52
+ type FileItemActionDelete = {
53
+ onDelete: (event: MouseEvent<HTMLButtonElement>) => void;
54
+ itemAction: "delete";
55
+ };
56
+
57
+ type FileItemActionRetry = {
58
+ onRetry: (event: MouseEvent<HTMLButtonElement>) => void;
59
+ itemAction: "retry";
60
+ };
61
+
62
+ type FileItemActionNone = {
63
+ itemAction?: "none";
64
+ };
65
+
66
+ type FileItemConditionalProps =
67
+ | FileItemActionDelete
68
+ | FileItemActionRetry
69
+ | FileItemActionNone;
70
+
71
+ export type FileUploadItemProps = FileItemBaseProps &
72
+ FileItemConditionalProps &
73
+ React.HTMLAttributes<HTMLDivElement>;
74
+
75
+ export const Item: OverridableComponent<FileUploadItemProps, HTMLDivElement> =
76
+ forwardRef(
77
+ (
78
+ {
79
+ as: Component = "div",
80
+ file,
81
+ status = "idle",
82
+ onDelete,
83
+ onRetry,
84
+ error,
85
+ className,
86
+ href,
87
+ onFileClick,
88
+ itemAction = "delete",
89
+ translations,
90
+ ...rest
91
+ },
92
+ ref,
93
+ ) => {
94
+ const context = useFileUploadTranslation(false);
95
+ const translate = useI18n(
96
+ "FileUpload",
97
+ { item: translations },
98
+ context?.translations,
99
+ );
100
+
101
+ const showError = !!error && status === "idle";
102
+
103
+ function getStatusText() {
104
+ if (status === "uploading") {
105
+ return translate("item.uploading");
106
+ }
107
+ if (status === "downloading") {
108
+ return translate("item.downloading");
109
+ }
110
+ return formatFileSize(file);
111
+ }
112
+
113
+ return (
114
+ <Component
115
+ ref={ref}
116
+ {...rest}
117
+ className={cl("navds-file-item", className, {
118
+ "navds-file-item--error": showError,
119
+ })}
120
+ >
121
+ <div className="navds-file-item__inner">
122
+ <ItemIcon
123
+ isLoading={status !== "idle"}
124
+ file={file}
125
+ showError={showError}
126
+ />
127
+ <div className="navds-file-item__file-info">
128
+ <ItemName file={file} href={href} onClick={onFileClick} />
129
+ <BodyShort as="div" size="small">
130
+ {getStatusText()}
131
+ </BodyShort>
132
+ <div
133
+ className="navds-file-item__error"
134
+ aria-relevant="additions removals"
135
+ aria-live="polite"
136
+ >
137
+ {showError && (
138
+ <BodyShort
139
+ size="small"
140
+ className="navds-file-item__error-content"
141
+ >
142
+ <ExclamationmarkTriangleIcon aria-hidden />
143
+ {error}
144
+ </BodyShort>
145
+ )}
146
+ </div>
147
+ </div>
148
+
149
+ {status === "idle" && (
150
+ <ItemButton
151
+ file={file}
152
+ onRetry={onRetry}
153
+ onDelete={onDelete}
154
+ action={itemAction}
155
+ retryTitle={translate("item.retryButtonTitle")}
156
+ deleteTitle={translate("item.deleteButtonTitle")}
157
+ />
158
+ )}
159
+ </div>
160
+ </Component>
161
+ );
162
+ },
163
+ );
164
+
165
+ export default Item;
@@ -0,0 +1,6 @@
1
+ export interface FileMetadata {
2
+ name: string;
3
+ size?: number;
4
+ }
5
+
6
+ export type FileItem = FileMetadata | File;
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import { ArrowsCirclepathIcon, TrashIcon } from "@navikt/aksel-icons";
3
+ import { Button } from "../../../../button";
4
+ import { FileItem } from "./Item.types";
5
+
6
+ interface Props {
7
+ file: FileItem;
8
+ onRetry?: (event: React.MouseEvent<HTMLButtonElement>) => void;
9
+ onDelete?: (event: React.MouseEvent<HTMLButtonElement>) => void;
10
+ action: "delete" | "retry" | "none";
11
+ retryTitle: string;
12
+ deleteTitle: string;
13
+ }
14
+
15
+ const ItemButton = ({
16
+ onRetry,
17
+ onDelete,
18
+ action,
19
+ retryTitle,
20
+ deleteTitle,
21
+ }: Props) => {
22
+ if (action === "none") {
23
+ return null;
24
+ }
25
+
26
+ if (onRetry && action === "retry") {
27
+ return (
28
+ <Button
29
+ className="navds-file-item__button"
30
+ type="button"
31
+ variant="tertiary-neutral"
32
+ onClick={onRetry}
33
+ icon={<ArrowsCirclepathIcon title={retryTitle} />}
34
+ />
35
+ );
36
+ }
37
+ if (onDelete && action === "delete") {
38
+ return (
39
+ <Button
40
+ className="navds-file-item__button"
41
+ type="button"
42
+ variant="tertiary-neutral"
43
+ onClick={onDelete}
44
+ icon={<TrashIcon title={deleteTitle} />}
45
+ />
46
+ );
47
+ }
48
+
49
+ return null;
50
+ };
51
+
52
+ export default ItemButton;
@@ -0,0 +1,74 @@
1
+ import React from "react";
2
+ import {
3
+ FileCsvIcon,
4
+ FileExcelIcon,
5
+ FileIcon,
6
+ FileImageIcon,
7
+ FilePdfIcon,
8
+ FileTextIcon,
9
+ FileWordIcon,
10
+ FileXMarkIcon,
11
+ } from "@navikt/aksel-icons";
12
+ import { Loader } from "../../../../loader";
13
+ import { FileItem } from "./Item.types";
14
+
15
+ interface ItemIconProps {
16
+ isLoading?: boolean;
17
+ file: FileItem;
18
+ showError: boolean;
19
+ }
20
+
21
+ const iconProps = {
22
+ fontSize: "2rem",
23
+ "aria-hidden": true,
24
+ };
25
+
26
+ function ItemIcon({ isLoading, file, showError }: ItemIconProps) {
27
+ if (isLoading) {
28
+ return (
29
+ <div className="navds-file-item__icon navds-file-item__icon--loading">
30
+ <Loader size="large" />
31
+ </div>
32
+ );
33
+ } else if (showError) {
34
+ return (
35
+ <div className="navds-file-item__icon">
36
+ <FileXMarkIcon {...iconProps} />
37
+ </div>
38
+ );
39
+ }
40
+ return (
41
+ <div className="navds-file-item__icon">
42
+ <Icon file={file} />
43
+ </div>
44
+ );
45
+ }
46
+
47
+ function Icon({ file }: { file: FileItem }) {
48
+ const extension = file.name.substring(file.name.lastIndexOf(".") + 1);
49
+
50
+ switch (extension) {
51
+ case "jpg":
52
+ case "jpeg":
53
+ case "png":
54
+ case "gif":
55
+ case "webp":
56
+ return <FileImageIcon {...iconProps} />;
57
+ case "pdf":
58
+ return <FilePdfIcon {...iconProps} />;
59
+ case "txt":
60
+ return <FileTextIcon {...iconProps} />;
61
+ case "csv":
62
+ return <FileCsvIcon {...iconProps} />;
63
+ case "xls":
64
+ case "xlsx":
65
+ return <FileExcelIcon {...iconProps} />;
66
+ case "doc":
67
+ case "docx":
68
+ return <FileWordIcon {...iconProps} />;
69
+ default:
70
+ return <FileIcon {...iconProps} />;
71
+ }
72
+ }
73
+
74
+ export default ItemIcon;
@@ -0,0 +1,58 @@
1
+ import React from "react";
2
+ import { Link } from "../../../../link";
3
+ import { FileItem } from "./Item.types";
4
+ import { downloadFile } from "./utils/download-file";
5
+ import { isNativeFile } from "./utils/file-type-checker";
6
+
7
+ interface Props {
8
+ file: FileItem;
9
+ href?: string;
10
+ onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
11
+ }
12
+
13
+ const ItemName = ({ file, href, onClick }: Props) => {
14
+ if (onClick && href) {
15
+ return (
16
+ <Link href={href} onClick={onClick}>
17
+ {file.name}
18
+ </Link>
19
+ );
20
+ }
21
+
22
+ if (onClick) {
23
+ return (
24
+ <Link
25
+ href="#"
26
+ onClick={(event) => {
27
+ event.preventDefault();
28
+ onClick(event);
29
+ }}
30
+ >
31
+ {file.name}
32
+ </Link>
33
+ );
34
+ }
35
+
36
+ if (href) {
37
+ return <Link href={href}>{file.name}</Link>;
38
+ }
39
+
40
+ if (isNativeFile(file)) {
41
+ return (
42
+ <Link
43
+ href="#"
44
+ download={file.name}
45
+ onClick={(event) => {
46
+ event.preventDefault();
47
+ downloadFile(file);
48
+ }}
49
+ >
50
+ {file.name}
51
+ </Link>
52
+ );
53
+ }
54
+
55
+ return <span>{file.name}</span>;
56
+ };
57
+
58
+ export default ItemName;
@@ -0,0 +1,9 @@
1
+ export const downloadFile = (file: File): void => {
2
+ const a = document.createElement("a");
3
+ const url = URL.createObjectURL(file);
4
+ a.href = url;
5
+ a.download = file.name;
6
+ a.click();
7
+
8
+ URL.revokeObjectURL(url);
9
+ };
@@ -0,0 +1,4 @@
1
+ import { FileItem } from "../Item.types";
2
+
3
+ export const isNativeFile = (fileItem: FileItem): fileItem is File =>
4
+ "lastModified" in fileItem;
@@ -0,0 +1,76 @@
1
+ import { formatFileSize } from "./format-file-size";
2
+
3
+ describe("format-file-size", () => {
4
+ describe("with native File", () => {
5
+ it('returns "0,01 MB" when file size is less than 0,01 MB', () => {
6
+ const file = new File(["abc"], "file.txt");
7
+
8
+ expect(formatFileSize(file)).toBe("0,01 MB");
9
+ });
10
+
11
+ it('returns "0,29 MB" when file size is 0,29 MB', () => {
12
+ const file = new File(["abc".repeat(99_999)], "file.txt");
13
+
14
+ expect(formatFileSize(file)).toBe("0,29 MB");
15
+ });
16
+
17
+ it('returns "> 500 MB" when file size is more than 500 MB', () => {
18
+ const file = createLargeMockFile(600_000_000);
19
+
20
+ expect(formatFileSize(file)).toBe("> 500 MB");
21
+ });
22
+ });
23
+
24
+ describe("with MetadataFile", () => {
25
+ it("returns null when file size is undefined", () => {
26
+ const file = {
27
+ name: "myfile.txt",
28
+ };
29
+
30
+ expect(formatFileSize(file)).toBeNull();
31
+ });
32
+ it('returns "0,01 MB" when file size is less than 0,01 MB', () => {
33
+ const file = {
34
+ name: "myfile.txt",
35
+ size: 1,
36
+ };
37
+
38
+ expect(formatFileSize(file)).toBe("0,01 MB");
39
+ });
40
+
41
+ it('returns "0,96 MB" when file size is 0,96 MB', () => {
42
+ const file = {
43
+ name: "myfile.txt",
44
+ size: 999_999,
45
+ };
46
+
47
+ expect(formatFileSize(file)).toBe("0,96 MB");
48
+ });
49
+
50
+ it('returns "> 500 MB" when file size is more than 500 MB', () => {
51
+ const file = {
52
+ name: "myfile.txt",
53
+ size: 600_000_000,
54
+ };
55
+
56
+ expect(formatFileSize(file)).toBe("> 500 MB");
57
+ });
58
+ });
59
+ });
60
+
61
+ function createLargeMockFile(sizeInBytes: number): File {
62
+ const chunkSize = 1024 * 1024; // 1MB chunk size
63
+ const chunks: Uint8Array[] = [];
64
+
65
+ for (let i = 0; i < sizeInBytes; i += chunkSize) {
66
+ const size = Math.min(chunkSize, sizeInBytes - i);
67
+ const chunk = new Uint8Array(size);
68
+ chunk.fill("a".charCodeAt(0));
69
+ chunks.push(chunk);
70
+ }
71
+
72
+ const blob = new Blob(chunks, { type: "text/plain" });
73
+ return new File([blob], "largeMockFile.txt", {
74
+ type: "text/plain",
75
+ });
76
+ }
@@ -0,0 +1,25 @@
1
+ import { FileItem } from "../Item.types";
2
+
3
+ const MAX_MEGA_BYTES = 500;
4
+
5
+ export function formatFileSize(file: FileItem): string | null {
6
+ if (!file.size) {
7
+ return null;
8
+ }
9
+ const megaBytes = file.size / (1024 * 1024);
10
+
11
+ if (megaBytes <= MAX_MEGA_BYTES) {
12
+ return formatter.format(megaBytes);
13
+ }
14
+
15
+ return `> ${MAX_MEGA_BYTES} MB`;
16
+ }
17
+
18
+ const formatter = new Intl.NumberFormat("nb-NO", {
19
+ style: "unit",
20
+ unit: "megabyte",
21
+ minimumFractionDigits: 2,
22
+ maximumFractionDigits: 2,
23
+ // @ts-expect-error - Looks like roundingMode hasn't been added to TypeScript yet
24
+ roundingMode: "ceil",
25
+ });
@@ -0,0 +1,54 @@
1
+ import { useRef } from "react";
2
+ import { useMergeRefs } from "../../util/hooks";
3
+ import { FileUploadBaseProps } from "./FileUpload.types";
4
+ import { validateFiles } from "./utils/validate-files";
5
+
6
+ export interface UseFileUploadProps
7
+ extends Omit<FileUploadBaseProps, "fileLimit"> {
8
+ ref: React.ForwardedRef<HTMLInputElement>;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ export const useFileUpload = ({
13
+ ref,
14
+ accept,
15
+ onSelect,
16
+ validator,
17
+ maxSizeInBytes,
18
+ disabled,
19
+ }: UseFileUploadProps) => {
20
+ const inputRef = useRef<HTMLInputElement | null>(null);
21
+ const mergedRef = useMergeRefs(inputRef, ref);
22
+
23
+ const upload = (fileList: FileList) => {
24
+ const { files, partitionedFiles } = validateFiles(
25
+ Array.from(fileList),
26
+ accept,
27
+ validator,
28
+ maxSizeInBytes,
29
+ );
30
+
31
+ onSelect(files, partitionedFiles);
32
+ };
33
+
34
+ const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
35
+ const fileList = event.target.files;
36
+ if (!fileList) {
37
+ return;
38
+ }
39
+
40
+ if (!disabled) {
41
+ upload(fileList);
42
+ }
43
+
44
+ // Resets the value to make it is possible to upload the same file several consecutive times
45
+ event.target.value = "";
46
+ };
47
+
48
+ return {
49
+ upload,
50
+ onChange,
51
+ inputRef,
52
+ mergedRef,
53
+ };
54
+ };
@@ -0,0 +1,69 @@
1
+ import { isAcceptedFileType } from "./is-accepted-file-type";
2
+
3
+ const txtFile = () =>
4
+ new File(["foo"], "foo.txt", {
5
+ type: "text/plain",
6
+ });
7
+
8
+ describe("isAcceptedFileType", () => {
9
+ test("returns true when accept is undefined", () => {
10
+ expect(isAcceptedFileType(txtFile(), undefined)).toBe(true);
11
+ });
12
+
13
+ test("returns true when accept is empty string", () => {
14
+ expect(isAcceptedFileType(txtFile(), "")).toBe(true);
15
+ });
16
+
17
+ test("returns true when file matches accepted extensions", () => {
18
+ expect(isAcceptedFileType(txtFile(), ".txt, .pdf")).toBe(true);
19
+ });
20
+
21
+ test("works as expected when there is no space after comma in accept", () => {
22
+ expect(isAcceptedFileType(txtFile(), ".txt,.pdf")).toBe(true);
23
+ });
24
+
25
+ test("returns false when file does not match the accepted extensions", () => {
26
+ expect(isAcceptedFileType(txtFile(), ".xlsx, .pdf")).toBe(false);
27
+ });
28
+
29
+ test("returns true when file matches the accepted exact mime types", () => {
30
+ expect(isAcceptedFileType(txtFile(), "application/pdf, text/plain")).toBe(
31
+ true,
32
+ );
33
+ });
34
+
35
+ test("returns false when file does not the accepted exact mime types", () => {
36
+ expect(isAcceptedFileType(txtFile(), "application/pdf, text/csv")).toBe(
37
+ false,
38
+ );
39
+ });
40
+
41
+ test("returns true when file matches the accepted wildcard mime types", () => {
42
+ expect(isAcceptedFileType(txtFile(), "application/*, text/*")).toBe(true);
43
+ });
44
+
45
+ test("returns false when file does not the accepted wildcard mime types", () => {
46
+ expect(isAcceptedFileType(txtFile(), "application/*, image/*")).toBe(false);
47
+ });
48
+
49
+ test("returns true if matches extension, but not exact or wildcard mime type", () => {
50
+ expect(
51
+ isAcceptedFileType(txtFile(), "application/*, image/*, .txt, audio/mpeg"),
52
+ ).toBe(true);
53
+ });
54
+
55
+ test("returns true if matches exact mime type, but not extension or wildcard mime type", () => {
56
+ expect(
57
+ isAcceptedFileType(
58
+ txtFile(),
59
+ "application/*, text/plain, .jpg, audio/mpeg",
60
+ ),
61
+ ).toBe(true);
62
+ });
63
+
64
+ test("returns true if matches wildcard mime type, but not extension or exact mime type", () => {
65
+ expect(
66
+ isAcceptedFileType(txtFile(), "application/*, text/*, .jpg, audio/mpeg"),
67
+ ).toBe(true);
68
+ });
69
+ });
@@ -0,0 +1,25 @@
1
+ export function isAcceptedFileType(
2
+ file: File,
3
+ accept: string | undefined,
4
+ ): boolean {
5
+ if (!accept) {
6
+ return true;
7
+ }
8
+ const mimeType = file.type;
9
+ const acceptedTypes = accept.split(",");
10
+
11
+ return acceptedTypes.some((type) => {
12
+ const validType = type.trim();
13
+ const isExtensionType = validType.startsWith(".");
14
+ const isWildcardMimeType = validType.endsWith("/*");
15
+
16
+ if (isExtensionType) {
17
+ return file.name.toLowerCase().endsWith(validType.toLowerCase());
18
+ } else if (isWildcardMimeType) {
19
+ const baseMimeType = mimeType.replace(/\/.*$/, "");
20
+ const baseValidType = validType.replace(/\/.*$/, "");
21
+ return baseMimeType === baseValidType;
22
+ }
23
+ return mimeType === validType;
24
+ });
25
+ }
@@ -0,0 +1,26 @@
1
+ import { isAcceptedSize } from "./is-accepted-size";
2
+
3
+ describe("isAcceptedSize", () => {
4
+ it("should return true if maxSize is less than or equal to 0", () => {
5
+ const file = new File([new Array(5000).join("a")], "filename.txt", {
6
+ type: "text/plain",
7
+ });
8
+ expect(isAcceptedSize(file, -1)).toBe(true);
9
+ expect(isAcceptedSize(file, 0)).toBe(true);
10
+ });
11
+
12
+ it("should return true if file size is less than or equal to maxSize", () => {
13
+ const file = new File([new Array(5000).join("a")], "filename.txt", {
14
+ type: "text/plain",
15
+ });
16
+ expect(isAcceptedSize(file, 5000)).toBe(true);
17
+ expect(isAcceptedSize(file, 6000)).toBe(true);
18
+ });
19
+
20
+ it("should return false if file size is greater than maxSize", () => {
21
+ const file = new File([new Array(5000).join("a")], "filename.txt", {
22
+ type: "text/plain",
23
+ });
24
+ expect(isAcceptedSize(file, 4000)).toBe(false);
25
+ });
26
+ });
@@ -0,0 +1,7 @@
1
+ export function isAcceptedSize(file: File, maxSize: number = -1): boolean {
2
+ if (maxSize <= 0 || file.size <= maxSize) {
3
+ return true;
4
+ }
5
+
6
+ return false;
7
+ }