@oslokommune/punkt-react 15.3.0 → 15.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oslokommune/punkt-react",
3
- "version": "15.3.0",
3
+ "version": "15.4.0",
4
4
  "description": "React komponentbibliotek til Punkt, et designsystem laget av Oslo Origo",
5
5
  "homepage": "https://punkt.oslo.kommune.no",
6
6
  "author": "Team Designsystem, Oslo Origo",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@lit-labs/ssr-dom-shim": "^1.2.1",
41
41
  "@lit/react": "^1.0.7",
42
- "@oslokommune/punkt-elements": "^15.2.0",
42
+ "@oslokommune/punkt-elements": "^15.4.0",
43
43
  "classnames": "^2.5.1",
44
44
  "prettier": "^3.3.3",
45
45
  "react-hook-form": "^7.53.0"
@@ -50,7 +50,7 @@
50
50
  "@eslint/eslintrc": "^3.3.3",
51
51
  "@eslint/js": "^9.37.0",
52
52
  "@oslokommune/punkt-assets": "^15.0.0",
53
- "@oslokommune/punkt-css": "^15.2.0",
53
+ "@oslokommune/punkt-css": "^15.4.0",
54
54
  "@testing-library/jest-dom": "^6.5.0",
55
55
  "@testing-library/react": "^16.0.1",
56
56
  "@testing-library/user-event": "^14.5.2",
@@ -109,5 +109,5 @@
109
109
  "url": "https://github.com/oslokommune/punkt/issues"
110
110
  },
111
111
  "license": "MIT",
112
- "gitHead": "0730d5feffa4c8d274daad9c7317dcf82b0f5b4f"
112
+ "gitHead": "0323bc56dc8636595ea48dd88dd966d363aac371"
113
113
  }
@@ -16,6 +16,8 @@ import { PktIcon } from '..'
16
16
  import { uiMultipleTexts, uiSingleTexts, uiThumbnailMultipleTexts, uiThumbnailSingleTexts } from './texts'
17
17
  import { FileItem, TFileItemList, TUploadStrategy } from './types'
18
18
 
19
+ const DEFAULT_FORMATS_HELP_TEXT = '.PDF, .JPEG, .JPG, .PNG, .HEIC, .DOC, .DOCX, .ODT'
20
+
19
21
  /**
20
22
  * Props for the internal `DropZone` building block.
21
23
  *
@@ -61,7 +63,7 @@ export const DropZone = forwardRef<HTMLInputElement, IDropZoneProps>(
61
63
  onFilesAdded = () => {},
62
64
  name,
63
65
  uploadStrategy,
64
- accept = '.pdf, .jpeg, .jpg, .png, .heic, .doc, .docx, .odt',
66
+ accept,
65
67
  isThumbnailView = false,
66
68
  disabled = false,
67
69
  srAnnouncementIds,
@@ -71,9 +73,18 @@ export const DropZone = forwardRef<HTMLInputElement, IDropZoneProps>(
71
73
  ) => {
72
74
  const fileInputRef = useRef<HTMLInputElement>(null)
73
75
 
76
+ const resolvedAccept = typeof accept === 'string' && accept.trim().length > 0 ? accept : undefined
74
77
  const acceptedFormatsReadableString = useMemo(
75
- () => (accept?.split(/\s*,\s*/).map((format) => format.trim()) || []).join(', ').toUpperCase(),
76
- [accept],
78
+ () =>
79
+ resolvedAccept
80
+ ? resolvedAccept
81
+ .split(/\s*,\s*/)
82
+ .map((format) => format.trim())
83
+ .filter(Boolean)
84
+ .join(', ')
85
+ .toUpperCase()
86
+ : DEFAULT_FORMATS_HELP_TEXT,
87
+ [resolvedAccept],
77
88
  )
78
89
 
79
90
  useImperativeHandle(forwardedRef, () => fileInputRef.current! as HTMLInputElement)
