@wallarm-org/design-system 1.27.0 → 1.27.1-rc-fix-remote-shell-breadcrumb-transition.2
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/dist/components/FileUpload/FileUpload.d.ts +46 -0
- package/dist/components/FileUpload/FileUpload.figma.d.ts +1 -0
- package/dist/components/FileUpload/FileUpload.figma.js +108 -0
- package/dist/components/FileUpload/FileUpload.js +224 -0
- package/dist/components/FileUpload/FileUpload.test.helpers.js +100 -0
- package/dist/components/FileUpload/FileUploadContext.d.ts +41 -0
- package/dist/components/FileUpload/FileUploadContext.js +10 -0
- package/dist/components/FileUpload/FileUploadDropzone.d.ts +15 -0
- package/dist/components/FileUpload/FileUploadDropzone.js +53 -0
- package/dist/components/FileUpload/FileUploadError.d.ts +15 -0
- package/dist/components/FileUpload/FileUploadError.js +38 -0
- package/dist/components/FileUpload/FileUploadItem.d.ts +23 -0
- package/dist/components/FileUpload/FileUploadItem.js +99 -0
- package/dist/components/FileUpload/FileUploadItemAction.d.ts +10 -0
- package/dist/components/FileUpload/FileUploadItemAction.js +40 -0
- package/dist/components/FileUpload/FileUploadItemContext.d.ts +7 -0
- package/dist/components/FileUpload/FileUploadItemContext.js +9 -0
- package/dist/components/FileUpload/FileUploadItemDeleteTrigger.d.ts +10 -0
- package/dist/components/FileUpload/FileUploadItemDeleteTrigger.js +74 -0
- package/dist/components/FileUpload/FileUploadItemGroup.d.ts +13 -0
- package/dist/components/FileUpload/FileUploadItemGroup.js +28 -0
- package/dist/components/FileUpload/FileUploadItemReplaceTrigger.d.ts +14 -0
- package/dist/components/FileUpload/FileUploadItemReplaceTrigger.js +89 -0
- package/dist/components/FileUpload/FileUploadTrigger.d.ts +8 -0
- package/dist/components/FileUpload/FileUploadTrigger.js +36 -0
- package/dist/components/FileUpload/classes.d.ts +24 -0
- package/dist/components/FileUpload/classes.js +11 -0
- package/dist/components/FileUpload/index.d.ts +11 -0
- package/dist/components/FileUpload/index.js +10 -0
- package/dist/components/FileUpload/lib/accept.d.ts +4 -0
- package/dist/components/FileUpload/lib/accept.js +12 -0
- package/dist/components/FileUpload/lib/checkFile.d.ts +13 -0
- package/dist/components/FileUpload/lib/checkFile.js +12 -0
- package/dist/components/FileUpload/lib/formatFileSize.d.ts +2 -0
- package/dist/components/FileUpload/lib/formatFileSize.js +22 -0
- package/dist/components/FileUpload/lib/formatRejection.d.ts +3 -0
- package/dist/components/FileUpload/lib/formatRejection.js +19 -0
- package/dist/components/FileUpload/lib/index.d.ts +5 -0
- package/dist/components/FileUpload/lib/index.js +5 -0
- package/dist/components/FileUpload/lib/setInputFiles.d.ts +7 -0
- package/dist/components/FileUpload/lib/setInputFiles.js +11 -0
- package/dist/components/FileUpload/types.d.ts +21 -0
- package/dist/components/FileUpload/types.js +0 -0
- package/dist/components/RemoteShell/NavPanelContent.d.ts +5 -0
- package/dist/components/RemoteShell/NavPanelContent.js +6 -4
- package/dist/components/RemoteShell/RemoteShell.d.ts +30 -1
- package/dist/components/RemoteShell/RemoteShell.js +35 -18
- package/dist/components/RemoteShell/RemoteShellPanel.js +10 -4
- package/dist/components/RemoteShell/model/index.d.ts +1 -1
- package/dist/components/RemoteShell/model/index.js +1 -1
- package/dist/components/RemoteShell/model/useDrillTransition.d.ts +12 -1
- package/dist/components/RemoteShell/model/useDrillTransition.js +24 -12
- package/dist/components/RemoteShell/model/useLocationPathname.d.ts +2 -0
- package/dist/components/RemoteShell/model/useLocationPathname.js +4 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/metadata/components.json +3215 -220
- package/package.json +3 -1
- package/dist/components/AppShell/story-content/_storyAccountDropdown.d.ts +0 -11
- package/dist/components/AppShell/story-content/_storyAccountDropdown.js +0 -242
- package/dist/components/AppShell/story-content/_storyConfigRenderer.d.ts +0 -12
- package/dist/components/AppShell/story-content/_storyConfigRenderer.js +0 -81
- package/dist/components/AppShell/story-content/_storyHeaderActions.d.ts +0 -1
- package/dist/components/AppShell/story-content/_storyHeaderActions.js +0 -59
- package/dist/components/AppShell/story-content/_storyHomeContent.d.ts +0 -2
- package/dist/components/AppShell/story-content/_storyHomeContent.js +0 -15
- package/dist/components/AppShell/story-content/_storyLib.d.ts +0 -9
- package/dist/components/AppShell/story-content/_storyLib.js +0 -39
- package/dist/components/AppShell/story-content/_storyNavConfigs.d.ts +0 -6
- package/dist/components/AppShell/story-content/_storyNavConfigs.js +0 -772
- package/dist/components/AppShell/story-content/_storyNavRailFooter.d.ts +0 -13
- package/dist/components/AppShell/story-content/_storyNavRailFooter.js +0 -26
- package/dist/components/AppShell/story-content/_storyProductNavItems.d.ts +0 -7
- package/dist/components/AppShell/story-content/_storyProductNavItems.js +0 -49
- package/dist/components/AppShell/story-content/_storyQuickHelpDropdown.d.ts +0 -1
- package/dist/components/AppShell/story-content/_storyQuickHelpDropdown.js +0 -144
- package/dist/components/AppShell/story-content/_storyRecentDropdown.d.ts +0 -1
- package/dist/components/AppShell/story-content/_storyRecentDropdown.js +0 -89
- package/dist/components/AppShell/story-content/_storySearchModal.d.ts +0 -1
- package/dist/components/AppShell/story-content/_storySearchModal.js +0 -252
- package/dist/components/AppShell/story-content/_storyWallarmLogo.d.ts +0 -1
- package/dist/components/AppShell/story-content/_storyWallarmLogo.js +0 -21
- package/dist/components/AppShell/story-content/index.d.ts +0 -11
- package/dist/components/AppShell/story-content/index.js +0 -11
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type FC, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import { type TestableProps } from '../../utils/testId';
|
|
3
|
+
import type { FileUploadRejection } from './types';
|
|
4
|
+
export interface FileUploadProps extends Omit<ComponentPropsWithoutRef<'div'>, 'defaultValue' | 'children'>, TestableProps {
|
|
5
|
+
/** Controlled accepted files. */
|
|
6
|
+
value?: File[];
|
|
7
|
+
/** Uncontrolled initial files. */
|
|
8
|
+
defaultValue?: File[];
|
|
9
|
+
/** Every change to the accepted list — pick, drop, replace, delete (then `[]`). */
|
|
10
|
+
onValueChange?: (files: File[]) => void;
|
|
11
|
+
/** Files that failed validation (for consumers that also want a toast). */
|
|
12
|
+
onFileReject?: (rejections: FileUploadRejection[]) => void;
|
|
13
|
+
/** Extensions and/or MIME types: `'.so,.dylib'`, `'image/png'`, `['.lua']`. */
|
|
14
|
+
accept?: string | string[];
|
|
15
|
+
/** Default `1`: a new pick replaces the file, and the picker stays visible, with the chosen file shown below it. */
|
|
16
|
+
maxFiles?: number;
|
|
17
|
+
/** Bytes. Checked on pick, drop and replace. */
|
|
18
|
+
maxFileSize?: number;
|
|
19
|
+
/** Bytes. */
|
|
20
|
+
minFileSize?: number;
|
|
21
|
+
/** Custom rules; returned strings are shown verbatim as rejection messages. */
|
|
22
|
+
validate?: (file: File) => string[] | null;
|
|
23
|
+
/** Hidden input name — the files submit with a native form under it. */
|
|
24
|
+
name?: string;
|
|
25
|
+
required?: boolean;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/** Hides the picker, Delete and Replace; `FileUploadItemAction` (e.g. Download) stays. */
|
|
28
|
+
readOnly?: boolean;
|
|
29
|
+
/** Error state (maps to Ark `invalid`); also read from a wrapping `Field`. */
|
|
30
|
+
error?: boolean;
|
|
31
|
+
/** Default `true`. */
|
|
32
|
+
allowDrop?: boolean;
|
|
33
|
+
children?: ReactNode;
|
|
34
|
+
ref?: Ref<HTMLDivElement>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* FileUpload — attach files to a form: pick with a drop Area (`FileUploadDropzone`) or a
|
|
38
|
+
* button (`FileUploadTrigger`), check type and size before anything is sent, and list the
|
|
39
|
+
* chosen files as `FileUploadItem` rows. Built on `@ark-ui/react/file-upload`.
|
|
40
|
+
*
|
|
41
|
+
* Picking never uploads: files are held in `value` and in the hidden input (`name`) and go
|
|
42
|
+
* with the form on save. The consumer owns the network — mark a row `loading` while it
|
|
43
|
+
* uploads (that locks the picker) and compose `FileUploadItemAction` for Download.
|
|
44
|
+
* Reads `Field` context (label, required, disabled, read-only, invalid).
|
|
45
|
+
*/
|
|
46
|
+
export declare const FileUpload: FC<FileUploadProps>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import code_connect from "@figma/code-connect";
|
|
3
|
+
import { Download } from "../../icons/index.js";
|
|
4
|
+
import { Field, FieldLabel } from "../Field/index.js";
|
|
5
|
+
import { FileUpload } from "./FileUpload.js";
|
|
6
|
+
import { FileUploadDropzone } from "./FileUploadDropzone.js";
|
|
7
|
+
import { FileUploadItem } from "./FileUploadItem.js";
|
|
8
|
+
import { FileUploadItemAction } from "./FileUploadItemAction.js";
|
|
9
|
+
import { FileUploadItemDeleteTrigger } from "./FileUploadItemDeleteTrigger.js";
|
|
10
|
+
import { FileUploadItemGroup } from "./FileUploadItemGroup.js";
|
|
11
|
+
import { FileUploadItemReplaceTrigger } from "./FileUploadItemReplaceTrigger.js";
|
|
12
|
+
import { FileUploadTrigger } from "./FileUploadTrigger.js";
|
|
13
|
+
const FILE_UPLOAD_URL = 'https://www.figma.com/design/VKb5gW46uSGw0rqrhZsbXT/WADS-Components?node-id=12403-10372';
|
|
14
|
+
const AREA_URL = 'https://www.figma.com/design/VKb5gW46uSGw0rqrhZsbXT/WADS-Components?node-id=12400-8762';
|
|
15
|
+
const ITEM_URL = 'https://www.figma.com/design/VKb5gW46uSGw0rqrhZsbXT/WADS-Components?node-id=12403-10197';
|
|
16
|
+
const rows = /*#__PURE__*/ jsx(FileUploadItemGroup, {
|
|
17
|
+
children: (file)=>/*#__PURE__*/ jsxs(FileUploadItem, {
|
|
18
|
+
file: file,
|
|
19
|
+
children: [
|
|
20
|
+
/*#__PURE__*/ jsx(FileUploadItemReplaceTrigger, {}),
|
|
21
|
+
/*#__PURE__*/ jsx(FileUploadItemDeleteTrigger, {})
|
|
22
|
+
]
|
|
23
|
+
})
|
|
24
|
+
});
|
|
25
|
+
const sharedProps = {
|
|
26
|
+
label: code_connect.boolean('Label', {
|
|
27
|
+
true: /*#__PURE__*/ jsx(FieldLabel, {
|
|
28
|
+
children: "Label"
|
|
29
|
+
}),
|
|
30
|
+
false: void 0
|
|
31
|
+
}),
|
|
32
|
+
items: code_connect.boolean('File items', {
|
|
33
|
+
true: rows,
|
|
34
|
+
false: void 0
|
|
35
|
+
})
|
|
36
|
+
};
|
|
37
|
+
code_connect.connect(FileUpload, FILE_UPLOAD_URL, {
|
|
38
|
+
variant: {
|
|
39
|
+
Type: 'Area'
|
|
40
|
+
},
|
|
41
|
+
props: sharedProps,
|
|
42
|
+
example: ({ label, items })=>/*#__PURE__*/ jsxs(Field, {
|
|
43
|
+
children: [
|
|
44
|
+
label,
|
|
45
|
+
/*#__PURE__*/ jsxs(FileUpload, {
|
|
46
|
+
children: [
|
|
47
|
+
/*#__PURE__*/ jsx(FileUploadDropzone, {}),
|
|
48
|
+
items
|
|
49
|
+
]
|
|
50
|
+
})
|
|
51
|
+
]
|
|
52
|
+
})
|
|
53
|
+
});
|
|
54
|
+
code_connect.connect(FileUpload, FILE_UPLOAD_URL, {
|
|
55
|
+
variant: {
|
|
56
|
+
Type: 'Button'
|
|
57
|
+
},
|
|
58
|
+
props: sharedProps,
|
|
59
|
+
example: ({ label, items })=>/*#__PURE__*/ jsxs(Field, {
|
|
60
|
+
children: [
|
|
61
|
+
label,
|
|
62
|
+
/*#__PURE__*/ jsxs(FileUpload, {
|
|
63
|
+
children: [
|
|
64
|
+
/*#__PURE__*/ jsx(FileUploadTrigger, {}),
|
|
65
|
+
items
|
|
66
|
+
]
|
|
67
|
+
})
|
|
68
|
+
]
|
|
69
|
+
})
|
|
70
|
+
});
|
|
71
|
+
code_connect.connect(FileUploadDropzone, AREA_URL, {
|
|
72
|
+
variant: {
|
|
73
|
+
State: 'Disable'
|
|
74
|
+
},
|
|
75
|
+
example: ()=>/*#__PURE__*/ jsx(FileUpload, {
|
|
76
|
+
disabled: true,
|
|
77
|
+
children: /*#__PURE__*/ jsx(FileUploadDropzone, {})
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
code_connect.connect(FileUploadDropzone, AREA_URL, {
|
|
81
|
+
example: ()=>/*#__PURE__*/ jsx(FileUploadDropzone, {})
|
|
82
|
+
});
|
|
83
|
+
code_connect.connect(FileUploadItem, ITEM_URL, {
|
|
84
|
+
props: {
|
|
85
|
+
loading: code_connect["enum"]('State', {
|
|
86
|
+
Loading: true,
|
|
87
|
+
Default: false
|
|
88
|
+
}),
|
|
89
|
+
description: code_connect.boolean("Description", {
|
|
90
|
+
true: "description",
|
|
91
|
+
false: void 0
|
|
92
|
+
})
|
|
93
|
+
},
|
|
94
|
+
example: ({ loading, description })=>/*#__PURE__*/ jsxs(FileUploadItem, {
|
|
95
|
+
file: {
|
|
96
|
+
name: 'file-name.format'
|
|
97
|
+
},
|
|
98
|
+
loading: loading,
|
|
99
|
+
description: description,
|
|
100
|
+
children: [
|
|
101
|
+
/*#__PURE__*/ jsx(FileUploadItemAction, {
|
|
102
|
+
"aria-label": "Download file-name.format",
|
|
103
|
+
children: /*#__PURE__*/ jsx(Download, {})
|
|
104
|
+
}),
|
|
105
|
+
/*#__PURE__*/ jsx(FileUploadItemDeleteTrigger, {})
|
|
106
|
+
]
|
|
107
|
+
})
|
|
108
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useFieldContext } from "@ark-ui/react/field";
|
|
4
|
+
import { FileUpload, useFileUpload } from "@ark-ui/react/file-upload";
|
|
5
|
+
import { cn } from "../../utils/cn.js";
|
|
6
|
+
import { TestIdProvider, useTestId } from "../../utils/testId.js";
|
|
7
|
+
import { fileUploadVariants } from "./classes.js";
|
|
8
|
+
import { FileUploadRootContextProvider } from "./FileUploadContext.js";
|
|
9
|
+
import { isSameFile, setInputFiles, toAcceptList, toAcceptString } from "./lib/index.js";
|
|
10
|
+
const FileUpload_FileUpload = ({ value, defaultValue, onValueChange, onFileReject, accept, maxFiles = 1, maxFileSize, minFileSize, validate, name, required, disabled, readOnly, error = false, allowDrop = true, className, children, ref, onDragOver, onDrop, 'data-testid': testIdProp, ...rest })=>{
|
|
11
|
+
const field = useFieldContext();
|
|
12
|
+
const inheritedTestId = useTestId();
|
|
13
|
+
const testId = testIdProp ?? inheritedTestId;
|
|
14
|
+
const isDisabled = disabled ?? field?.disabled ?? false;
|
|
15
|
+
const isReadOnly = readOnly ?? field?.readOnly ?? false;
|
|
16
|
+
const isRequired = required ?? field?.required ?? false;
|
|
17
|
+
const invalid = error || Boolean(field?.invalid);
|
|
18
|
+
const single = maxFiles <= 1;
|
|
19
|
+
const [loadingCount, setLoadingCount] = useState(0);
|
|
20
|
+
const registerLoading = useCallback(()=>{
|
|
21
|
+
setLoadingCount((count)=>count + 1);
|
|
22
|
+
return ()=>setLoadingCount((count)=>count - 1);
|
|
23
|
+
}, []);
|
|
24
|
+
const locked = loadingCount > 0;
|
|
25
|
+
const [heldCount, setHeldCount] = useState(defaultValue?.length ?? 0);
|
|
26
|
+
const atLimit = !single && (value?.length ?? heldCount) >= maxFiles;
|
|
27
|
+
const [replaceRejections, setReplaceRejections] = useState([]);
|
|
28
|
+
const acceptString = toAcceptString(accept);
|
|
29
|
+
const acceptList = useMemo(()=>toAcceptList(accept), [
|
|
30
|
+
accept
|
|
31
|
+
]);
|
|
32
|
+
const visible = useCallback((rejections)=>rejections.map((r)=>({
|
|
33
|
+
file: r.file,
|
|
34
|
+
errors: single ? r.errors.filter((e)=>'FILE_EXISTS' !== e) : r.errors
|
|
35
|
+
})).filter((r)=>r.errors.length > 0), [
|
|
36
|
+
single
|
|
37
|
+
]);
|
|
38
|
+
const handleAccept = useCallback((details)=>{
|
|
39
|
+
setReplaceRejections([]);
|
|
40
|
+
setHeldCount(details.files.length);
|
|
41
|
+
onValueChange?.(details.files);
|
|
42
|
+
}, [
|
|
43
|
+
onValueChange
|
|
44
|
+
]);
|
|
45
|
+
const hiddenInputRef = useRef(null);
|
|
46
|
+
const apiRef = useRef(null);
|
|
47
|
+
const writeHiddenInput = useCallback((files)=>{
|
|
48
|
+
if (hiddenInputRef.current) setInputFiles(hiddenInputRef.current, files);
|
|
49
|
+
}, []);
|
|
50
|
+
const commitFiles = useCallback((files)=>{
|
|
51
|
+
const current = apiRef.current;
|
|
52
|
+
if (!current) return;
|
|
53
|
+
const held = current.acceptedFiles;
|
|
54
|
+
const looksUnchanged = held.length === files.length && held.every((file, i)=>isSameFile(file, files[i]));
|
|
55
|
+
current.setFiles(files);
|
|
56
|
+
if (!looksUnchanged) return;
|
|
57
|
+
writeHiddenInput(files);
|
|
58
|
+
handleAccept({
|
|
59
|
+
files
|
|
60
|
+
});
|
|
61
|
+
}, [
|
|
62
|
+
writeHiddenInput,
|
|
63
|
+
handleAccept
|
|
64
|
+
]);
|
|
65
|
+
const handleReject = useCallback((details)=>{
|
|
66
|
+
const [only] = details.files;
|
|
67
|
+
const held = apiRef.current?.acceptedFiles[0];
|
|
68
|
+
if (single && 1 === details.files.length && only && held && 1 === only.errors.length && 'FILE_EXISTS' === only.errors[0] && only.file.lastModified !== held.lastModified) return void queueMicrotask(()=>commitFiles([
|
|
69
|
+
only.file
|
|
70
|
+
]));
|
|
71
|
+
const shown = visible(details.files);
|
|
72
|
+
if (shown.length) onFileReject?.(shown);
|
|
73
|
+
}, [
|
|
74
|
+
single,
|
|
75
|
+
visible,
|
|
76
|
+
onFileReject,
|
|
77
|
+
commitFiles
|
|
78
|
+
]);
|
|
79
|
+
const api = useFileUpload({
|
|
80
|
+
name,
|
|
81
|
+
accept: acceptString,
|
|
82
|
+
maxFiles,
|
|
83
|
+
maxFileSize,
|
|
84
|
+
minFileSize,
|
|
85
|
+
validate: validate ? (file)=>validate(file) : void 0,
|
|
86
|
+
required: isRequired,
|
|
87
|
+
disabled: isDisabled,
|
|
88
|
+
readOnly: isReadOnly,
|
|
89
|
+
invalid,
|
|
90
|
+
allowDrop: allowDrop && !locked && !atLimit,
|
|
91
|
+
preventDocumentDrop: false,
|
|
92
|
+
acceptedFiles: value,
|
|
93
|
+
defaultAcceptedFiles: defaultValue,
|
|
94
|
+
onFileAccept: handleAccept,
|
|
95
|
+
onFileReject: handleReject
|
|
96
|
+
});
|
|
97
|
+
apiRef.current = api;
|
|
98
|
+
const initialFiles = useRef(api.acceptedFiles);
|
|
99
|
+
useEffect(()=>{
|
|
100
|
+
if (initialFiles.current.length) writeHiddenInput(initialFiles.current);
|
|
101
|
+
}, [
|
|
102
|
+
writeHiddenInput
|
|
103
|
+
]);
|
|
104
|
+
const reportRejections = useCallback((rejections)=>{
|
|
105
|
+
setReplaceRejections(rejections);
|
|
106
|
+
if (rejections.length) onFileReject?.(rejections);
|
|
107
|
+
}, [
|
|
108
|
+
onFileReject
|
|
109
|
+
]);
|
|
110
|
+
const clearRejections = useCallback(()=>{
|
|
111
|
+
api.clearRejectedFiles();
|
|
112
|
+
setReplaceRejections([]);
|
|
113
|
+
}, [
|
|
114
|
+
api
|
|
115
|
+
]);
|
|
116
|
+
const guardPage = allowDrop && !isDisabled;
|
|
117
|
+
useEffect(()=>{
|
|
118
|
+
if (!guardPage) return;
|
|
119
|
+
const guard = (event)=>event.preventDefault();
|
|
120
|
+
document.addEventListener('dragover', guard);
|
|
121
|
+
document.addEventListener('drop', guard);
|
|
122
|
+
return ()=>{
|
|
123
|
+
document.removeEventListener('dragover', guard);
|
|
124
|
+
document.removeEventListener('drop', guard);
|
|
125
|
+
};
|
|
126
|
+
}, [
|
|
127
|
+
guardPage
|
|
128
|
+
]);
|
|
129
|
+
const handleDragOver = (event)=>{
|
|
130
|
+
onDragOver?.(event);
|
|
131
|
+
if (event.defaultPrevented) return;
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
try {
|
|
134
|
+
event.dataTransfer.dropEffect = 'none';
|
|
135
|
+
} catch {}
|
|
136
|
+
};
|
|
137
|
+
const handleDrop = (event)=>{
|
|
138
|
+
onDrop?.(event);
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
};
|
|
141
|
+
const pickerBlocked = isDisabled || locked || !single && api.maxFilesReached;
|
|
142
|
+
const errorId = useId();
|
|
143
|
+
const rejections = useMemo(()=>[
|
|
144
|
+
...visible(api.rejectedFiles),
|
|
145
|
+
...replaceRejections
|
|
146
|
+
], [
|
|
147
|
+
visible,
|
|
148
|
+
api.rejectedFiles,
|
|
149
|
+
replaceRejections
|
|
150
|
+
]);
|
|
151
|
+
const contextValue = useMemo(()=>({
|
|
152
|
+
api,
|
|
153
|
+
single,
|
|
154
|
+
disabled: isDisabled,
|
|
155
|
+
readOnly: isReadOnly,
|
|
156
|
+
invalid,
|
|
157
|
+
locked,
|
|
158
|
+
pickerHidden: isReadOnly,
|
|
159
|
+
pickerBlocked,
|
|
160
|
+
accept: acceptString,
|
|
161
|
+
limits: {
|
|
162
|
+
acceptList,
|
|
163
|
+
maxFiles,
|
|
164
|
+
maxFileSize,
|
|
165
|
+
minFileSize
|
|
166
|
+
},
|
|
167
|
+
validate,
|
|
168
|
+
rejections,
|
|
169
|
+
reportRejections,
|
|
170
|
+
clearRejections,
|
|
171
|
+
commitFiles,
|
|
172
|
+
registerLoading,
|
|
173
|
+
errorId
|
|
174
|
+
}), [
|
|
175
|
+
api,
|
|
176
|
+
single,
|
|
177
|
+
isDisabled,
|
|
178
|
+
isReadOnly,
|
|
179
|
+
invalid,
|
|
180
|
+
locked,
|
|
181
|
+
pickerBlocked,
|
|
182
|
+
acceptString,
|
|
183
|
+
acceptList,
|
|
184
|
+
maxFiles,
|
|
185
|
+
maxFileSize,
|
|
186
|
+
minFileSize,
|
|
187
|
+
validate,
|
|
188
|
+
rejections,
|
|
189
|
+
reportRejections,
|
|
190
|
+
clearRejections,
|
|
191
|
+
commitFiles,
|
|
192
|
+
registerLoading,
|
|
193
|
+
errorId
|
|
194
|
+
]);
|
|
195
|
+
return /*#__PURE__*/ jsxs(FileUpload.RootProvider, {
|
|
196
|
+
...rest,
|
|
197
|
+
value: api,
|
|
198
|
+
ref: ref,
|
|
199
|
+
onDragOver: handleDragOver,
|
|
200
|
+
onDrop: handleDrop,
|
|
201
|
+
"data-slot": "file-upload",
|
|
202
|
+
"data-testid": testIdProp,
|
|
203
|
+
className: cn(fileUploadVariants(), className),
|
|
204
|
+
children: [
|
|
205
|
+
/*#__PURE__*/ jsx(FileUploadRootContextProvider, {
|
|
206
|
+
value: contextValue,
|
|
207
|
+
children: /*#__PURE__*/ jsx(TestIdProvider, {
|
|
208
|
+
value: testId,
|
|
209
|
+
children: children
|
|
210
|
+
})
|
|
211
|
+
}),
|
|
212
|
+
/*#__PURE__*/ jsx(FileUpload.HiddenInput, {
|
|
213
|
+
ref: hiddenInputRef,
|
|
214
|
+
onClick: (event)=>{
|
|
215
|
+
if (pickerBlocked) event.preventDefault();
|
|
216
|
+
},
|
|
217
|
+
"data-slot": "file-upload-hidden-input",
|
|
218
|
+
"data-testid": testId ? `${testId}--hidden-input` : void 0
|
|
219
|
+
})
|
|
220
|
+
]
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
FileUpload_FileUpload.displayName = 'FileUpload';
|
|
224
|
+
export { FileUpload_FileUpload as FileUpload };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { fireEvent, screen } from "@testing-library/react";
|
|
3
|
+
import { FileUpload } from "./FileUpload.js";
|
|
4
|
+
import { FileUploadDropzone } from "./FileUploadDropzone.js";
|
|
5
|
+
import { FileUploadError } from "./FileUploadError.js";
|
|
6
|
+
import { FileUploadItem } from "./FileUploadItem.js";
|
|
7
|
+
import { FileUploadItemDeleteTrigger } from "./FileUploadItemDeleteTrigger.js";
|
|
8
|
+
import { FileUploadItemGroup } from "./FileUploadItemGroup.js";
|
|
9
|
+
import { FileUploadItemReplaceTrigger } from "./FileUploadItemReplaceTrigger.js";
|
|
10
|
+
const makeFile = (name = 'policy.wasm', size = 3, type = 'application/wasm', lastModified)=>new File([
|
|
11
|
+
'x'.repeat(size)
|
|
12
|
+
], name, {
|
|
13
|
+
type,
|
|
14
|
+
lastModified
|
|
15
|
+
});
|
|
16
|
+
const implOf = (wrapper)=>{
|
|
17
|
+
const symbol = Object.getOwnPropertySymbols(wrapper).find((s)=>'impl' === s.description);
|
|
18
|
+
return symbol ? wrapper[symbol] : void 0;
|
|
19
|
+
};
|
|
20
|
+
const installDataTransfer = ()=>{
|
|
21
|
+
class TestDataTransfer {
|
|
22
|
+
list = [];
|
|
23
|
+
items = {
|
|
24
|
+
add: (file)=>{
|
|
25
|
+
this.list.push(file);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
get files() {
|
|
29
|
+
const input = document.createElement('input');
|
|
30
|
+
input.type = 'file';
|
|
31
|
+
const files = input.files;
|
|
32
|
+
implOf(files).push(...this.list.map(implOf));
|
|
33
|
+
return files;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const win = document.defaultView;
|
|
37
|
+
const targets = [
|
|
38
|
+
win,
|
|
39
|
+
globalThis
|
|
40
|
+
];
|
|
41
|
+
const previous = targets.map((target)=>target.DataTransfer);
|
|
42
|
+
for (const target of targets)target.DataTransfer = TestDataTransfer;
|
|
43
|
+
return ()=>targets.forEach((target, i)=>{
|
|
44
|
+
if (void 0 === previous[i]) delete target.DataTransfer;
|
|
45
|
+
else target.DataTransfer = previous[i];
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const hiddenInput = (container)=>container.querySelector('[data-slot="file-upload-hidden-input"]');
|
|
49
|
+
const pick = (container, ...files)=>fireEvent.input(hiddenInput(container), {
|
|
50
|
+
target: {
|
|
51
|
+
files
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const fileTransfer = (...files)=>({
|
|
55
|
+
types: [
|
|
56
|
+
'Files'
|
|
57
|
+
],
|
|
58
|
+
dropEffect: 'none',
|
|
59
|
+
items: files.map((file)=>({
|
|
60
|
+
kind: 'file',
|
|
61
|
+
type: file.type,
|
|
62
|
+
getAsFile: ()=>file,
|
|
63
|
+
webkitGetAsEntry: ()=>({
|
|
64
|
+
isFile: true,
|
|
65
|
+
isDirectory: false,
|
|
66
|
+
name: file.name
|
|
67
|
+
})
|
|
68
|
+
})),
|
|
69
|
+
files
|
|
70
|
+
});
|
|
71
|
+
const dragOver = (el, ...files)=>fireEvent.dragOver(el, {
|
|
72
|
+
dataTransfer: fileTransfer(...files)
|
|
73
|
+
});
|
|
74
|
+
const drop = (el, ...files)=>fireEvent.drop(el, {
|
|
75
|
+
dataTransfer: fileTransfer(...files)
|
|
76
|
+
});
|
|
77
|
+
const nextFrame = ()=>new Promise((resolve)=>requestAnimationFrame(()=>resolve(null)));
|
|
78
|
+
const byTestId = (id)=>screen.getByTestId(id);
|
|
79
|
+
const queryByTestId = (id)=>screen.queryByTestId(id);
|
|
80
|
+
const Uploader = ({ children, ...props })=>/*#__PURE__*/ jsx(FileUpload, {
|
|
81
|
+
"data-testid": "fu",
|
|
82
|
+
name: "artifact",
|
|
83
|
+
...props,
|
|
84
|
+
children: children ?? /*#__PURE__*/ jsxs(Fragment, {
|
|
85
|
+
children: [
|
|
86
|
+
/*#__PURE__*/ jsx(FileUploadDropzone, {}),
|
|
87
|
+
/*#__PURE__*/ jsx(FileUploadError, {}),
|
|
88
|
+
/*#__PURE__*/ jsx(FileUploadItemGroup, {
|
|
89
|
+
children: (file)=>/*#__PURE__*/ jsxs(FileUploadItem, {
|
|
90
|
+
file: file,
|
|
91
|
+
children: [
|
|
92
|
+
/*#__PURE__*/ jsx(FileUploadItemReplaceTrigger, {}),
|
|
93
|
+
/*#__PURE__*/ jsx(FileUploadItemDeleteTrigger, {})
|
|
94
|
+
]
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
]
|
|
98
|
+
})
|
|
99
|
+
});
|
|
100
|
+
export { Uploader, byTestId, dragOver, drop, fileTransfer, hiddenInput, installDataTransfer, makeFile, nextFrame, pick, queryByTestId };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { UseFileUploadReturn } from '@ark-ui/react/file-upload';
|
|
2
|
+
import type { FileUploadLimits, FileUploadRejection } from './types';
|
|
3
|
+
export interface FileUploadRootContextValue {
|
|
4
|
+
/** The Ark/zag API (accepted files, deleteFile, setFiles, openFilePicker, …). */
|
|
5
|
+
api: UseFileUploadReturn;
|
|
6
|
+
/** `maxFiles === 1`: a new pick replaces the file; the picker stays visible. */
|
|
7
|
+
single: boolean;
|
|
8
|
+
disabled: boolean;
|
|
9
|
+
readOnly: boolean;
|
|
10
|
+
/** Own `error` or Field `invalid` (rejections are tracked separately in `rejections`). */
|
|
11
|
+
invalid: boolean;
|
|
12
|
+
/** Some `FileUploadItem loading` is mounted — the picker is locked until it finishes. */
|
|
13
|
+
locked: boolean;
|
|
14
|
+
/** Pickers render nothing while read-only (a chosen file never hides them). */
|
|
15
|
+
pickerHidden: boolean;
|
|
16
|
+
/** Pickers render but are inert: disabled, locked, or multiple mode at `maxFiles`. */
|
|
17
|
+
pickerBlocked: boolean;
|
|
18
|
+
/** Native / Ark accept string. */
|
|
19
|
+
accept: string | undefined;
|
|
20
|
+
limits: FileUploadLimits;
|
|
21
|
+
validate?: (file: File) => string[] | null;
|
|
22
|
+
/** Rejections to display (Ark's, minus single-mode FILE_EXISTS, plus multi-mode Replace ones). */
|
|
23
|
+
rejections: FileUploadRejection[];
|
|
24
|
+
/** Used by the multi-mode Replace, which validates outside Ark. */
|
|
25
|
+
reportRejections: (rejections: FileUploadRejection[]) => void;
|
|
26
|
+
/** Clears Ark's and the DS's rejections (on delete). */
|
|
27
|
+
clearRejections: () => void;
|
|
28
|
+
/**
|
|
29
|
+
* `api.setFiles` that also reports and writes the hidden input when zag sees no change
|
|
30
|
+
* (an edited file with the same name, size and type).
|
|
31
|
+
*/
|
|
32
|
+
commitFiles: (files: File[]) => void;
|
|
33
|
+
/** Registers one loading row; returns the unregister cleanup. */
|
|
34
|
+
registerLoading: () => () => void;
|
|
35
|
+
/** Id of `FileUploadError`, for `aria-describedby` on the pickers. */
|
|
36
|
+
errorId: string;
|
|
37
|
+
}
|
|
38
|
+
export declare const FileUploadRootContextProvider: import("react").Provider<FileUploadRootContextValue | null>;
|
|
39
|
+
/** Nullable — rows (`FileUploadItem*`) may render outside a `FileUpload` (stored-file lists). */
|
|
40
|
+
export declare const useFileUploadRootContext: () => FileUploadRootContextValue | null;
|
|
41
|
+
export declare const useRequiredFileUploadRootContext: (part: string) => FileUploadRootContextValue;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
const FileUploadRootContext = /*#__PURE__*/ createContext(null);
|
|
3
|
+
const FileUploadRootContextProvider = FileUploadRootContext.Provider;
|
|
4
|
+
const useFileUploadRootContext = ()=>useContext(FileUploadRootContext);
|
|
5
|
+
const useRequiredFileUploadRootContext = (part)=>{
|
|
6
|
+
const context = useContext(FileUploadRootContext);
|
|
7
|
+
if (!context) throw new Error(`${part} must be rendered inside <FileUpload>.`);
|
|
8
|
+
return context;
|
|
9
|
+
};
|
|
10
|
+
export { FileUploadRootContextProvider, useFileUploadRootContext, useRequiredFileUploadRootContext };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type FC, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import { type TestableProps } from '../../utils/testId';
|
|
3
|
+
export interface FileUploadDropzoneProps extends ComponentPropsWithoutRef<'div'>, TestableProps {
|
|
4
|
+
/** Replaces the default upload icon. */
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
/** Replaces the default text ("Drag and drop files or click to select"). */
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
ref?: Ref<HTMLDivElement>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Dashed drop Area — drop files on it, or click / Enter / Space to open the picker.
|
|
12
|
+
* Stays visible once a file is chosen; hidden only when read-only; inert while disabled,
|
|
13
|
+
* while a row is uploading, and at `maxFiles`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const FileUploadDropzone: FC<FileUploadDropzoneProps>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from "react";
|
|
3
|
+
import { useFieldContext } from "@ark-ui/react/field";
|
|
4
|
+
import { FileUpload } from "@ark-ui/react/file-upload";
|
|
5
|
+
import { Share } from "../../icons/index.js";
|
|
6
|
+
import { cn } from "../../utils/cn.js";
|
|
7
|
+
import { useTestId } from "../../utils/testId.js";
|
|
8
|
+
import { fileUploadDropzoneVariants } from "./classes.js";
|
|
9
|
+
import { useRequiredFileUploadRootContext } from "./FileUploadContext.js";
|
|
10
|
+
const DEFAULT_TEXT = 'Drag and drop files or click to select';
|
|
11
|
+
const FileUploadDropzone = ({ icon, children, className, ref, 'data-testid': testIdProp, 'aria-describedby': describedByProp, ...props })=>{
|
|
12
|
+
const ctx = useRequiredFileUploadRootContext('FileUploadDropzone');
|
|
13
|
+
const field = useFieldContext();
|
|
14
|
+
const testId = useTestId('dropzone', testIdProp);
|
|
15
|
+
const textId = useId();
|
|
16
|
+
if (ctx.pickerHidden) return null;
|
|
17
|
+
const blocked = ctx.pickerBlocked;
|
|
18
|
+
const hasRejections = ctx.rejections.length > 0;
|
|
19
|
+
const ownName = void 0 !== props['aria-label'] || void 0 !== props['aria-labelledby'];
|
|
20
|
+
const labelledBy = ownName ? void 0 : [
|
|
21
|
+
field?.ids.label,
|
|
22
|
+
textId
|
|
23
|
+
].filter(Boolean).join(' ');
|
|
24
|
+
const describedBy = [
|
|
25
|
+
describedByProp,
|
|
26
|
+
hasRejections ? ctx.errorId : void 0
|
|
27
|
+
].filter(Boolean).join(' ') || void 0;
|
|
28
|
+
return /*#__PURE__*/ jsxs(FileUpload.Dropzone, {
|
|
29
|
+
"aria-labelledby": labelledBy,
|
|
30
|
+
...props,
|
|
31
|
+
"aria-describedby": describedBy,
|
|
32
|
+
ref: ref,
|
|
33
|
+
disableClick: blocked,
|
|
34
|
+
role: "button",
|
|
35
|
+
"aria-disabled": blocked || void 0,
|
|
36
|
+
"data-disabled": blocked ? '' : void 0,
|
|
37
|
+
"data-invalid": ctx.invalid || hasRejections ? '' : void 0,
|
|
38
|
+
"data-slot": "file-upload-dropzone",
|
|
39
|
+
"data-testid": testId,
|
|
40
|
+
className: cn(fileUploadDropzoneVariants(), className),
|
|
41
|
+
children: [
|
|
42
|
+
icon ?? /*#__PURE__*/ jsx(Share, {
|
|
43
|
+
size: "md"
|
|
44
|
+
}),
|
|
45
|
+
/*#__PURE__*/ jsx("span", {
|
|
46
|
+
id: textId,
|
|
47
|
+
children: children ?? DEFAULT_TEXT
|
|
48
|
+
})
|
|
49
|
+
]
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
FileUploadDropzone.displayName = 'FileUploadDropzone';
|
|
53
|
+
export { FileUploadDropzone };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, FC, ReactNode, Ref } from 'react';
|
|
2
|
+
import { type TestableProps } from '../../utils/testId';
|
|
3
|
+
import type { FileUploadErrorCode, FileUploadRejection } from './types';
|
|
4
|
+
export interface FileUploadErrorProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children'>, TestableProps {
|
|
5
|
+
/** Override a message: called once per (file, error) pair. */
|
|
6
|
+
children?: (rejection: FileUploadRejection, code: FileUploadErrorCode) => ReactNode;
|
|
7
|
+
ref?: Ref<HTMLDivElement>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Inline rejection message — names the file and the rule it broke ("policy.txt — Not a
|
|
11
|
+
* .wasm file"). Renders nothing until a pick, drop or replace is rejected; clears on the
|
|
12
|
+
* next accepted file or on delete. Styled like `FieldError`; use `FieldError` itself for
|
|
13
|
+
* form-level errors (e.g. "required"). Test id slot: `{base}--rejections`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const FileUploadError: FC<FileUploadErrorProps>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { OctagonAlert } from "../../icons/index.js";
|
|
3
|
+
import { cn } from "../../utils/cn.js";
|
|
4
|
+
import { useTestId } from "../../utils/testId.js";
|
|
5
|
+
import { Text } from "../Text/index.js";
|
|
6
|
+
import { fileUploadErrorClassNames } from "./classes.js";
|
|
7
|
+
import { useRequiredFileUploadRootContext } from "./FileUploadContext.js";
|
|
8
|
+
import { formatRejection } from "./lib/index.js";
|
|
9
|
+
const FileUploadError = ({ children, className, ref, 'data-testid': testIdProp, ...props })=>{
|
|
10
|
+
const ctx = useRequiredFileUploadRootContext('FileUploadError');
|
|
11
|
+
const testId = useTestId('rejections', testIdProp);
|
|
12
|
+
if (0 === ctx.rejections.length) return null;
|
|
13
|
+
return /*#__PURE__*/ jsx("div", {
|
|
14
|
+
...props,
|
|
15
|
+
ref: ref,
|
|
16
|
+
id: ctx.errorId,
|
|
17
|
+
role: "alert",
|
|
18
|
+
"data-slot": "file-upload-error",
|
|
19
|
+
"data-testid": testId,
|
|
20
|
+
className: cn(fileUploadErrorClassNames, className),
|
|
21
|
+
children: ctx.rejections.flatMap((rejection, i)=>rejection.errors.map((code)=>/*#__PURE__*/ jsxs("div", {
|
|
22
|
+
className: "flex gap-4",
|
|
23
|
+
children: [
|
|
24
|
+
/*#__PURE__*/ jsx(OctagonAlert, {
|
|
25
|
+
size: "md",
|
|
26
|
+
className: "my-2 shrink-0 self-start"
|
|
27
|
+
}),
|
|
28
|
+
/*#__PURE__*/ jsx(Text, {
|
|
29
|
+
size: "sm",
|
|
30
|
+
color: "danger",
|
|
31
|
+
children: children ? children(rejection, code) : formatRejection(rejection.file.name, code, ctx.limits, rejection.file.size)
|
|
32
|
+
})
|
|
33
|
+
]
|
|
34
|
+
}, `${i}-${code}`)))
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
FileUploadError.displayName = 'FileUploadError';
|
|
38
|
+
export { FileUploadError };
|