@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
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { createContext, ReactElement, ReactNode } from 'react'
|
|
2
2
|
|
|
3
|
+
import type {
|
|
4
|
+
IFileItem,
|
|
5
|
+
IFileTransfer,
|
|
6
|
+
IFileValidateDetail,
|
|
7
|
+
TFileUploadStrategy,
|
|
8
|
+
TTransferProgress,
|
|
9
|
+
} from 'shared-types'
|
|
10
|
+
|
|
3
11
|
import { uuidish } from '../../utils/stringutils'
|
|
4
12
|
|
|
5
13
|
/** Unique identifier assigned to each selected file. */
|
|
@@ -11,11 +19,13 @@ export type TFileId = string
|
|
|
11
19
|
* Notes:
|
|
12
20
|
* - A `fileId` is generated automatically if omitted.
|
|
13
21
|
* - `attributes.targetFilename` is the filename shown in the queue and may be updated by extensions (e.g. rename).
|
|
22
|
+
*
|
|
23
|
+
* Implements the shared `IFileItem` type from `shared-types`.
|
|
14
24
|
*/
|
|
15
|
-
export class FileItem implements
|
|
25
|
+
export class FileItem implements IFileItem {
|
|
16
26
|
fileId: TFileId
|
|
17
27
|
file: File
|
|
18
|
-
attributes: Record<string,
|
|
28
|
+
attributes: { targetFilename: string } & Record<string, unknown>
|
|
19
29
|
|
|
20
30
|
constructor(file: File, fileId?: TFileId) {
|
|
21
31
|
this.fileId = fileId || uuidish()
|
|
@@ -33,51 +43,58 @@ export const PktFileUploadContext = createContext<TPktFileUploadContext>({} as T
|
|
|
33
43
|
/** The value type for `PktFileUpload` (`value` / `defaultValue`). */
|
|
34
44
|
export type TFileItemList = Array<FileItem>
|
|
35
45
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
* - `progress`: `0..1` during upload, or a state (`queued`, `done`, `error`, `canceled`)
|
|
40
|
-
* - `showProgress`: if false, UI uses indeterminate "Laster opp..." style instead of progress bar
|
|
41
|
-
* - `lastProgress`: used to render a "failed at X%" bar when an upload errors
|
|
42
|
-
*/
|
|
43
|
-
export type TFileTransfer = {
|
|
44
|
-
fileId: TFileId
|
|
45
|
-
progress: number | 'done' | 'error' | 'canceled' | 'queued' // i tilfelle number: 0-1
|
|
46
|
-
errorMessage?: string
|
|
47
|
-
showProgress?: boolean // true = show progress bar/file size, false = show "Laster opp..."/just error message
|
|
48
|
-
lastProgress?: number // Store progress value when error occurs (0-1)
|
|
49
|
-
}
|
|
46
|
+
/** Transfer status for a file when using `uploadStrategy="custom"` — aliased from `shared-types`. */
|
|
47
|
+
export type TFileTransfer = IFileTransfer
|
|
50
48
|
|
|
51
49
|
/** Upload mode: `form` posts the files on submit, `custom` posts file IDs and uploads separately. */
|
|
52
|
-
export type TUploadStrategy =
|
|
50
|
+
export type TUploadStrategy = TFileUploadStrategy
|
|
51
|
+
|
|
52
|
+
/** Detail payload passed to the React `onFileValidate` escape hatch. */
|
|
53
|
+
export type TFileValidateDetail = IFileValidateDetail
|
|
54
|
+
|
|
55
|
+
/** Re-export for parity with Lit types. */
|
|
56
|
+
export type { TTransferProgress }
|
|
57
|
+
|
|
53
58
|
export type TFileAttribute<T> = {
|
|
54
59
|
get: (fileId: TFileId) => T | undefined
|
|
55
60
|
set: (fileId: TFileId, attributeValue: T | undefined) => void
|
|
56
61
|
}
|
|
57
62
|
export type TFileAttributes = <T>(attributeName: string) => TFileAttribute<T>
|
|
58
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Context passed to queue-item operations. Mirrors the Lit
|
|
66
|
+
* `TQueueOperationContext` so operations authored for one runtime are
|
|
67
|
+
* trivial to port to the other.
|
|
68
|
+
*/
|
|
69
|
+
export type TQueueOperationContext = {
|
|
70
|
+
file: FileItem
|
|
71
|
+
inputName: string
|
|
72
|
+
disabled: boolean
|
|
73
|
+
isActive: boolean
|
|
74
|
+
activate: () => void
|
|
75
|
+
close: () => void
|
|
76
|
+
getAttribute: <T>(name: string) => T | undefined
|
|
77
|
+
setAttribute: (name: string, value: unknown) => void
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type TQueueOperationLabel = string | ((fileItem: FileItem) => string)
|
|
81
|
+
|
|
59
82
|
/**
|
|
60
83
|
* An operation that can be attached to a queue item (rename, comment, remove, etc).
|
|
61
84
|
*
|
|
62
|
-
*
|
|
63
|
-
* - be a simple action (`onClick`)
|
|
64
|
-
* - render inline UI (e.g. rename)
|
|
65
|
-
* - render extended UI (e.g. comments)
|
|
66
|
-
* - render hidden inputs for form submission (`renderHidden`)
|
|
85
|
+
* Must carry a stable `id: string` — symbols are no longer supported.
|
|
67
86
|
*/
|
|
68
87
|
export type TQueueItemOperation = {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
id: string
|
|
89
|
+
title: TQueueOperationLabel
|
|
90
|
+
ariaLabel?: TQueueOperationLabel
|
|
91
|
+
onClick?: (context: TQueueOperationContext) => void
|
|
92
|
+
renderInlineUI?: (context: TQueueOperationContext) => ReactNode
|
|
93
|
+
renderExtendedUI?: (context: TQueueOperationContext) => ReactNode
|
|
94
|
+
renderContent?: (context: TQueueOperationContext) => ReactNode
|
|
95
|
+
renderHidden?: (context: TQueueOperationContext) => ReactNode
|
|
77
96
|
}
|
|
78
97
|
|
|
79
|
-
/** Factory that produces a queue item operation given access to file attributes. */
|
|
80
|
-
export type TQueueItemExtension = (attributes: TFileAttributes) => TQueueItemOperation
|
|
81
98
|
export type TFileAndTransfer = FileItem &
|
|
82
99
|
Pick<TFileTransfer, 'progress' | 'errorMessage' | 'showProgress' | 'lastProgress'>
|
|
83
100
|
export type TTransferItemInProgress = TFileAndTransfer & { progress: number }
|
|
@@ -1,56 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Re-export of the shared fileupload utilities.
|
|
3
|
+
* Kept as a file entry so existing `import { parseFileSize } from './utils'` paths keep working.
|
|
3
4
|
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Parse a file size string (e.g. "5MB", "500KB") to bytes.
|
|
7
|
-
* Also accepts raw numbers (returned as-is).
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* parseFileSize("5MB") // 5242880
|
|
11
|
-
* parseFileSize("500KB") // 512000
|
|
12
|
-
* parseFileSize("1GB") // 1073741824
|
|
13
|
-
* parseFileSize(1024) // 1024
|
|
14
|
-
*/
|
|
15
|
-
export const parseFileSize = (size: string | number): number => {
|
|
16
|
-
if (typeof size === 'number') return size
|
|
17
|
-
|
|
18
|
-
const match = size.trim().match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)$/i)
|
|
19
|
-
if (!match) {
|
|
20
|
-
console.warn(
|
|
21
|
-
`PktFileUpload: Invalid maxFileSize format "${size}". Use format like "5MB", "500KB", or a number in bytes.`,
|
|
22
|
-
)
|
|
23
|
-
return 0
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const value = parseFloat(match[1])
|
|
27
|
-
const unit = match[2].toUpperCase()
|
|
28
|
-
|
|
29
|
-
switch (unit) {
|
|
30
|
-
case 'B':
|
|
31
|
-
return value
|
|
32
|
-
case 'KB':
|
|
33
|
-
return value * 1024
|
|
34
|
-
case 'MB':
|
|
35
|
-
return value * 1024 * 1024
|
|
36
|
-
case 'GB':
|
|
37
|
-
return value * 1024 * 1024 * 1024
|
|
38
|
-
default:
|
|
39
|
-
return value
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Format bytes to a human-readable string (e.g. "5 MB", "500 KB").
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* formatFileSize(5242880) // "5 MB"
|
|
48
|
-
* formatFileSize(512000) // "500 KB"
|
|
49
|
-
* formatFileSize(500) // "500 B"
|
|
50
|
-
*/
|
|
51
|
-
export const formatFileSize = (bytes: number): string => {
|
|
52
|
-
if (bytes < 1024) return `${bytes} B`
|
|
53
|
-
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`
|
|
54
|
-
if (bytes < 1024 * 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))} MB`
|
|
55
|
-
return `${Math.round(bytes / (1024 * 1024 * 1024))} GB`
|
|
56
|
-
}
|
|
5
|
+
export { parseFileSize, formatFileSize } from 'shared-utils/fileupload'
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ForwardedRef, forwardRef, type ReactNode, RefAttributes, useState } from 'react'
|
|
4
4
|
|
|
5
|
+
import { useFocusModality } from '../../hooks/useFocusModality'
|
|
5
6
|
import { PktAlert } from '../alert/Alert'
|
|
6
7
|
import { PktButton } from '../button/Button'
|
|
7
8
|
|
|
@@ -64,6 +65,8 @@ export const PktInputWrapper = forwardRef(
|
|
|
64
65
|
) => {
|
|
65
66
|
const [isHelpTextOpen, setIsHelpTextOpen] = useState(false)
|
|
66
67
|
|
|
68
|
+
const focusModality = useFocusModality<HTMLDivElement>(ref)
|
|
69
|
+
|
|
67
70
|
const describedBy = ariaDescribedby || (helptext ? `${forId}-helptext` : undefined)
|
|
68
71
|
const showCounter = !!counter
|
|
69
72
|
const counterTop = showCounter && counterPosition === 'top'
|
|
@@ -189,7 +192,7 @@ export const PktInputWrapper = forwardRef(
|
|
|
189
192
|
)
|
|
190
193
|
|
|
191
194
|
return (
|
|
192
|
-
<div className={wrapperClasses}
|
|
195
|
+
<div className={wrapperClasses} role={role} {...focusModality}>
|
|
193
196
|
{hasFieldset ? (
|
|
194
197
|
<fieldset className="pkt-inputwrapper__fieldset" aria-describedby={describedBy}>
|
|
195
198
|
{content}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ForwardedRef,
|
|
6
6
|
forwardRef,
|
|
7
7
|
HTMLAttributes,
|
|
8
|
+
MutableRefObject,
|
|
8
9
|
ReactNode,
|
|
9
10
|
Ref,
|
|
10
11
|
useCallback,
|
|
@@ -51,18 +52,29 @@ export const PktModal = forwardRef(
|
|
|
51
52
|
}: IPktModal,
|
|
52
53
|
ref: ForwardedRef<HTMLDialogElement>,
|
|
53
54
|
) => {
|
|
54
|
-
const internalRef = useRef<HTMLDialogElement>(null)
|
|
55
|
-
|
|
55
|
+
const internalRef = useRef<HTMLDialogElement | null>(null)
|
|
56
|
+
|
|
57
|
+
// Merge consumer ref + internal ref so we always have a working DOM handle
|
|
58
|
+
// for our own logic, regardless of whether the consumer passes a callback ref
|
|
59
|
+
// or an object ref (or nothing).
|
|
60
|
+
const setDialogRef = useCallback(
|
|
61
|
+
(node: HTMLDialogElement | null) => {
|
|
62
|
+
internalRef.current = node
|
|
63
|
+
if (typeof ref === 'function') ref(node)
|
|
64
|
+
else if (ref) (ref as MutableRefObject<HTMLDialogElement | null>).current = node
|
|
65
|
+
},
|
|
66
|
+
[ref],
|
|
67
|
+
)
|
|
56
68
|
|
|
57
69
|
const close = useCallback(() => {
|
|
58
|
-
if (!
|
|
59
|
-
|
|
70
|
+
if (!internalRef.current?.open) return
|
|
71
|
+
internalRef.current.close()
|
|
60
72
|
onClose?.()
|
|
61
|
-
}, [
|
|
73
|
+
}, [onClose])
|
|
62
74
|
|
|
63
75
|
// Synkroniser scroll-lock med dialogens faktiske åpen/lukket-tilstand
|
|
64
76
|
useEffect(() => {
|
|
65
|
-
const dialog =
|
|
77
|
+
const dialog = internalRef.current
|
|
66
78
|
if (!dialog) return
|
|
67
79
|
|
|
68
80
|
const observer = new MutationObserver(() => {
|
|
@@ -74,28 +86,28 @@ export const PktModal = forwardRef(
|
|
|
74
86
|
})
|
|
75
87
|
observer.observe(dialog, { attributes: true, attributeFilter: ['open'] })
|
|
76
88
|
return () => observer.disconnect()
|
|
77
|
-
}, [
|
|
89
|
+
}, [])
|
|
78
90
|
|
|
79
91
|
// Deklarativ åpne/lukke via open-prop
|
|
80
92
|
useEffect(() => {
|
|
81
93
|
if (open) {
|
|
82
|
-
if (!
|
|
83
|
-
|
|
94
|
+
if (!internalRef.current?.open) {
|
|
95
|
+
internalRef.current?.showModal()
|
|
84
96
|
}
|
|
85
97
|
} else {
|
|
86
|
-
if (
|
|
87
|
-
|
|
98
|
+
if (internalRef.current?.open) {
|
|
99
|
+
internalRef.current.close()
|
|
88
100
|
}
|
|
89
101
|
}
|
|
90
|
-
}, [open
|
|
102
|
+
}, [open])
|
|
91
103
|
|
|
92
104
|
const handleBackdropClick = useCallback(
|
|
93
105
|
(event: React.MouseEvent<HTMLDialogElement>) => {
|
|
94
|
-
if (closeOnBackdropClick && event.target ===
|
|
106
|
+
if (closeOnBackdropClick && event.target === internalRef.current) {
|
|
95
107
|
close()
|
|
96
108
|
}
|
|
97
109
|
},
|
|
98
|
-
[closeOnBackdropClick,
|
|
110
|
+
[closeOnBackdropClick, close],
|
|
99
111
|
)
|
|
100
112
|
|
|
101
113
|
const handleNativeClose = useCallback(() => {
|
|
@@ -122,7 +134,7 @@ export const PktModal = forwardRef(
|
|
|
122
134
|
<dialog
|
|
123
135
|
{...props}
|
|
124
136
|
className={classes}
|
|
125
|
-
ref={
|
|
137
|
+
ref={setDialogRef}
|
|
126
138
|
aria-labelledby={headingText ? 'pkt-modal__headingText' : undefined}
|
|
127
139
|
aria-describedby="pkt-modal__content"
|
|
128
140
|
onClick={handleBackdropClick}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ChangeEvent, createElement, forwardRef, HTMLProps, ReactNode } from 'react'
|
|
4
4
|
|
|
5
|
+
import { useFocusModality } from '../../hooks/useFocusModality'
|
|
5
6
|
import { PktButton } from '../button/Button'
|
|
6
7
|
|
|
7
8
|
interface SearchSuggestion {
|
|
@@ -74,22 +75,22 @@ export const PktSearchInput = forwardRef<HTMLInputElement, ISearchInput | ISearc
|
|
|
74
75
|
fullwidth ? 'pkt-searchinput--fullwidth' : ''
|
|
75
76
|
}`
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
const focusModality = useFocusModality()
|
|
79
|
+
|
|
80
|
+
const wrapperProps = {
|
|
81
|
+
role: 'search',
|
|
82
|
+
className: wrapperClass,
|
|
83
|
+
...focusModality,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const WrapperElement = (children: ReactNode) =>
|
|
87
|
+
action ? (
|
|
88
|
+
<form {...wrapperProps} action={action} method={method}>
|
|
82
89
|
{children}
|
|
83
90
|
</form>
|
|
91
|
+
) : (
|
|
92
|
+
<div {...wrapperProps}>{children}</div>
|
|
84
93
|
)
|
|
85
|
-
} else {
|
|
86
|
-
// eslint-disable-next-line react/display-name
|
|
87
|
-
WrapperElement = (children: ReactNode) => (
|
|
88
|
-
<div role="search" className={wrapperClass}>
|
|
89
|
-
{children}
|
|
90
|
-
</div>
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
94
|
|
|
94
95
|
return WrapperElement(
|
|
95
96
|
<>
|
package/src/components/types.ts
CHANGED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export const uiMultipleTexts = {
|
|
2
|
-
dropFilesHere: 'Slipp filene her ...',
|
|
3
|
-
selectOrDragFiles: 'Dra filer hit for å laste dem opp eller ',
|
|
4
|
-
chooseFiles: 'velg filer',
|
|
5
|
-
}
|
|
6
|
-
export const uiSingleTexts: typeof uiMultipleTexts = {
|
|
7
|
-
dropFilesHere: 'Slipp filen her ...',
|
|
8
|
-
selectOrDragFiles: 'Dra en fil for å laste den opp eller ',
|
|
9
|
-
chooseFiles: 'velg en fil',
|
|
10
|
-
}
|
|
11
|
-
export const uiThumbnailMultipleTexts: typeof uiMultipleTexts = {
|
|
12
|
-
dropFilesHere: 'Slipp bildene her ...',
|
|
13
|
-
selectOrDragFiles: 'Dra bilder hit for å laste dem opp eller ',
|
|
14
|
-
chooseFiles: 'velg fra kamerarull',
|
|
15
|
-
}
|
|
16
|
-
export const uiThumbnailSingleTexts: typeof uiMultipleTexts = {
|
|
17
|
-
dropFilesHere: 'Slipp bildet her ...',
|
|
18
|
-
selectOrDragFiles: 'Dra et bilde for å laste det opp eller ',
|
|
19
|
-
chooseFiles: 'velg fra kamerarull',
|
|
20
|
-
}
|