@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.
- package/CHANGELOG.md +35 -0
- package/dist/{dialog-polyfill.esm-1hPr0gl9-EorOF9Wq.js → dialog-polyfill.esm-BmVHGRTX-CuhXqEqJ.js} +1 -1
- package/dist/index.d.ts +50 -47
- package/dist/punkt-react.es.js +4560 -3038
- package/dist/punkt-react.umd.js +670 -274
- package/dist/shared-types/fileupload.d.ts +73 -0
- package/dist/shared-types/index.d.ts +2 -0
- package/dist/shared-utils/fileupload/announcements.d.ts +11 -0
- package/dist/shared-utils/fileupload/filename.d.ts +13 -0
- package/dist/shared-utils/fileupload/focus-trap.d.ts +12 -0
- package/dist/shared-utils/fileupload/formats.d.ts +30 -0
- package/dist/shared-utils/fileupload/index.d.ts +15 -0
- package/dist/shared-utils/fileupload/navigation.d.ts +5 -0
- package/dist/shared-utils/fileupload/size.d.ts +12 -0
- package/dist/shared-utils/fileupload/transfers.d.ts +17 -0
- package/dist/shared-utils/fileupload/truncate.d.ts +13 -0
- package/dist/shared-utils/fileupload/validation.d.ts +24 -0
- package/package.json +4 -4
- package/src/components/fileupload/DropZone.tsx +35 -22
- package/src/components/fileupload/FileUpload.test.tsx +165 -10
- package/src/components/fileupload/FileUpload.tsx +116 -184
- package/src/components/fileupload/QueueDisplay.test.tsx +51 -8
- package/src/components/fileupload/QueueDisplay.tsx +71 -99
- package/src/components/fileupload/QueueItemContent.tsx +120 -135
- package/src/components/fileupload/Subcomponents.tsx +175 -144
- package/src/components/fileupload/Truncate.test.tsx +65 -207
- package/src/components/fileupload/Truncate.tsx +24 -120
- package/src/components/fileupload/extensions/Comments.tsx +94 -106
- package/src/components/fileupload/extensions/Remove.tsx +3 -5
- package/src/components/fileupload/extensions/Rename.tsx +45 -42
- package/src/components/fileupload/hooks/index.ts +1 -0
- package/src/components/fileupload/hooks/useFileAttributes.ts +37 -29
- package/src/components/fileupload/hooks/useImagePreview.ts +3 -7
- package/src/components/fileupload/hooks/useOperationState.ts +29 -8
- package/src/components/fileupload/hooks/useRequiredFormValidation.ts +43 -0
- package/src/components/fileupload/hooks.ts +1 -0
- package/src/components/fileupload/types.ts +49 -32
- package/src/components/fileupload/utils.ts +3 -54
- package/src/components/inputwrapper/InputWrapper.tsx +4 -1
- package/src/components/modal/Modal.tsx +27 -15
- package/src/components/searchinput/SearchInput.tsx +14 -13
- package/src/components/types.ts +1 -1
- package/src/components/fileupload/texts.ts +0 -20
|
@@ -27,35 +27,39 @@ import {
|
|
|
27
27
|
useState,
|
|
28
28
|
} from 'react'
|
|
29
29
|
|
|
30
|
+
import { defaultFileUploadStrings } from 'shared-types'
|
|
31
|
+
import {
|
|
32
|
+
countFileTransferStates,
|
|
33
|
+
resolveAcceptAttribute,
|
|
34
|
+
THUMBNAIL_ACCEPT,
|
|
35
|
+
validateFile as sharedValidateFile,
|
|
36
|
+
} from 'shared-utils/fileupload'
|
|
37
|
+
|
|
30
38
|
import { PktAlert } from '../alert/Alert'
|
|
31
39
|
import { PktInputWrapper } from '../inputwrapper/InputWrapper'
|
|
32
40
|
import { DropZone } from './DropZone'
|
|
33
41
|
import { addCommentOperation } from './extensions/Comments'
|
|
34
42
|
import { removeFileOperation } from './extensions/Remove'
|
|
35
43
|
import { renameFileOperation } from './extensions/Rename'
|
|
36
|
-
import { useFileAttributes } from './hooks'
|
|
44
|
+
import { useFileAttributes, useRequiredFormValidation } from './hooks'
|
|
37
45
|
import { ItemRenderers, QueueDisplay } from './QueueDisplay'
|
|
38
46
|
import {
|
|
39
47
|
TFileId,
|
|
40
48
|
FileItem,
|
|
41
49
|
TFileItemList,
|
|
42
50
|
TFileTransfer,
|
|
51
|
+
TFileValidateDetail,
|
|
43
52
|
TItemRenderer,
|
|
44
53
|
PktFileUploadContext,
|
|
45
|
-
|
|
54
|
+
TQueueItemOperation,
|
|
46
55
|
TUploadStrategy,
|
|
47
56
|
} from './types'
|
|
48
|
-
import { formatFileSize, parseFileSize } from './utils'
|
|
49
57
|
|
|
50
58
|
// Re-export for external use
|
|
51
|
-
export { formatFileSize, parseFileSize } from '
|
|
59
|
+
export { formatFileSize, parseFileSize } from 'shared-utils/fileupload'
|
|
52
60
|
|
|
53
61
|
/**
|
|
54
62
|
* Shared props for `PktFileUpload` (both `form` and `custom` strategies).
|
|
55
|
-
*
|
|
56
|
-
* The component can be used as:
|
|
57
|
-
* - **controlled**: pass `value` + `onFilesChanged`
|
|
58
|
-
* - **uncontrolled**: pass `defaultValue` (internal state is not stored; you still receive new lists via callbacks)
|
|
59
63
|
*/
|
|
60
64
|
interface IBaseFileUploadProps
|
|
61
65
|
extends Omit<
|
|
@@ -88,9 +92,9 @@ interface IBaseFileUploadProps
|
|
|
88
92
|
/** Uncontrolled initial value (use either `value` or `defaultValue`, not both). */
|
|
89
93
|
defaultValue?: TFileItemList
|
|
90
94
|
/** Extra operations appended after built-ins (rename/comment). */
|
|
91
|
-
extraOperations?: Array<
|
|
95
|
+
extraOperations?: Array<TQueueItemOperation>
|
|
92
96
|
/** Stretch to full container width (drop zone + queue). */
|
|
93
|
-
|
|
97
|
+
fullwidth?: boolean
|
|
94
98
|
/** Allowed formats for built-in validation (extensions like `pdf`, or MIME patterns like `image/*`). */
|
|
95
99
|
allowedFormats?: string[]
|
|
96
100
|
/** Custom message for invalid format. Use `{formats}` placeholder. */
|
|
@@ -101,6 +105,11 @@ interface IBaseFileUploadProps
|
|
|
101
105
|
sizeErrorMessage?: string
|
|
102
106
|
/** Optional additional validation hook (runs after built-in format/size checks). Return string to block. */
|
|
103
107
|
onFileValidation?: (file: File) => string | null
|
|
108
|
+
/**
|
|
109
|
+
* Low-level validation escape hatch — Lit parity with the `file-validate` CustomEvent.
|
|
110
|
+
* Receives a detail object whose `errorMessage` can be mutated; set it to reject the file.
|
|
111
|
+
*/
|
|
112
|
+
onFileValidate?: (detail: TFileValidateDetail) => void
|
|
104
113
|
/** External/programmatic error message shown under the component. */
|
|
105
114
|
errorMessage?: string
|
|
106
115
|
/** External error flag (combined with internal validation errors). */
|
|
@@ -121,21 +130,13 @@ interface IBaseFileUploadProps
|
|
|
121
130
|
enableImagePreview?: boolean
|
|
122
131
|
}
|
|
123
132
|
|
|
124
|
-
/** Props for `uploadStrategy="form"` (native file input submission). */
|
|
125
133
|
interface IFileInputFileUploadProps
|
|
126
134
|
extends IBaseFileUploadProps,
|
|
127
|
-
|
|
135
|
+
Omit<HTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'defaultValue'> {
|
|
128
136
|
uploadStrategy?: 'form'
|
|
129
137
|
onFileUploadRequested?: never
|
|
130
138
|
}
|
|
131
139
|
|
|
132
|
-
/**
|
|
133
|
-
* Props for `uploadStrategy="custom"` (upload handled externally).
|
|
134
|
-
*
|
|
135
|
-
* Requirements:
|
|
136
|
-
* - `transfers` must be provided (to render status/progress)
|
|
137
|
-
* - `onFileUploadRequested` is called for each added file
|
|
138
|
-
*/
|
|
139
140
|
interface ImmediateFileUploadProps extends IBaseFileUploadProps {
|
|
140
141
|
id: string
|
|
141
142
|
uploadStrategy: 'custom'
|
|
@@ -156,17 +157,18 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
|
|
|
156
157
|
uploadStrategy = 'form',
|
|
157
158
|
addCommentsEnabled = false,
|
|
158
159
|
renameFilesEnabled = false,
|
|
159
|
-
truncateTail,
|
|
160
|
+
truncateTail = 4,
|
|
160
161
|
onFilesChanged,
|
|
161
162
|
onFileUploadRequested,
|
|
162
163
|
extraOperations = [],
|
|
163
164
|
itemRenderer: itemRendererProp = ItemRenderers.filename,
|
|
164
|
-
|
|
165
|
+
fullwidth = false,
|
|
165
166
|
allowedFormats,
|
|
166
167
|
formatErrorMessage,
|
|
167
|
-
maxFileSize,
|
|
168
|
+
maxFileSize,
|
|
168
169
|
sizeErrorMessage,
|
|
169
170
|
onFileValidation,
|
|
171
|
+
onFileValidate,
|
|
170
172
|
errorMessage: externalErrorMessage,
|
|
171
173
|
hasError: externalHasError = false,
|
|
172
174
|
disabled = false,
|
|
@@ -189,64 +191,41 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
|
|
|
189
191
|
|
|
190
192
|
const transfers = 'transfers' in props ? props.transfers : undefined
|
|
191
193
|
const [validationError, setValidationError] = useState<string | null>(null)
|
|
194
|
+
const [addedAnnouncement, setAddedAnnouncement] = useState('')
|
|
195
|
+
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (!addedAnnouncement) return
|
|
198
|
+
const t = setTimeout(() => setAddedAnnouncement(''), 1500)
|
|
199
|
+
return () => clearTimeout(t)
|
|
200
|
+
}, [addedAnnouncement])
|
|
192
201
|
|
|
193
|
-
|
|
194
|
-
const hasError = externalHasError || !!validationError
|
|
195
|
-
const errorMessage = externalErrorMessage || validationError
|
|
202
|
+
const errorMessage = validationError ?? externalErrorMessage ?? null
|
|
203
|
+
const hasError = externalHasError || !!validationError || !!(externalErrorMessage && externalErrorMessage.trim())
|
|
196
204
|
const errorMessageId = errorMessage ? `${id}-error` : undefined
|
|
197
205
|
const helptextId = helptext ? `${id}-helptext` : undefined
|
|
198
206
|
const describedBy = [ariaDescribedByProp, helptextId, errorMessageId].filter(Boolean).join(' ') || undefined
|
|
199
207
|
const isAriaInvalid = hasError || ariaInvalidProp === true || ariaInvalidProp === 'true'
|
|
200
208
|
|
|
201
|
-
// Parse maxFileSize once (supports "5MB" strings or raw bytes)
|
|
202
|
-
const maxFileSizeBytes = maxFileSize ? parseFileSize(maxFileSize) : undefined
|
|
203
|
-
|
|
204
|
-
// Built-in validation function
|
|
205
209
|
const validateFile = useCallback(
|
|
206
210
|
(file: File): string | null => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// Check MIME type patterns (e.g. 'image/*', 'application/pdf')
|
|
216
|
-
if (normalizedFormat.includes('/')) {
|
|
217
|
-
if (normalizedFormat.endsWith('/*')) {
|
|
218
|
-
const category = normalizedFormat.replace('/*', '')
|
|
219
|
-
return fileMimeType.startsWith(category + '/')
|
|
220
|
-
}
|
|
221
|
-
return fileMimeType === normalizedFormat
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Check file extension (e.g. 'pdf', 'jpg', '.png')
|
|
225
|
-
return fileExtension === normalizedFormat
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
if (!isAllowed) {
|
|
229
|
-
const formatsDisplay = allowedFormats.join(', ')
|
|
230
|
-
const defaultMessage = `Ugyldig filtype. Tillatte formater: ${formatsDisplay}`
|
|
231
|
-
return formatErrorMessage?.replace('{formats}', formatsDisplay) || defaultMessage
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// 2. Check file size if maxFileSize is specified
|
|
236
|
-
if (maxFileSizeBytes && file.size > maxFileSizeBytes) {
|
|
237
|
-
const maxSizeDisplay = formatFileSize(maxFileSizeBytes)
|
|
238
|
-
const defaultMessage = `Filen er for stor. Maks størrelse er ${maxSizeDisplay}.`
|
|
239
|
-
return sizeErrorMessage?.replace('{maxSize}', maxSizeDisplay) || defaultMessage
|
|
240
|
-
}
|
|
211
|
+
const baseError = sharedValidateFile(file, {
|
|
212
|
+
allowedFormats,
|
|
213
|
+
maxFileSize,
|
|
214
|
+
formatErrorMessage,
|
|
215
|
+
sizeErrorMessage,
|
|
216
|
+
onFileValidation,
|
|
217
|
+
})
|
|
218
|
+
if (baseError) return baseError
|
|
241
219
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
220
|
+
if (onFileValidate) {
|
|
221
|
+
const detail: TFileValidateDetail = { file, errorMessage: null }
|
|
222
|
+
onFileValidate(detail)
|
|
223
|
+
if (detail.errorMessage) return detail.errorMessage
|
|
245
224
|
}
|
|
246
225
|
|
|
247
226
|
return null
|
|
248
227
|
},
|
|
249
|
-
[allowedFormats, formatErrorMessage,
|
|
228
|
+
[allowedFormats, formatErrorMessage, maxFileSize, sizeErrorMessage, onFileValidation, onFileValidate],
|
|
250
229
|
)
|
|
251
230
|
|
|
252
231
|
if (valueProp !== undefined && defaultValue !== undefined) {
|
|
@@ -263,168 +242,120 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
|
|
|
263
242
|
}
|
|
264
243
|
|
|
265
244
|
const isControlled = valueProp !== undefined
|
|
266
|
-
const
|
|
245
|
+
const [internalValue, setInternalValue] = useState<TFileItemList>(() => defaultValue ?? [])
|
|
246
|
+
const value = isControlled ? (valueProp as TFileItemList) : internalValue
|
|
267
247
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const handleFormSubmit = (event: SubmitEvent) => {
|
|
275
|
-
if (value.length > 0) return
|
|
276
|
-
event.preventDefault()
|
|
277
|
-
setValidationError('Velg minst én fil før du sender inn skjemaet.')
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
form.addEventListener('submit', handleFormSubmit)
|
|
281
|
-
return () => {
|
|
282
|
-
form.removeEventListener('submit', handleFormSubmit)
|
|
283
|
-
}
|
|
284
|
-
}, [required, uploadStrategy, value.length])
|
|
248
|
+
useRequiredFormValidation(fileInputRef, {
|
|
249
|
+
uploadStrategy,
|
|
250
|
+
required,
|
|
251
|
+
currentFileCount: value.length,
|
|
252
|
+
onMissingFiles: useCallback(() => setValidationError(defaultFileUploadStrings.requiredMissing), []),
|
|
253
|
+
})
|
|
285
254
|
|
|
286
255
|
const itemRenderer = typeof itemRendererProp === 'string' ? ItemRenderers[itemRendererProp] : itemRendererProp
|
|
287
256
|
const isThumbnailView = itemRendererProp === 'thumbnail'
|
|
288
257
|
const explicitAccept = typeof props.accept === 'string' ? props.accept.trim() : undefined
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
return
|
|
292
|
-
|
|
293
|
-
.filter(Boolean)
|
|
294
|
-
.map((format) => {
|
|
295
|
-
const normalized = format.toLowerCase()
|
|
296
|
-
if (normalized.includes('/')) return normalized
|
|
297
|
-
if (normalized.startsWith('.')) return normalized
|
|
298
|
-
return `.${normalized}`
|
|
299
|
-
})
|
|
300
|
-
.join(', ')
|
|
301
|
-
}, [allowedFormats])
|
|
302
|
-
const resolvedAcceptForDropZone = explicitAccept || acceptFromAllowedFormats
|
|
258
|
+
const resolvedAcceptForDropZone = useMemo(() => {
|
|
259
|
+
const resolved = resolveAcceptAttribute(explicitAccept, allowedFormats)
|
|
260
|
+
return resolved || undefined
|
|
261
|
+
}, [explicitAccept, allowedFormats])
|
|
303
262
|
|
|
304
263
|
const effectiveRenameEnabled = renameFilesEnabled && !isThumbnailView
|
|
305
264
|
const effectiveCommentsEnabled = addCommentsEnabled && !isThumbnailView
|
|
306
265
|
|
|
307
266
|
const onFilesAdded = useCallback(
|
|
308
267
|
(addedFileItems: TFileItemList) => {
|
|
309
|
-
// Run validation on each file
|
|
310
268
|
for (const fileItem of addedFileItems) {
|
|
311
269
|
const error = validateFile(fileItem.file)
|
|
312
270
|
if (error) {
|
|
313
271
|
setValidationError(error)
|
|
314
|
-
return
|
|
272
|
+
return
|
|
315
273
|
}
|
|
316
274
|
}
|
|
317
275
|
|
|
318
|
-
// Clear any previous validation error
|
|
319
276
|
setValidationError(null)
|
|
320
277
|
|
|
321
278
|
const newValue: TFileItemList = multiple ? [...value, ...addedFileItems] : [addedFileItems[0]]
|
|
322
279
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
280
|
+
const acceptedItems: TFileItemList = multiple ? addedFileItems : [addedFileItems[0]]
|
|
281
|
+
setAddedAnnouncement(
|
|
282
|
+
acceptedItems.length === 1
|
|
283
|
+
? defaultFileUploadStrings.srFileAdded(acceptedItems[0].attributes.targetFilename)
|
|
284
|
+
: defaultFileUploadStrings.srFilesAdded(acceptedItems.length),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if (!isControlled) setInternalValue(newValue)
|
|
288
|
+
onFilesChanged?.(newValue)
|
|
289
|
+
|
|
326
290
|
if (uploadStrategy === 'custom' && onFileUploadRequested) {
|
|
327
291
|
addedFileItems.forEach((fileItem) => {
|
|
328
292
|
onFileUploadRequested?.(fileItem)
|
|
329
293
|
})
|
|
330
294
|
}
|
|
331
295
|
},
|
|
332
|
-
[onFilesChanged, value, multiple, uploadStrategy, onFileUploadRequested, validateFile],
|
|
296
|
+
[onFilesChanged, value, multiple, uploadStrategy, onFileUploadRequested, validateFile, isControlled],
|
|
333
297
|
)
|
|
334
298
|
|
|
335
299
|
const onFileUpdated = useCallback(
|
|
336
300
|
(fileId: TFileId, updates: Partial<FileItem>) => {
|
|
337
301
|
const newValue = value.map((fileItem) => (fileItem.fileId === fileId ? { ...fileItem, ...updates } : fileItem))
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
}
|
|
302
|
+
if (!isControlled) setInternalValue(newValue)
|
|
303
|
+
onFilesChanged?.(newValue)
|
|
341
304
|
},
|
|
342
|
-
[onFilesChanged, value],
|
|
305
|
+
[onFilesChanged, value, isControlled],
|
|
343
306
|
)
|
|
344
307
|
|
|
345
308
|
const onFileRemoved = useCallback(
|
|
346
309
|
(fileId: TFileId) => {
|
|
347
310
|
const newValue = value.filter((fileItem) => fileItem.fileId !== fileId)
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
}
|
|
311
|
+
if (!isControlled) setInternalValue(newValue)
|
|
312
|
+
onFilesChanged?.(newValue)
|
|
351
313
|
if ('onTransferCancelled' in props && props.onTransferCancelled) {
|
|
352
314
|
props.onTransferCancelled?.(fileId)
|
|
353
315
|
}
|
|
354
316
|
},
|
|
355
|
-
[onFilesChanged, value, props],
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
const { fileAttributes } = useFileAttributes(value, onFileUpdated)
|
|
359
|
-
|
|
360
|
-
const queueItemExtensions = useMemo(
|
|
361
|
-
() =>
|
|
362
|
-
[
|
|
363
|
-
{
|
|
364
|
-
op: renameFileOperation,
|
|
365
|
-
enabled: effectiveRenameEnabled,
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
op: (attributes: Parameters<typeof addCommentOperation>[0]) => addCommentOperation(attributes),
|
|
369
|
-
enabled: effectiveCommentsEnabled,
|
|
370
|
-
},
|
|
371
|
-
]
|
|
372
|
-
.filter(({ enabled }) => enabled)
|
|
373
|
-
.map(({ op }) => op)
|
|
374
|
-
.concat(...extraOperations),
|
|
375
|
-
[effectiveRenameEnabled, effectiveCommentsEnabled],
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
// Screen reader announcement IDs
|
|
379
|
-
const srAnnouncementIds = useMemo(
|
|
380
|
-
() => ({
|
|
381
|
-
uploaded: `${id}-uploaded`,
|
|
382
|
-
errors: `${id}-errors`,
|
|
383
|
-
}),
|
|
384
|
-
[id],
|
|
317
|
+
[onFilesChanged, value, props, isControlled],
|
|
385
318
|
)
|
|
386
319
|
|
|
387
|
-
|
|
388
|
-
// F.eks. "1 av 3 filer lastet opp" eller "2 av 3 filer feilet"
|
|
389
|
-
const { totalCount, uploadedCount, failedCount } = useMemo(() => {
|
|
390
|
-
const totalCount = value.length
|
|
391
|
-
if (!transfers) {
|
|
392
|
-
return { totalCount, uploadedCount: 0, failedCount: 0 }
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
let uploadedCount = 0
|
|
396
|
-
let failedCount = 0
|
|
320
|
+
const { buildAttributeAccessors } = useFileAttributes(value, onFileUpdated)
|
|
397
321
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
322
|
+
const queueItemOperations = useMemo<TQueueItemOperation[]>(() => {
|
|
323
|
+
const operations: TQueueItemOperation[] = []
|
|
324
|
+
if (effectiveRenameEnabled) operations.push(renameFileOperation)
|
|
325
|
+
if (effectiveCommentsEnabled) operations.push(addCommentOperation)
|
|
326
|
+
operations.push(...extraOperations)
|
|
327
|
+
operations.push(removeFileOperation(onFileRemoved))
|
|
328
|
+
return operations
|
|
329
|
+
}, [effectiveRenameEnabled, effectiveCommentsEnabled, extraOperations, onFileRemoved])
|
|
403
330
|
|
|
404
|
-
|
|
405
|
-
|
|
331
|
+
const { totalCount, uploadedCount, failedCount } = useMemo(
|
|
332
|
+
() => countFileTransferStates(value, transfers),
|
|
333
|
+
[value, transfers],
|
|
334
|
+
)
|
|
406
335
|
|
|
407
|
-
const totalLabel = totalCount
|
|
336
|
+
const totalLabel = defaultFileUploadStrings.fileLabel(totalCount)
|
|
408
337
|
|
|
409
338
|
const fileUploadContent = (
|
|
410
339
|
<div
|
|
411
340
|
className={classNames('pkt-fileupload', {
|
|
412
|
-
'pkt-fileupload--full-width':
|
|
341
|
+
'pkt-fileupload--full-width': fullwidth,
|
|
413
342
|
'pkt-fileupload--error': hasError,
|
|
414
343
|
'pkt-fileupload--disabled': disabled,
|
|
415
344
|
})}
|
|
416
345
|
aria-disabled={disabled}
|
|
417
|
-
{...(disabled ? { inert: ''
|
|
346
|
+
{...(disabled ? { inert: '' } : {})}
|
|
418
347
|
>
|
|
419
348
|
<PktFileUploadContext.Provider value={{ name: props.name || '', multiple: !!multiple, id }}>
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
349
|
+
<div className="pkt-sr-only" aria-live="polite" aria-atomic="true">
|
|
350
|
+
{addedAnnouncement ||
|
|
351
|
+
(totalCount > 0 && uploadedCount > 0
|
|
352
|
+
? defaultFileUploadStrings.srFilesUploadedOfTotal(uploadedCount, totalCount, totalLabel)
|
|
353
|
+
: '')}
|
|
423
354
|
</div>
|
|
424
|
-
<div
|
|
355
|
+
<div className="pkt-sr-only" aria-live="assertive" aria-atomic="true">
|
|
425
356
|
{totalCount > 0 &&
|
|
426
357
|
failedCount > 0 &&
|
|
427
|
-
|
|
358
|
+
defaultFileUploadStrings.srFilesFailedOfTotal(failedCount, totalCount, totalLabel)}
|
|
428
359
|
</div>
|
|
429
360
|
|
|
430
361
|
<DropZone
|
|
@@ -435,10 +366,9 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
|
|
|
435
366
|
multiple={multiple}
|
|
436
367
|
uploadStrategy={uploadStrategy}
|
|
437
368
|
ref={fileInputRef}
|
|
438
|
-
accept={isThumbnailView ?
|
|
369
|
+
accept={isThumbnailView ? THUMBNAIL_ACCEPT : resolvedAcceptForDropZone}
|
|
439
370
|
isThumbnailView={isThumbnailView}
|
|
440
371
|
disabled={disabled}
|
|
441
|
-
srAnnouncementIds={srAnnouncementIds}
|
|
442
372
|
required={required && uploadStrategy === 'form'}
|
|
443
373
|
aria-required={required ? 'true' : undefined}
|
|
444
374
|
aria-invalid={isAriaInvalid ? 'true' : undefined}
|
|
@@ -457,23 +387,25 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
|
|
|
457
387
|
{errorMessage}
|
|
458
388
|
</PktAlert>
|
|
459
389
|
)}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
.
|
|
471
|
-
|
|
390
|
+
{value.length > 0 && (
|
|
391
|
+
<QueueDisplay
|
|
392
|
+
files={value}
|
|
393
|
+
cancelTransfer={onFileRemoved}
|
|
394
|
+
truncateTail={truncateTail}
|
|
395
|
+
transfers={transfers}
|
|
396
|
+
uploadStrategy={uploadStrategy}
|
|
397
|
+
ItemRenderer={itemRenderer}
|
|
398
|
+
enableImagePreview={isThumbnailView && enableImagePreview}
|
|
399
|
+
disabled={disabled}
|
|
400
|
+
inputName={props.name || ''}
|
|
401
|
+
buildAttributeAccessors={buildAttributeAccessors}
|
|
402
|
+
queueItemOperations={queueItemOperations}
|
|
403
|
+
/>
|
|
404
|
+
)}
|
|
472
405
|
</PktFileUploadContext.Provider>
|
|
473
406
|
</div>
|
|
474
407
|
)
|
|
475
408
|
|
|
476
|
-
// Wrap with InputWrapper if label is provided
|
|
477
409
|
if (label) {
|
|
478
410
|
return (
|
|
479
411
|
<PktInputWrapper
|
|
@@ -484,7 +416,7 @@ export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IP
|
|
|
484
416
|
hasError={hasError}
|
|
485
417
|
optionalTag={optionalTag}
|
|
486
418
|
requiredTag={requiredTag}
|
|
487
|
-
className={classNames('pkt-fileupload-wrapper', { 'pkt-fileupload-wrapper--full-width':
|
|
419
|
+
className={classNames('pkt-fileupload-wrapper', { 'pkt-fileupload-wrapper--full-width': fullwidth })}
|
|
488
420
|
>
|
|
489
421
|
{fileUploadContent}
|
|
490
422
|
</PktInputWrapper>
|
|
@@ -24,14 +24,16 @@ const createMockFile = (name: string, type = 'text/plain'): File => new File(['c
|
|
|
24
24
|
const createFileItem = (name: string, fileId?: string) => new FileItem(createMockFile(name), fileId)
|
|
25
25
|
|
|
26
26
|
const expectVisibleFilename = (filename: string) => {
|
|
27
|
-
// Truncate
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
)
|
|
34
|
-
|
|
27
|
+
// Truncate may render the filename as a single span (data-pkt-truncate-part="first")
|
|
28
|
+
// or as a head + tail pair when the name is long enough to split. Assemble the parts
|
|
29
|
+
// from each title element and assert the filename is found in at least one.
|
|
30
|
+
const titleElements = document.querySelectorAll('.pkt-fileupload__queue-display__item__title')
|
|
31
|
+
const reconstructed = Array.from(titleElements).map((title) => {
|
|
32
|
+
const head = title.querySelector('[data-pkt-truncate-part="first"]')?.textContent ?? ''
|
|
33
|
+
const tail = title.querySelector('[data-pkt-truncate-part="tail"]')?.textContent ?? ''
|
|
34
|
+
return head + tail
|
|
35
|
+
})
|
|
36
|
+
expect(reconstructed).toContain(filename)
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
describe('QueueDisplay', () => {
|
|
@@ -219,4 +221,45 @@ describe('QueueDisplay', () => {
|
|
|
219
221
|
expect(container.querySelector('.pkt-fileupload__queue-display__item__progress')).not.toBeInTheDocument()
|
|
220
222
|
})
|
|
221
223
|
})
|
|
224
|
+
|
|
225
|
+
describe('Sort order', () => {
|
|
226
|
+
it('renders queue items in the order: in-progress → error → done/queued', () => {
|
|
227
|
+
const initialValue: TFileItemList = [
|
|
228
|
+
createFileItem('done.pdf', 'done-1'),
|
|
229
|
+
createFileItem('failed.pdf', 'error-1'),
|
|
230
|
+
createFileItem('uploading.pdf', 'progress-1'),
|
|
231
|
+
createFileItem('queued.pdf', 'queued-1'),
|
|
232
|
+
]
|
|
233
|
+
const transfers: TFileTransfer[] = [
|
|
234
|
+
{ fileId: 'done-1', progress: 'done' },
|
|
235
|
+
{ fileId: 'error-1', progress: 'error', errorMessage: 'failed' },
|
|
236
|
+
{ fileId: 'progress-1', progress: 0.5, showProgress: true },
|
|
237
|
+
{ fileId: 'queued-1', progress: 'queued' },
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
const { container } = render(
|
|
241
|
+
<PktFileUpload
|
|
242
|
+
id="sort-order-test"
|
|
243
|
+
value={initialValue}
|
|
244
|
+
name="pktFileUpload"
|
|
245
|
+
uploadStrategy="custom"
|
|
246
|
+
onFileUploadRequested={vi.fn()}
|
|
247
|
+
transfers={transfers}
|
|
248
|
+
onFilesChanged={NOOP}
|
|
249
|
+
/>,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
// Reconstruct each filename from head + tail spans (middle-truncation may split it).
|
|
253
|
+
const queueItems = Array.from(
|
|
254
|
+
container.querySelectorAll<HTMLElement>('.pkt-fileupload__queue-display__item'),
|
|
255
|
+
)
|
|
256
|
+
const visibleNames = queueItems.map((item) => {
|
|
257
|
+
const head = item.querySelector<HTMLElement>('[data-pkt-truncate-part="first"]')?.textContent ?? ''
|
|
258
|
+
const tail = item.querySelector<HTMLElement>('[data-pkt-truncate-part="tail"]')?.textContent ?? ''
|
|
259
|
+
return (head + tail).trim()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
expect(visibleNames).toEqual(['uploading.pdf', 'failed.pdf', 'done.pdf', 'queued.pdf'])
|
|
263
|
+
})
|
|
264
|
+
})
|
|
222
265
|
})
|