@oslokommune/punkt-react 16.13.3 → 16.15.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 (43) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/{dialog-polyfill.esm-1hPr0gl9-EorOF9Wq.js → dialog-polyfill.esm-BmVHGRTX-CuhXqEqJ.js} +1 -1
  3. package/dist/index.d.ts +50 -47
  4. package/dist/punkt-react.es.js +4560 -3038
  5. package/dist/punkt-react.umd.js +670 -274
  6. package/dist/shared-types/fileupload.d.ts +73 -0
  7. package/dist/shared-types/index.d.ts +2 -0
  8. package/dist/shared-utils/fileupload/announcements.d.ts +11 -0
  9. package/dist/shared-utils/fileupload/filename.d.ts +13 -0
  10. package/dist/shared-utils/fileupload/focus-trap.d.ts +12 -0
  11. package/dist/shared-utils/fileupload/formats.d.ts +30 -0
  12. package/dist/shared-utils/fileupload/index.d.ts +15 -0
  13. package/dist/shared-utils/fileupload/navigation.d.ts +5 -0
  14. package/dist/shared-utils/fileupload/size.d.ts +12 -0
  15. package/dist/shared-utils/fileupload/transfers.d.ts +17 -0
  16. package/dist/shared-utils/fileupload/truncate.d.ts +13 -0
  17. package/dist/shared-utils/fileupload/validation.d.ts +24 -0
  18. package/package.json +4 -4
  19. package/src/components/fileupload/DropZone.tsx +35 -22
  20. package/src/components/fileupload/FileUpload.test.tsx +165 -10
  21. package/src/components/fileupload/FileUpload.tsx +116 -184
  22. package/src/components/fileupload/QueueDisplay.test.tsx +51 -8
  23. package/src/components/fileupload/QueueDisplay.tsx +71 -99
  24. package/src/components/fileupload/QueueItemContent.tsx +120 -135
  25. package/src/components/fileupload/Subcomponents.tsx +175 -144
  26. package/src/components/fileupload/Truncate.test.tsx +65 -207
  27. package/src/components/fileupload/Truncate.tsx +24 -120
  28. package/src/components/fileupload/extensions/Comments.tsx +94 -106
  29. package/src/components/fileupload/extensions/Remove.tsx +3 -5
  30. package/src/components/fileupload/extensions/Rename.tsx +45 -42
  31. package/src/components/fileupload/hooks/index.ts +1 -0
  32. package/src/components/fileupload/hooks/useFileAttributes.ts +37 -29
  33. package/src/components/fileupload/hooks/useImagePreview.ts +3 -7
  34. package/src/components/fileupload/hooks/useOperationState.ts +29 -8
  35. package/src/components/fileupload/hooks/useRequiredFormValidation.ts +43 -0
  36. package/src/components/fileupload/hooks.ts +1 -0
  37. package/src/components/fileupload/types.ts +49 -32
  38. package/src/components/fileupload/utils.ts +3 -54
  39. package/src/components/inputwrapper/InputWrapper.tsx +4 -1
  40. package/src/components/modal/Modal.tsx +27 -15
  41. package/src/components/searchinput/SearchInput.tsx +14 -13
  42. package/src/components/types.ts +1 -1
  43. package/src/components/fileupload/texts.ts +0 -20
@@ -1,14 +1,23 @@
1
- import { forwardRef, ForwardedRef, useEffect, useState } from 'react'
1
+ import { RefObject, useEffect, useState } from 'react'
2
+
3
+ import { getDisplayFilename, trapTabInside } from 'shared-utils/fileupload'
2
4
 
3
5
  import { PktButton, PktIcon, PktProgressbar } from '..'
4
6
  import { PktModal } from '../modal/Modal'
5
7
  import { Truncate } from './Truncate'
6
- import { TFileAndTransfer, TItemRenderer, TQueueItemOperation, TTransferItemInProgress } from './types'
8
+ import { TFileAndTransfer, TQueueItemOperation, TQueueOperationContext, TTransferItemInProgress } from './types'
7
9
  import { formatFileSize } from './utils'