@@ -181,7 +192,7 @@ export const DropZone = forwardRef<HTMLInputElement, IDropZoneProps>(
181
192
  ref={fileInputRef}
182
193
  multiple={multiple}
183
194
  onChange={filesSelectedInDialog}
184
- accept={accept}
195
+ accept={resolvedAccept}
185
196
  disabled={disabled}
186
197
  name={(uploadStrategy === 'form' && name) || undefined} // Ikke sett name hvis uploadStrategy er 'custom' - ignorerer ved POST
187
198
  aria-label={multiple ? 'Velg filer' : 'Velg fil'}
@@ -240,6 +240,21 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
240
240
 
241
241
  const itemRenderer = typeof itemRendererProp === 'string' ? ItemRenderers[itemRendererProp] : itemRendererProp
242
242
  const isThumbnailView = itemRendererProp === 'thumbnail'
243
+ const explicitAccept = typeof props.accept === 'string' ? props.accept.trim() : undefined
244
+ const acceptFromAllowedFormats = useMemo(() => {
245
+ if (!allowedFormats || allowedFormats.length === 0) return undefined
246
+ return allowedFormats
247
+ .map((format) => format.trim())
248
+ .filter(Boolean)
249
+ .map((format) => {
250
+ const normalized = format.toLowerCase()
251
+ if (normalized.includes('/')) return normalized
252
+ if (normalized.startsWith('.')) return normalized
253
+ return `.${normalized}`
254
+ })
255
+ .join(', ')
256
+ }, [allowedFormats])
257
+ const resolvedAcceptForDropZone = explicitAccept || acceptFromAllowedFormats
243
258
 
244
259
  const effectiveRenameEnabled = renameFilesEnabled && !isThumbnailView
245
260
  const effectiveCommentsEnabled = addCommentsEnabled && !isThumbnailView
@@ -375,7 +390,7 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
375
390
  multiple={multiple}
376
391
  uploadStrategy={uploadStrategy}
377
392
  ref={forwardedRef}
378
- accept={isThumbnailView ? '.jpeg, .jpg, .png, .gif, .webp, .heic' : props.accept}
393
+ accept={isThumbnailView ? '.jpeg, .jpg, .png, .gif, .webp, .heic' : resolvedAcceptForDropZone}
379
394
  isThumbnailView={isThumbnailView}
380
395
  disabled={disabled}
381
396
  srAnnouncementIds={srAnnouncementIds}
@@ -390,6 +405,7 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
390
405
  cancelTransfer={onFileRemoved}
391
406
  truncateTail={truncateTail}
392
407
  transfers={transfers}
408
+ uploadStrategy={uploadStrategy}
393
409
  ItemRenderer={itemRenderer}
394
410
  enableImagePreview={isThumbnailView && enableImagePreview}
395
411
  queueItemOperations={queueItemExtensions
@@ -411,7 +427,7 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
411
427
  hasError={hasError}
412
428
  optionalTag={optionalTag}
413
429
  requiredTag={requiredTag}
414
- className="pkt-fileupload-wrapper"
430
+ className={classNames('pkt-fileupload-wrapper', { 'pkt-fileupload-wrapper--full-width': fullWidth })}
415
431
  >
416
432
  {fileUploadContent}
417
433
  </PktInputWrapper>
@@ -7,6 +7,11 @@ import { FilenameRenderer, ImagePreviewModal, ThumbnailRenderer } from './Subcom
7
7
  import { TruncateContext } from './Truncate'
8
8
  import { TFileAndTransfer, FileItem, TFileItemList, TFileTransfer, TItemRenderer, TQueueItemOperation } from './types'
9
9
 
10
+ const isImageFile = (item: TFileAndTransfer): boolean => {
11
+ if (item.file.type?.startsWith('image/')) return true
12
+ return /\.(jpe?g|png|gif|webp|heic|heif|bmp|svg)$/i.test(item.file.name || '')
13
+ }
14
+
10
15
  // ============================================
11
16
  // Helper: Transform files with transfer status
12
17
  // ============================================
@@ -19,13 +24,17 @@ import { TFileAndTransfer, FileItem, TFileItemList, TFileTransfer, TItemRenderer
19
24
  * - then errors
20
25
  * - then everything else
21
26
  */
22
- const useFilesAndTransfers = (files: TFileItemList, transfers: TFileTransfer[]): TFileAndTransfer[] => {
27
+ const useFilesAndTransfers = (
28
+ files: TFileItemList,
29
+ transfers: TFileTransfer[],
30
+ uploadStrategy: 'form' | 'custom',
31
+ ): TFileAndTransfer[] => {
23
32
  return useMemo(() => {
24
33
  const mapped = files.map((fileItem: FileItem) => {
25
34
  const transfer = transfers.find((t) => t.fileId === fileItem.fileId)
26
35
  return {
27
36
  ...fileItem,
28
- progress: transfer?.progress ?? 'queued',
37
+ progress: transfer?.progress ?? (uploadStrategy === 'form' ? 'done' : 'queued'),
29
38
  errorMessage: transfer?.errorMessage,
30
39
  showProgress: transfer?.showProgress,
31
40
  lastProgress: transfer?.lastProgress,
@@ -35,7 +44,7 @@ const useFilesAndTransfers = (files: TFileItemList, transfers: TFileTransfer[]):
35
44
  // Sort order: in-progress first, then errors, then the rest
36
45
  const priority = { 'in-progress': 0, error: 1, idle: 2 } as const
37
46
  return mapped.sort((a, b) => priority[getProgressState(a.progress)] - priority[getProgressState(b.progress)])
38
- }, [files, transfers])
47
+ }, [files, transfers, uploadStrategy])
39
48
  }
40
49
 
41
50
  // ============================================
@@ -46,7 +55,7 @@ const useFilesAndTransfers = (files: TFileItemList, transfers: TFileTransfer[]):
46
55
  const usePreviewableImages = (filesAndTransfers: TFileAndTransfer[], enabled: boolean): TFileAndTransfer[] => {
47
56
  return useMemo(() => {
48
57
  if (!enabled) return []
49
- return filesAndTransfers.filter((item) => item.progress === 'done' && item.file.type.startsWith('image/'))
58
+ return filesAndTransfers.filter((item) => item.progress === 'done' && isImageFile(item))
50
59
  }, [filesAndTransfers, enabled])
51
60
  }
52
61
 
@@ -74,6 +83,8 @@ interface IQueueDisplay {
74
83
  truncateTail?: number
75
84
  /** Enable image preview modal (only affects thumbnail view). */
76
85
  enableImagePreview?: boolean
86
+ /** Upload mode decides default queue state when no transfer exists. */
87
+ uploadStrategy?: 'form' | 'custom'
77
88
  }
78
89
 
79
90
  // ============================================
@@ -88,9 +99,10 @@ export const QueueDisplay: FC<IQueueDisplay> = ({
88
99
  ItemRenderer = FilenameRenderer,
89
100
  truncateTail,
90
101
  enableImagePreview = false,
102
+ uploadStrategy = 'form',
91
103
  }) => {
92
104
  // Transform data
93
- const filesAndTransfers = useFilesAndTransfers(files, transfers)
105
+ const filesAndTransfers = useFilesAndTransfers(files, transfers, uploadStrategy)
94
106
  const previewableImages = usePreviewableImages(filesAndTransfers, enableImagePreview)
95
107
 
96
108
  // State management
@@ -4,6 +4,11 @@ import { PktIcon } from '..'
4
4
  import { OperationButton, TransferError, TransferInProgress } from './Subcomponents'
5
5
  import { TFileAndTransfer, TFileId, TItemRenderer, TQueueItemOperation, TTransferItemInProgress } from './types'
6
6
 
7
+ const isImageFile = (item: TFileAndTransfer): boolean => {
8
+ if (item.file.type?.startsWith('image/')) return true
9
+ return /\.(jpe?g|png|gif|webp|heic|heif|bmp|svg)$/i.test(item.file.name || '')
10
+ }
11
+
7
12
  type TProgressState = 'in-progress' | 'error' | 'idle'
8
13
 
9
14
  export const getProgressState = (progress: TFileAndTransfer['progress']): TProgressState => {
@@ -170,8 +175,7 @@ export const QueueItemContent: FC<IQueueItemContent> = ({
170
175
  onOpenPreview,
171
176
  }) => {
172
177
  const state = getProgressState(transferItem.progress)
173
- const isPreviewable =
174
- enableImagePreview && transferItem.progress === 'done' && transferItem.file.type.startsWith('image/')
178
+ const isPreviewable = enableImagePreview && transferItem.progress === 'done' && isImageFile(transferItem)
175
179
 
176
180
  switch (state) {
177
181
  case 'in-progress':
@@ -11,6 +11,7 @@ export { PktCombobox } from './combobox/Combobox'
11
11
  export { PktConsent } from './consent/Consent'
12
12
  export { PktDatepicker } from './datepicker/Datepicker'
13
13
  export { PktFileUpload } from './fileupload/FileUpload'
14
+ export { ItemRenderers } from './fileupload/QueueDisplay'
14
15
  export { PktFooter } from './footer/Footer'
15
16
  export { PktFooterSimple } from './footerSimple/FooterSimple'
16
17
  export { PktHeader } from './header/Header'
@@ -43,3 +44,4 @@ export { PktTag } from './tag/Tag'
43
44
  export { PktTextarea } from './textarea/Textarea'
44
45
  export { PktTextinput } from './textinput/Textinput'
45
46
  export * from './interfaces'
47
+ export * from './types'
@@ -0,0 +1,14 @@
1
+ export { FileItem } from './fileupload/types'
2
+ export type {
3
+ TFileAndTransfer,
4
+ TFileAttribute,
5
+ TFileAttributes,
6
+ TFileId,
7
+ TFileItemList,
8
+ TFileTransfer,
9
+ TItemRenderer,
10
+ TQueueItemExtension,
11
+ TQueueItemOperation,
12
+ TTransferItemInProgress,
13
+ TUploadStrategy,
14
+ } from './fileupload/types'