@oslokommune/punkt-react 15.0.4 → 15.2.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 +49 -0
- package/dist/index.d.ts +173 -0
- package/dist/punkt-react.es.js +8216 -7134
- package/dist/punkt-react.umd.js +435 -435
- package/package.json +4 -4
- package/src/components/fileupload/DropZone.tsx +236 -0
- package/src/components/fileupload/FileUpload.test.tsx +584 -0
- package/src/components/fileupload/FileUpload.tsx +425 -0
- package/src/components/fileupload/QueueDisplay.test.tsx +222 -0
- package/src/components/fileupload/QueueDisplay.tsx +155 -0
- package/src/components/fileupload/QueueItemContent.tsx +200 -0
- package/src/components/fileupload/Subcomponents.tsx +363 -0
- package/src/components/fileupload/Truncate.test.tsx +256 -0
- package/src/components/fileupload/Truncate.tsx +137 -0
- package/src/components/fileupload/extensions/Comments.tsx +188 -0
- package/src/components/fileupload/extensions/Remove.tsx +10 -0
- package/src/components/fileupload/extensions/Rename.tsx +79 -0
- package/src/components/fileupload/hooks/index.ts +3 -0
- package/src/components/fileupload/hooks/useFileAttributes.ts +46 -0
- package/src/components/fileupload/hooks/useImagePreview.ts +42 -0
- package/src/components/fileupload/hooks/useOperationState.ts +30 -0
- package/src/components/fileupload/hooks.ts +4 -0
- package/src/components/fileupload/texts.ts +20 -0
- package/src/components/fileupload/types.ts +94 -0
- package/src/components/fileupload/utils.ts +56 -0
- package/src/components/index.ts +1 -0
- package/src/components/interfaces.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oslokommune/punkt-react",
|
|
3
|
-
"version": "15.0
|
|
3
|
+
"version": "15.2.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.0
|
|
42
|
+
"@oslokommune/punkt-elements": "^15.2.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.0
|
|
53
|
+
"@oslokommune/punkt-css": "^15.2.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": "
|
|
112
|
+
"gitHead": "67c9a3c4ede793c0e6bf1e4611ae29d00cf072ee"
|
|
113
113
|
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import {
|
|
3
|
+
ChangeEvent,
|
|
4
|
+
DragEvent,
|
|
5
|
+
forwardRef,
|
|
6
|
+
InputHTMLAttributes,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useImperativeHandle,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from 'react'
|
|
14
|
+
|
|
15
|
+
import { PktIcon } from '..'
|
|
16
|
+
import { uiMultipleTexts, uiSingleTexts, uiThumbnailMultipleTexts, uiThumbnailSingleTexts } from './texts'
|
|
17
|
+
import { FileItem, TFileItemList, TUploadStrategy } from './types'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Props for the internal `DropZone` building block.
|
|
21
|
+
*
|
|
22
|
+
* This component is responsible for:
|
|
23
|
+
* - rendering the native `<input type="file">` (visually hidden, still accessible)
|
|
24
|
+
* - handling drag & drop + file dialog selection
|
|
25
|
+
* - optionally rendering hidden inputs with file IDs when `uploadStrategy="custom"`
|
|
26
|
+
*/
|
|
27
|
+
interface IDropZoneProps
|
|
28
|
+
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'checked' | 'size' | 'type' | 'value' | 'onChange' | 'width'> {
|
|
29
|
+
/** Called with `FileItem`s created from dropped/selected files. */
|
|
30
|
+
onFilesAdded?: (files: TFileItemList) => void
|
|
31
|
+
/** Used for the native input `id` and related label/ARIA wiring. */
|
|
32
|
+
id: string
|
|
33
|
+
/** Allow selecting/dropping multiple files. */
|
|
34
|
+
multiple?: boolean
|
|
35
|
+
/** Current file list (used for custom hidden inputs + re-populating the native input). */
|
|
36
|
+
value: TFileItemList
|
|
37
|
+
/**
|
|
38
|
+
* Field name.
|
|
39
|
+
* - `uploadStrategy="form"`: used as `<input name="...">` so the files are posted on submit.
|
|
40
|
+
* - `uploadStrategy="custom"`: used for hidden `<input type="hidden" name="...">` file IDs.
|
|
41
|
+
*/
|
|
42
|
+
name: string
|
|
43
|
+
/** Upload mode. */
|
|
44
|
+
uploadStrategy: TUploadStrategy
|
|
45
|
+
/** Enables alternate texts/UX for thumbnail view. */
|
|
46
|
+
isThumbnailView?: boolean
|
|
47
|
+
/** Disables all interaction. */
|
|
48
|
+
disabled?: boolean
|
|
49
|
+
/** IDs for screen reader announcements (uploaded/errors). */
|
|
50
|
+
srAnnouncementIds?: {
|
|
51
|
+
uploaded: string
|
|
52
|
+
errors: string
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export const DropZone = forwardRef<HTMLInputElement, IDropZoneProps>(
|
|
56
|
+
(
|
|
57
|
+
{
|
|
58
|
+
id,
|
|
59
|
+
multiple,
|
|
60
|
+
value,
|
|
61
|
+
onFilesAdded = () => {},
|
|
62
|
+
name,
|
|
63
|
+
uploadStrategy,
|
|
64
|
+
accept = '.pdf, .jpeg, .jpg, .png, .heic, .doc, .docx, .odt',
|
|
65
|
+
isThumbnailView = false,
|
|
66
|
+
disabled = false,
|
|
67
|
+
srAnnouncementIds,
|
|
68
|
+
...inputProps
|
|
69
|
+
}: IDropZoneProps,
|
|
70
|
+
forwardedRef,
|
|
71
|
+
) => {
|
|
72
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
73
|
+
|
|
74
|
+
const acceptedFormatsReadableString = useMemo(
|
|
75
|
+
() => (accept?.split(/\s*,\s*/).map((format) => format.trim()) || []).join(', ').toUpperCase(),
|
|
76
|
+
[accept],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
useImperativeHandle(forwardedRef, () => fileInputRef.current! as HTMLInputElement)
|
|
80
|
+
|
|
81
|
+
const [isDragActive, setIsDragActive] = useState(false)
|
|
82
|
+
|
|
83
|
+
const uiTexts: typeof uiMultipleTexts = useMemo(() => {
|
|
84
|
+
if (isThumbnailView) {
|
|
85
|
+
return multiple ? uiThumbnailMultipleTexts : uiThumbnailSingleTexts
|
|
86
|
+
}
|
|
87
|
+
return multiple ? uiMultipleTexts : uiSingleTexts
|
|
88
|
+
}, [multiple, isThumbnailView])
|
|
89
|
+
|
|
90
|
+
const populateNativeFileInput = useCallback(
|
|
91
|
+
(fileItemList: TFileItemList) => {
|
|
92
|
+
if (!fileInputRef.current) return
|
|
93
|
+
try {
|
|
94
|
+
fileInputRef.current.files = createFileList(fileItemList)
|
|
95
|
+
} catch {
|
|
96
|
+
// Setting files property may fail in test environments like jsdom
|
|
97
|
+
// This is not critical for the component's functionality
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
[fileInputRef.current, value],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
populateNativeFileInput(value)
|
|
105
|
+
}, [fileInputRef, value])
|
|
106
|
+
const filesAdded = useCallback(
|
|
107
|
+
(files: Array<File>) => {
|
|
108
|
+
const addedFileItems = files.map((file) => new FileItem(file))
|
|
109
|
+
onFilesAdded(addedFileItems)
|
|
110
|
+
// Clear the input value to allow uploading the same file again if needed
|
|
111
|
+
if (fileInputRef.current) {
|
|
112
|
+
fileInputRef.current.value = ''
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
[onFilesAdded],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const filesSelectedInDialog = (event: ChangeEvent<HTMLInputElement>) => {
|
|
119
|
+
const selectedFiles = (event.target as HTMLInputElement).files!
|
|
120
|
+
const userCancelledFileSelectionDialog = selectedFiles.length === 0
|
|
121
|
+
if (userCancelledFileSelectionDialog) {
|
|
122
|
+
if (multiple) {
|
|
123
|
+
populateNativeFileInput(value)
|
|
124
|
+
} else {
|
|
125
|
+
// onFilesChanged([]) // TODO: Nullstill ved avbryt i enkel opplasting?
|
|
126
|
+
}
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
filesAdded(Array.from(selectedFiles))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const filesDroppedOnDropzone = (files: Array<File>) => {
|
|
133
|
+
filesAdded(files)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
|
|
137
|
+
event.preventDefault()
|
|
138
|
+
setIsDragActive(false)
|
|
139
|
+
if (disabled) return
|
|
140
|
+
const droppedFiles = event.dataTransfer.files
|
|
141
|
+
const arr: Array<File> = Array.from(droppedFiles)
|
|
142
|
+
filesDroppedOnDropzone(arr)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
|
146
|
+
event.preventDefault()
|
|
147
|
+
if (disabled) return
|
|
148
|
+
setIsDragActive(true)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const handleDragLeave = () => {
|
|
152
|
+
setIsDragActive(false)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const handleDropZoneClick = useCallback(
|
|
156
|
+
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
157
|
+
if (disabled) return
|
|
158
|
+
const target = event.target as HTMLElement
|
|
159
|
+
// ignore clicks on the explicit open button (it opens the dialog itself)
|
|
160
|
+
if (target.closest('.pkt-fileupload__drop-zone__placeholder__title__open-file-dialog')) return
|
|
161
|
+
fileInputRef.current?.click()
|
|
162
|
+
},
|
|
163
|
+
[disabled],
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
onDrop={handleDrop}
|
|
169
|
+
onDragOver={handleDragOver}
|
|
170
|
+
onDragLeave={handleDragLeave}
|
|
171
|
+
onClick={handleDropZoneClick}
|
|
172
|
+
className={classNames('pkt-fileupload__drop-zone', {
|
|
173
|
+
'pkt-fileupload__drop-zone--drag-active': isDragActive,
|
|
174
|
+
'pkt-fileupload__drop-zone--disabled': disabled,
|
|
175
|
+
})}
|
|
176
|
+
>
|
|
177
|
+
<input
|
|
178
|
+
{...inputProps}
|
|
179
|
+
id={id}
|
|
180
|
+
type={'file'}
|
|
181
|
+
ref={fileInputRef}
|
|
182
|
+
multiple={multiple}
|
|
183
|
+
onChange={filesSelectedInDialog}
|
|
184
|
+
accept={accept}
|
|
185
|
+
disabled={disabled}
|
|
186
|
+
name={(uploadStrategy === 'form' && name) || undefined} // Ikke sett name hvis uploadStrategy er 'custom' - ignorerer ved POST
|
|
187
|
+
aria-label={multiple ? 'Velg filer' : 'Velg fil'}
|
|
188
|
+
/>
|
|
189
|
+
{uploadStrategy === 'custom' && (
|
|
190
|
+
<>
|
|
191
|
+
{value?.map((fileItem) => (
|
|
192
|
+
<input key={fileItem.fileId} type="hidden" name={name} value={fileItem.fileId} />
|
|
193
|
+
))}
|
|
194
|
+
</>
|
|
195
|
+
)}
|
|
196
|
+
<div className="pkt-fileupload__drop-zone__placeholder">
|
|
197
|
+
<PktIcon name={'attachment'} className="pkt-fileupload__drop-zone__placeholder__icon" aria-hidden="true" />
|
|
198
|
+
<p className={'pkt-fileupload__drop-zone__placeholder__title'}>
|
|
199
|
+
{isDragActive ? (
|
|
200
|
+
`${uiTexts.dropFilesHere} ...`
|
|
201
|
+
) : (
|
|
202
|
+
<>
|
|
203
|
+
{uiTexts.selectOrDragFiles}{' '}
|
|
204
|
+
<button
|
|
205
|
+
className="pkt-fileupload__drop-zone__placeholder__title__open-file-dialog"
|
|
206
|
+
onClick={(e) => {
|
|
207
|
+
e.preventDefault()
|
|
208
|
+
e.stopPropagation()
|
|
209
|
+
fileInputRef.current?.click()
|
|
210
|
+
}}
|
|
211
|
+
type="button"
|
|
212
|
+
>
|
|
213
|
+
{uiTexts.chooseFiles}
|
|
214
|
+
</button>
|
|
215
|
+
</>
|
|
216
|
+
)}
|
|
217
|
+
</p>
|
|
218
|
+
<p className={'pkt-fileupload__drop-zone__placeholder__formats'}>Format: {acceptedFormatsReadableString}</p>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
)
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Create a `FileList` from a list of `FileItem`s.
|
|
227
|
+
*
|
|
228
|
+
* Note: relies on `DataTransfer`, which may be missing or restricted in test environments (jsdom).
|
|
229
|
+
*/
|
|
230
|
+
export const createFileList = (fileItemList: TFileItemList) => {
|
|
231
|
+
const dataTransfer = new DataTransfer()
|
|
232
|
+
for (const fileItem of fileItemList) {
|
|
233
|
+
dataTransfer.items.add(fileItem.file)
|
|
234
|
+
}
|
|
235
|
+
return dataTransfer.files
|
|
236
|
+
}
|