8
10
 
9
- const CancelTransferButton = (props: { onClick: () => void; label?: string; ariaLabel?: string }) => (
11
+ const CancelTransferButton = (props: {
12
+ onClick: () => void
13
+ label?: string
14
+ ariaLabel?: string
15
+ disabled?: boolean
16
+ }) => (
10
17
  <button
18
+ type="button"
11
19
  onClick={props.onClick}
20
+ disabled={props.disabled}
12
21
  className="pkt-fileupload__queue-display__item__cancel-button"
13
22
  aria-label={props.ariaLabel || 'Avbryt opplasting'}
14
23
  >
@@ -16,30 +25,20 @@ const CancelTransferButton = (props: { onClick: () => void; label?: string; aria
16
25
  </button>
17
26
  )
18
27
 
19
- const LoadingText = () => {
20
- const [dotCount, setDotCount] = useState(1)
21
-
22
- useEffect(() => {
23
- const interval = setInterval(() => {
24
- setDotCount((prev) => (prev >= 3 ? 1 : prev + 1))
25
- }, 400)
26
-
27
- return () => clearInterval(interval)
28
- }, [])
29
-
30
- return (
31
- <span className="pkt-fileupload__queue-display__item__loading-text" aria-label="Laster opp fil">
32
- Laster opp{'.'.repeat(dotCount)}
33
- </span>
34
- )
35
- }
28
+ const LoadingText = () => (
29
+ <span className="pkt-fileupload__queue-display__item__loading-text" aria-label="Laster opp fil">
30
+ Laster opp
31
+ </span>
32
+ )
36
33
 
37
34
  export const TransferInProgress = ({
38
35
  cancelTransfer,
39
36
  transferItem,
37
+ disabled = false,
40
38
  }: {
41
39
  transferItem: TTransferItemInProgress
42
40
  cancelTransfer: () => void
41
+ disabled?: boolean
43
42
  }) => {
44
43
  const showProgressBar = transferItem.showProgress ?? transferItem.progress !== 0
45
44
 
@@ -51,9 +50,13 @@ export const TransferInProgress = ({
51
50
  aria-hidden="true"
52
51
  />
53
52
  <p className="pkt-fileupload__queue-display__item__title" aria-label="Filnavn">
54
- <Truncate>{transferItem.attributes['targetFilename']}</Truncate>
53
+ <Truncate>{getDisplayFilename(transferItem)}</Truncate>
54
+ <span className="pkt-fileupload__queue-display__item__filesize">
55
+ {' '}
56
+ ({formatFileSize(transferItem.file.size)})
57
+ </span>
55
58
  </p>
56
- <CancelTransferButton onClick={cancelTransfer} />
59
+ <CancelTransferButton onClick={cancelTransfer} disabled={disabled} />
57
60
  {showProgressBar ? (
58
61
  <>
59
62
  <PktProgressbar
@@ -63,9 +66,6 @@ export const TransferInProgress = ({
63
66
  statusPlacement={'following'}
64
67
  className="pkt-fileupload__queue-display__item__progress"
65
68
  />
66
- <span className="pkt-fileupload__queue-display__item__filesize">
67
- {formatFileSize(transferItem.file.size)}
68
- </span>
69
69
  <span className="pkt-fileupload__queue-display__item__percentage" aria-hidden="true">
70
70
  {`${Math.round(transferItem.progress * 100)}%`}
71
71
  </span>
@@ -77,13 +77,27 @@ export const TransferInProgress = ({
77
77
  )
78
78
  }
79
79
 
80
+ const TitleWithSize = ({ transferItem }: { transferItem: TFileAndTransfer }) => (
81
+ <p className="pkt-fileupload__queue-display__item__title">
82
+ <Truncate>{getDisplayFilename(transferItem)}</Truncate>
83
+ {transferItem.file && (
84
+ <span className="pkt-fileupload__queue-display__item__filesize">
85
+ {' '}
86
+ ({formatFileSize(transferItem.file.size)})
87
+ </span>
88
+ )}
89
+ </p>
90
+ )
91
+
80
92
  // Error state with progress bar (when showProgress was true during upload)
81
93
  const TransferErrorWithProgress = ({
82
94
  transferItem,
83
95
  onRemove,
96
+ disabled,
84
97
  }: {
85
98
  transferItem: TFileAndTransfer
86
99
  onRemove: () => void
100
+ disabled?: boolean
87
101
  }) => {
88
102
  // Use lastProgress to show where the upload failed (default to 100% if not available)
89
103
  const progressValue = (transferItem.lastProgress ?? 1) * 100
@@ -95,10 +109,13 @@ const TransferErrorWithProgress = ({
95
109
  name="alert-error"
96
110
  aria-hidden="true"
97
111
  />
98
- <p className="pkt-fileupload__queue-display__item__title">
99
- <Truncate>{transferItem.attributes['targetFilename']}</Truncate>
100
- </p>
101
- <CancelTransferButton onClick={onRemove} label="Fjern" ariaLabel="Fjern fil som feilet under opplasting" />
112
+ <TitleWithSize transferItem={transferItem} />
113
+ <CancelTransferButton
114
+ onClick={onRemove}
115
+ label="Fjern"
116
+ ariaLabel="Fjern fil som feilet under opplasting"
117
+ disabled={disabled}
118
+ />
102
119
  <PktProgressbar
103
120
  valueCurrent={progressValue}
104
121
  statusType={'none'}
@@ -106,7 +123,6 @@ const TransferErrorWithProgress = ({
106
123
  statusPlacement={'following'}
107
124
  className="pkt-fileupload__queue-display__item__progress pkt-fileupload__queue-display__item__progress--error"
108
125
  />
109
- <span className="pkt-fileupload__queue-display__item__filesize">{formatFileSize(transferItem.file.size)}</span>
110
126
  {transferItem.errorMessage && (
111
127
  <span className="pkt-fileupload__queue-display__item__error-message">{transferItem.errorMessage}</span>
112
128
  )}
@@ -118,9 +134,11 @@ const TransferErrorWithProgress = ({
118
134
  const TransferErrorIndeterminate = ({
119
135
  transferItem,
120
136
  onRemove,
137
+ disabled,
121
138
  }: {
122
139
  transferItem: TFileAndTransfer
123
140
  onRemove: () => void
141
+ disabled?: boolean
124
142
  }) => (
125
143
  <>
126
144
  <PktIcon
@@ -128,10 +146,13 @@ const TransferErrorIndeterminate = ({
128
146
  name="alert-error"
129
147
  aria-hidden="true"
130
148
  />
131
- <p className="pkt-fileupload__queue-display__item__title">
132
- <Truncate>{transferItem.attributes['targetFilename']}</Truncate>
133
- </p>
134
- <CancelTransferButton onClick={onRemove} label="Fjern" ariaLabel="Fjern fil som feilet under opplasting" />
149
+ <TitleWithSize transferItem={transferItem} />
150
+ <CancelTransferButton
151
+ onClick={onRemove}
152
+ label="Fjern"
153
+ ariaLabel="Fjern fil som feilet under opplasting"
154
+ disabled={disabled}
155
+ />
135
156
  {transferItem.errorMessage && (
136
157
  <span className="pkt-fileupload__queue-display__item__error-message">{transferItem.errorMessage}</span>
137
158
  )}
@@ -139,41 +160,46 @@ const TransferErrorIndeterminate = ({
139
160
  )
140
161
 
141
162
  // Main error component - chooses display based on showProgress
142
- export const TransferError = ({ transferItem, onRemove }: { transferItem: TFileAndTransfer; onRemove: () => void }) => {
163
+ export const TransferError = ({
164
+ transferItem,
165
+ onRemove,
166
+ disabled,
167
+ }: {
168
+ transferItem: TFileAndTransfer
169
+ onRemove: () => void
170
+ disabled?: boolean
171
+ }) => {
143
172
  // If showProgress was true, show error with progress bar, otherwise show indeterminate error
144
173
  if (transferItem.showProgress) {
145
- return <TransferErrorWithProgress transferItem={transferItem} onRemove={onRemove} />
174
+ return <TransferErrorWithProgress transferItem={transferItem} onRemove={onRemove} disabled={disabled} />
146
175
  }
147
- return <TransferErrorIndeterminate transferItem={transferItem} onRemove={onRemove} />
176
+ return <TransferErrorIndeterminate transferItem={transferItem} onRemove={onRemove} disabled={disabled} />
148
177
  }
149
178
  export const OperationButton = ({
150
179
  operation,
151
- onActivate,
152
- transferItem,
180
+ context,
153
181
  }: {
154
182
  operation: TQueueItemOperation
155
- onActivate: (fileId: string, operation: TQueueItemOperation) => void
156
- transferItem: TFileAndTransfer
183
+ context: TQueueOperationContext
157
184
  }) => {
158
- const title = typeof operation.title === 'function' ? operation.title(transferItem) : operation.title
185
+ const title = typeof operation.title === 'function' ? operation.title(context.file) : operation.title
159
186
 
160
187
  // Don't render button if title is empty (e.g., comment exists, show icons instead)
161
188
  if (!title) return null
162
189
 
163
190
  const onClick = () => {
164
191
  if (operation.renderInlineUI || operation.renderExtendedUI) {
165
- onActivate(transferItem.fileId, operation)
166
- }
167
- if (operation.onClick) {
168
- operation.onClick(transferItem)
192
+ context.activate()
169
193
  }
194
+ operation.onClick?.(context)
170
195
  }
171
196
  return (
172
197
  <button
173
198
  type="button"
174
199
  className={'pkt-fileupload__queue-display__item__operation'}
175
200
  onClick={onClick}
176
- aria-label={typeof operation.ariaLabel === 'function' ? operation.ariaLabel(transferItem) : operation.ariaLabel}
201
+ disabled={context.disabled}
202
+ aria-label={typeof operation.ariaLabel === 'function' ? operation.ariaLabel(context.file) : operation.ariaLabel}
177
203
  >
178
204
  {title}
179
205
  </button>
@@ -192,10 +218,7 @@ export const FilenameRenderer = ({
192
218
  className="pkt-fileupload__queue-display__item__icon pkt-icon--medium"
193
219
  aria-hidden="true"
194
220
  />
195
- <p className={'pkt-fileupload__queue-display__item__title'}>
196
- <Truncate>{transferItem.attributes['targetFilename']}</Truncate>
197
- </p>
198
- <span className="pkt-fileupload__queue-display__item__filesize">{formatFileSize(transferItem.file.size)}</span>
221
+ <TitleWithSize transferItem={transferItem} />
199
222
  </>
200
223
  )
201
224
 
@@ -262,9 +285,9 @@ export const ThumbnailRenderer = ({
262
285
  )}
263
286
  {img}
264
287
  </PktButton>
265
- <Truncate className={'pkt-fileupload__queue-display__item__thumbnail__title'}>
266
- {transferItem.attributes['targetFilename']}
267
- </Truncate>
288
+ <span className="pkt-fileupload__queue-display__item__thumbnail__title">
289
+ <Truncate>{transferItem.attributes['targetFilename']}</Truncate>
290
+ </span>
268
291
  </div>
269
292
  )
270
293
  }
@@ -277,107 +300,115 @@ interface IImagePreviewModal {
277
300
  currentIndex: number
278
301
  onClose: () => void
279
302
  onNavigate: (direction: 'prev' | 'next') => void
303
+ modalRef: RefObject<HTMLDialogElement>
280
304
  }
281
305
 
282
- export const ImagePreviewModal = forwardRef(
283
- ({ isOpen, images, currentIndex, onClose, onNavigate }: IImagePreviewModal, ref: ForwardedRef<HTMLDialogElement>) => {
284
- const [imageUrl, setImageUrl] = useState<string | null>(null)
285
- const [imageLoadFailed, setImageLoadFailed] = useState(false)
286
- const currentImage = images[currentIndex]
287
- const hasMultipleImages = images.length > 1
306
+ export const ImagePreviewModal = ({
307
+ isOpen,
308
+ images,
309
+ currentIndex,
310
+ onClose,
311
+ onNavigate,
312
+ modalRef,
313
+ }: IImagePreviewModal) => {
314
+ const [imageUrl, setImageUrl] = useState<string | null>(null)
315
+ const [imageLoadFailed, setImageLoadFailed] = useState(false)
316
+ const currentImage = images[currentIndex]
317
+ const hasMultipleImages = images.length > 1
288
318
 
289
- useEffect(() => {
290
- setImageLoadFailed(false)
319
+ useEffect(() => {
320
+ setImageLoadFailed(false)
291
321
 
292
- if (currentImage && currentImage.file.type.startsWith('image/')) {
293
- const url = URL.createObjectURL(currentImage.file)
294
- setImageUrl(url)
295
- return () => URL.revokeObjectURL(url)
322
+ if (currentImage && currentImage.file.type.startsWith('image/')) {
323
+ const url = URL.createObjectURL(currentImage.file)
324
+ setImageUrl(url)
325
+ return () => URL.revokeObjectURL(url)
326
+ }
327
+ setImageUrl(null)
328
+ }, [currentImage])
329
+
330
+ useEffect(() => {
331
+ if (!isOpen) return
332
+
333
+ const handleKeyDown = (event: KeyboardEvent) => {
334
+ if (event.key === 'Tab') {
335
+ trapTabInside(event, modalRef.current)
336
+ return
296
337
  }
297
- setImageUrl(null)
298
- }, [currentImage])
299
-
300
- // Keyboard navigation
301
- useEffect(() => {
302
- if (!isOpen) return
303
-
304
- const handleKeyDown = (e: KeyboardEvent) => {
305
- switch (e.key) {
306
- case 'ArrowLeft':
307
- e.preventDefault()
308
- onNavigate('prev')
309
- break
310
- case 'ArrowRight':
311
- e.preventDefault()
312
- onNavigate('next')
313
- break
314
- case 'Escape':
315
- e.preventDefault()
316
- onClose()
317
- break
318
- }
338
+ switch (event.key) {
339
+ case 'ArrowLeft':
340
+ event.preventDefault()
341
+ onNavigate('prev')
342
+ break
343
+ case 'ArrowRight':
344
+ event.preventDefault()
345
+ onNavigate('next')
346
+ break
347
+ case 'Escape':
348
+ event.preventDefault()
349
+ onClose()
350
+ break
319
351
  }
352
+ }
320
353
 
321
- window.addEventListener('keydown', handleKeyDown)
322
- return () => window.removeEventListener('keydown', handleKeyDown)
323
- }, [isOpen, onNavigate, onClose])
324
-
325
- if (!currentImage) return null
354
+ window.addEventListener('keydown', handleKeyDown)
355
+ return () => window.removeEventListener('keydown', handleKeyDown)
356
+ }, [isOpen, onNavigate, onClose, modalRef])
326
357
 
327
- return (
328
- <PktModal
329
- ref={ref}
330
- headingText={currentImage.attributes['targetFilename']}
331
- closeOnBackdropClick
332
- className="pkt-fileupload__image-preview-modal"
333
- >
334
- <div className="pkt-fileupload__image-preview">
335
- {hasMultipleImages && (
336
- <PktButton
337
- variant="icon-only"
338
- iconName="chevron-thin-left"
339
- skin="secondary"
340
- size="small"
341
- className="pkt-fileupload__image-preview__nav pkt-fileupload__image-preview__nav--prev"
342
- onClick={() => onNavigate('prev')}
343
- aria-label={`Forrige bilde ${currentIndex} / ${images.length}`}
344
- />
345
- )}
358
+ if (!currentImage) return null
346
359
 
347
- <div className="pkt-fileupload__image-preview__content">
348
- {imageUrl && !imageLoadFailed ? (
349
- <img
350
- src={imageUrl}
351
- alt={`${currentImage.attributes['targetFilename']} - ${currentIndex + 1} av ${images.length}`}
352
- className="pkt-fileupload__image-preview__image"
353
- onError={() => setImageLoadFailed(true)}
354
- />
355
- ) : (
356
- <PktIcon name="picture" className="pkt-fileupload__queue-display__item__icon pkt-icon--medium" />
357
- )}
358
- </div>
359
-
360
- {hasMultipleImages && (
361
- <span className="pkt-fileupload__image-preview__counter">
362
- {currentIndex + 1} / {images.length}
363
- </span>
364
- )}
360
+ return (
361
+ <PktModal
362
+ ref={modalRef}
363
+ headingText={currentImage.attributes['targetFilename']}
364
+ closeOnBackdropClick
365
+ className="pkt-fileupload__image-preview-modal"
366
+ onClose={onClose}
367
+ >
368
+ <div className="pkt-fileupload__image-preview">
369
+ {hasMultipleImages && (
370
+ <PktButton
371
+ variant="icon-only"
372
+ iconName="chevron-thin-left"
373
+ skin="secondary"
374
+ size="medium"
375
+ className="pkt-fileupload__image-preview__nav pkt-fileupload__image-preview__nav--prev"
376
+ onClick={() => onNavigate('prev')}
377
+ aria-label={`Forrige bilde ${currentIndex} / ${images.length}`}
378
+ />
379
+ )}
365
380
 
366
- {hasMultipleImages && (
367
- <PktButton
368
- variant="icon-only"
369
- iconName="chevron-thin-right"
370
- skin="secondary"
371
- size="small"
372
- className="pkt-fileupload__image-preview__nav pkt-fileupload__image-preview__nav--next"
373
- onClick={() => onNavigate('next')}
374
- aria-label={`Neste bilde ${currentIndex + 2} / ${images.length}`}
381
+ <div className="pkt-fileupload__image-preview__content">
382
+ {imageUrl && !imageLoadFailed ? (
383
+ <img
384
+ src={imageUrl}
385
+ alt={`${currentImage.attributes['targetFilename']} - ${currentIndex + 1} av ${images.length}`}
386
+ className="pkt-fileupload__image-preview__image"
387
+ onError={() => setImageLoadFailed(true)}
375
388
  />
389
+ ) : (
390
+ <PktIcon name="picture" className="pkt-fileupload__queue-display__item__icon pkt-icon--medium" />
376
391
  )}
377
392
  </div>
378
- </PktModal>
379
- )
380
- },
381
- )
382
393
 
383
- ImagePreviewModal.displayName = 'ImagePreviewModal'
394
+ {hasMultipleImages && (
395
+ <span className="pkt-fileupload__image-preview__counter">
396
+ {currentIndex + 1} / {images.length}
397
+ </span>
398
+ )}
399
+
400
+ {hasMultipleImages && (
401
+ <PktButton
402
+ variant="icon-only"
403
+ iconName="chevron-thin-right"
404
+ skin="secondary"
405
+ size="medium"
406
+ className="pkt-fileupload__image-preview__nav pkt-fileupload__image-preview__nav--next"
407
+ onClick={() => onNavigate('next')}
408
+ aria-label={`Neste bilde ${currentIndex + 2} / ${images.length}`}
409
+ />
410
+ )}
411
+ </div>
412
+ </PktModal>
413
+ )
414
+ }