@colixsystems/widget-sdk 0.237.0 → 0.239.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/README.md +39 -4
- package/dist/checkbox-view.js +28 -16
- package/dist/contract.cjs +26 -12
- package/dist/contract.js +26 -12
- package/dist/file-upload-logic.js +178 -0
- package/dist/file-upload-view.js +321 -0
- package/dist/file-upload.js +62 -0
- package/dist/file-upload.native.js +11 -0
- package/dist/filepicker.js +11 -0
- package/dist/filepicker.native.js +10 -1
- package/dist/form-control-view.js +8 -1
- package/dist/index.d.ts +162 -16
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/list-tokens.js +54 -14
- package/dist/list-view.js +199 -82
- package/dist/list.js +7 -1
- package/dist/list.native.js +3 -2
- package/dist/primitives.js +1 -3
- package/dist/primitives.native.js +2 -4
- package/dist/property-schema.js +6 -0
- package/dist/select-logic.js +83 -0
- package/dist/select-view.js +165 -32
- package/dist/select.js +6 -1
- package/dist/select.native.js +5 -3
- package/package.json +1 -1
- package/src/contract.js +26 -12
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// The ONE implementation behind `<FileUpload>`. It is built on `<FilePicker>`,
|
|
2
|
+
// which owns opening each platform's file dialog, and draws the themed field
|
|
3
|
+
// around it: the button, the chosen files with their size and a remove
|
|
4
|
+
// control, and the uploading, error and disabled states. It uploads nothing
|
|
5
|
+
// itself; the caller passes `onPick` to `useFilestoreUpload().upload`.
|
|
6
|
+
//
|
|
7
|
+
// The bindings differ in their primitives and in one extra: the web binding
|
|
8
|
+
// passes a `DropZone`, so files can also be dropped onto the field there.
|
|
9
|
+
|
|
10
|
+
import React from "react";
|
|
11
|
+
import { useHostSharedText, useHostTheme } from "./hooks.js";
|
|
12
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
13
|
+
import { Icon } from "./icon.js";
|
|
14
|
+
import { focusHaloStyle, resolveFormControlTokens } from "./form-control-tokens.js";
|
|
15
|
+
import { errorMessage, hasNote, isShown, isText } from "./form-control-logic.js";
|
|
16
|
+
import { makeFieldFrame } from "./form-control-view.js";
|
|
17
|
+
import { fileKey, formatFileSize, pickPayload, planPick } from "./file-upload-logic.js";
|
|
18
|
+
|
|
19
|
+
const h = React.createElement;
|
|
20
|
+
// The remove control's real size; a web host ignores hitSlop.
|
|
21
|
+
const REMOVE_TARGET = 36;
|
|
22
|
+
const HIDDEN = {
|
|
23
|
+
"aria-hidden": true,
|
|
24
|
+
importantForAccessibility: "no-hide-descendants",
|
|
25
|
+
accessibilityElementsHidden: true,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Bind the shared file upload to one platform's primitives.
|
|
30
|
+
*
|
|
31
|
+
* @param {{View: any, Text: any, Pressable: any, Platform: any, FilePicker: any, Button: any, Spinner: any}} rn
|
|
32
|
+
* @param {{DropZone?: any}} [extras] — the web's drop target; absent on native.
|
|
33
|
+
*/
|
|
34
|
+
export function makeFileUpload(rn, extras) {
|
|
35
|
+
const { View, Text, Pressable, Platform, FilePicker, Button, Spinner } = rn;
|
|
36
|
+
const DropZone = (extras && extras.DropZone) || null;
|
|
37
|
+
const FieldFrame = makeFieldFrame(rn);
|
|
38
|
+
|
|
39
|
+
// Keyboard focus is a web concern; a device's reader moves on by itself.
|
|
40
|
+
function focusNode(node) {
|
|
41
|
+
if (Platform.OS !== "web" || !node) return false;
|
|
42
|
+
const target =
|
|
43
|
+
node.tabIndex >= 0 || typeof node.querySelector !== "function"
|
|
44
|
+
? node
|
|
45
|
+
: node.querySelector("input");
|
|
46
|
+
if (!target || typeof target.focus !== "function") return false;
|
|
47
|
+
target.focus();
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function FileRow({ t, file, index, blocked, removeLabel, onRemove, removeRef, testID }) {
|
|
52
|
+
const size = formatFileSize(file && file.size);
|
|
53
|
+
const name = file && typeof file.name === "string" && file.name ? file.name : "file";
|
|
54
|
+
return h(
|
|
55
|
+
View,
|
|
56
|
+
{
|
|
57
|
+
testID: testID ? `${testID}-file-${index}` : undefined,
|
|
58
|
+
style: {
|
|
59
|
+
flexDirection: "row",
|
|
60
|
+
alignItems: "center",
|
|
61
|
+
gap: t.rowGap,
|
|
62
|
+
paddingLeft: t.paddingHorizontal,
|
|
63
|
+
minHeight: REMOVE_TARGET,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
h(View, HIDDEN, h(Icon, { name: "file", size: 16, color: t.placeholderColor })),
|
|
67
|
+
h(
|
|
68
|
+
Text,
|
|
69
|
+
{
|
|
70
|
+
numberOfLines: 1,
|
|
71
|
+
style: { flexShrink: 1, color: t.textColor, fontFamily: t.fontFamily, fontSize: t.labelSize },
|
|
72
|
+
},
|
|
73
|
+
name,
|
|
74
|
+
),
|
|
75
|
+
size
|
|
76
|
+
? h(
|
|
77
|
+
Text,
|
|
78
|
+
{ style: { color: t.helperColor, fontFamily: t.fontFamily, fontSize: t.helperSize } },
|
|
79
|
+
size,
|
|
80
|
+
)
|
|
81
|
+
: null,
|
|
82
|
+
h(View, { style: { flex: 1 } }),
|
|
83
|
+
h(
|
|
84
|
+
Pressable,
|
|
85
|
+
{
|
|
86
|
+
ref: removeRef,
|
|
87
|
+
onPress: () => onRemove(index),
|
|
88
|
+
disabled: blocked,
|
|
89
|
+
accessibilityRole: "button",
|
|
90
|
+
accessibilityLabel: `${removeLabel} ${name}`,
|
|
91
|
+
"aria-disabled": blocked,
|
|
92
|
+
testID: testID ? `${testID}-remove-${index}` : undefined,
|
|
93
|
+
style: ({ pressed }) => [
|
|
94
|
+
{
|
|
95
|
+
width: REMOVE_TARGET,
|
|
96
|
+
height: REMOVE_TARGET,
|
|
97
|
+
alignItems: "center",
|
|
98
|
+
justifyContent: "center",
|
|
99
|
+
borderRadius: t.radius,
|
|
100
|
+
},
|
|
101
|
+
pressed ? { opacity: 0.7 } : null,
|
|
102
|
+
blocked ? { opacity: t.disabledOpacity } : null,
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
h(Icon, { name: "x", size: 16, color: t.placeholderColor }),
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {object} props — see `FileUploadProps` in index.d.ts.
|
|
112
|
+
*/
|
|
113
|
+
function FileUpload(props) {
|
|
114
|
+
const {
|
|
115
|
+
accept,
|
|
116
|
+
multiple = false,
|
|
117
|
+
files,
|
|
118
|
+
onFilesChange,
|
|
119
|
+
onPick,
|
|
120
|
+
onRemove,
|
|
121
|
+
onReject,
|
|
122
|
+
uploading = false,
|
|
123
|
+
label,
|
|
124
|
+
helper,
|
|
125
|
+
error,
|
|
126
|
+
disabled = false,
|
|
127
|
+
buttonLabel = multiple ? "Choose files" : "Choose file",
|
|
128
|
+
buttonVariant = "outlined",
|
|
129
|
+
uploadingLabel = "Uploading…",
|
|
130
|
+
emptyLabel = multiple ? "No files chosen" : "No file chosen",
|
|
131
|
+
dropLabel = multiple ? "or drop files here" : "or drop a file here",
|
|
132
|
+
rejectedMessage = "That file type isn't accepted here.",
|
|
133
|
+
singleFileMessage = "Only one file can be added here, so the first was kept.",
|
|
134
|
+
unsupportedMessage = "Choosing files isn't available here.",
|
|
135
|
+
removeAccessibilityLabel,
|
|
136
|
+
accessibilityLabel,
|
|
137
|
+
style,
|
|
138
|
+
containerStyle,
|
|
139
|
+
testID,
|
|
140
|
+
} = props;
|
|
141
|
+
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
142
|
+
const t = resolveFormControlTokens(theme);
|
|
143
|
+
const sharedRemove = useHostSharedText("remove", "Remove");
|
|
144
|
+
const [ownFiles, setOwnFiles] = React.useState([]);
|
|
145
|
+
const [rejection, setRejection] = React.useState(null);
|
|
146
|
+
const [focused, setFocused] = React.useState(false);
|
|
147
|
+
const [dragging, setDragging] = React.useState(false);
|
|
148
|
+
const noteId = React.useId();
|
|
149
|
+
const pickerRef = React.useRef(null);
|
|
150
|
+
const removeRefs = React.useRef([]);
|
|
151
|
+
const pendingFocus = React.useRef(null);
|
|
152
|
+
|
|
153
|
+
// The newest message shows: a new `error` replaces an older refusal.
|
|
154
|
+
const [seenError, setSeenError] = React.useState(error);
|
|
155
|
+
if (error !== seenError) {
|
|
156
|
+
setSeenError(error);
|
|
157
|
+
if (isShown(error)) setRejection(null);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const supported = FilePicker.isSupported !== false;
|
|
161
|
+
const blocked = disabled || uploading || !supported;
|
|
162
|
+
const controlled = Array.isArray(files);
|
|
163
|
+
const chosen = controlled ? files.filter(Boolean) : ownFiles;
|
|
164
|
+
const shownError = rejection || errorMessage(error);
|
|
165
|
+
const invalid = isShown(error) || rejection !== null;
|
|
166
|
+
const helperShown = supported ? helper : unsupportedMessage;
|
|
167
|
+
const noted = hasNote({ helper: helperShown, error: shownError });
|
|
168
|
+
|
|
169
|
+
const commit = (next) => {
|
|
170
|
+
if (!controlled) setOwnFiles(next);
|
|
171
|
+
if (typeof onFilesChange === "function") onFilesChange(next);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const receive = (picked) => {
|
|
175
|
+
if (blocked) return;
|
|
176
|
+
const { added, rejected, extra, next } = planPick(chosen, picked, accept, multiple);
|
|
177
|
+
setRejection(
|
|
178
|
+
rejected.length > 0 ? rejectedMessage : extra.length > 0 ? singleFileMessage : null,
|
|
179
|
+
);
|
|
180
|
+
if (typeof onReject === "function") {
|
|
181
|
+
if (rejected.length > 0) onReject(rejected, "type");
|
|
182
|
+
if (extra.length > 0) onReject(extra, "count");
|
|
183
|
+
}
|
|
184
|
+
if (added.length === 0) return;
|
|
185
|
+
commit(next);
|
|
186
|
+
if (typeof onPick === "function") onPick(pickPayload(added, multiple));
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const remove = (index) => {
|
|
190
|
+
if (blocked) return;
|
|
191
|
+
const file = chosen[index];
|
|
192
|
+
setRejection(null);
|
|
193
|
+
pendingFocus.current = { file, index };
|
|
194
|
+
commit(chosen.filter((_, i) => i !== index));
|
|
195
|
+
if (typeof onRemove === "function") onRemove(file, index);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// The pressed remove button is gone: focus its neighbour, else the picker.
|
|
199
|
+
React.useEffect(() => {
|
|
200
|
+
const pending = pendingFocus.current;
|
|
201
|
+
if (!pending || chosen[pending.index] === pending.file) return;
|
|
202
|
+
pendingFocus.current = null;
|
|
203
|
+
const neighbour = Math.min(pending.index, chosen.length - 1);
|
|
204
|
+
if (!focusNode(neighbour >= 0 ? removeRefs.current[neighbour] : null)) {
|
|
205
|
+
focusNode(pickerRef.current);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const status = uploading
|
|
210
|
+
? null
|
|
211
|
+
: DropZone && !blocked
|
|
212
|
+
? dropLabel
|
|
213
|
+
: chosen.length === 0
|
|
214
|
+
? emptyLabel
|
|
215
|
+
: null;
|
|
216
|
+
const statusText = { color: t.placeholderColor, fontFamily: t.fontFamily, fontSize: t.labelSize };
|
|
217
|
+
|
|
218
|
+
const field = h(
|
|
219
|
+
View,
|
|
220
|
+
{
|
|
221
|
+
testID,
|
|
222
|
+
style: [
|
|
223
|
+
{
|
|
224
|
+
flexDirection: "row",
|
|
225
|
+
alignItems: "center",
|
|
226
|
+
flexWrap: "wrap",
|
|
227
|
+
gap: t.rowGap,
|
|
228
|
+
minHeight: t.height,
|
|
229
|
+
padding: t.rowGap,
|
|
230
|
+
borderWidth: 1,
|
|
231
|
+
borderStyle: DropZone ? "dashed" : "solid",
|
|
232
|
+
borderColor: invalid ? t.errorColor : t.borderColor,
|
|
233
|
+
borderRadius: t.radius,
|
|
234
|
+
backgroundColor: t.background,
|
|
235
|
+
},
|
|
236
|
+
dragging && !blocked ? { borderColor: t.focusColor, borderStyle: "solid" } : null,
|
|
237
|
+
style,
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
h(
|
|
241
|
+
View,
|
|
242
|
+
{ ref: pickerRef, style: focusHaloStyle(t, focused && !blocked, t.radius) },
|
|
243
|
+
h(
|
|
244
|
+
FilePicker,
|
|
245
|
+
{
|
|
246
|
+
accept,
|
|
247
|
+
multiple,
|
|
248
|
+
disabled: blocked,
|
|
249
|
+
onPick: receive,
|
|
250
|
+
onFocus: () => setFocused(true),
|
|
251
|
+
onBlur: () => setFocused(false),
|
|
252
|
+
accessibilityLabel,
|
|
253
|
+
"aria-invalid": invalid || undefined,
|
|
254
|
+
"aria-describedby": noted ? noteId : undefined,
|
|
255
|
+
},
|
|
256
|
+
h(Button, {
|
|
257
|
+
title: buttonLabel,
|
|
258
|
+
icon: uploading ? undefined : "upload",
|
|
259
|
+
variant: buttonVariant,
|
|
260
|
+
testID: testID ? `${testID}-button` : undefined,
|
|
261
|
+
}),
|
|
262
|
+
),
|
|
263
|
+
),
|
|
264
|
+
uploading
|
|
265
|
+
? h(
|
|
266
|
+
View,
|
|
267
|
+
{ style: { flexDirection: "row", alignItems: "center", gap: t.gap } },
|
|
268
|
+
h(Spinner, { size: "sm", accessibilityLabel: isText(uploadingLabel) ? String(uploadingLabel) : undefined }),
|
|
269
|
+
h(Text, { ...HIDDEN, style: statusText }, uploadingLabel),
|
|
270
|
+
)
|
|
271
|
+
: null,
|
|
272
|
+
isShown(status)
|
|
273
|
+
? h(Text, { style: [statusText, { flexShrink: 1 }, blocked ? { opacity: t.disabledOpacity } : null] }, status)
|
|
274
|
+
: null,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const list =
|
|
278
|
+
chosen.length > 0
|
|
279
|
+
? h(
|
|
280
|
+
View,
|
|
281
|
+
{ style: [{ gap: 2 }, disabled ? { opacity: t.disabledOpacity } : null] },
|
|
282
|
+
...chosen.map((file, index) =>
|
|
283
|
+
h(FileRow, {
|
|
284
|
+
key: fileKey(file, index),
|
|
285
|
+
t,
|
|
286
|
+
file,
|
|
287
|
+
index,
|
|
288
|
+
blocked,
|
|
289
|
+
removeLabel: isShown(removeAccessibilityLabel) ? removeAccessibilityLabel : sharedRemove,
|
|
290
|
+
onRemove: remove,
|
|
291
|
+
removeRef: (node) => {
|
|
292
|
+
removeRefs.current[index] = node;
|
|
293
|
+
},
|
|
294
|
+
testID,
|
|
295
|
+
}),
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
: null;
|
|
299
|
+
|
|
300
|
+
const body = h(View, { style: { gap: t.gap } }, field, list);
|
|
301
|
+
|
|
302
|
+
return h(
|
|
303
|
+
FieldFrame,
|
|
304
|
+
{
|
|
305
|
+
t,
|
|
306
|
+
label,
|
|
307
|
+
helper: helperShown,
|
|
308
|
+
error: shownError,
|
|
309
|
+
noteId,
|
|
310
|
+
alertOnError: true,
|
|
311
|
+
style: containerStyle,
|
|
312
|
+
},
|
|
313
|
+
DropZone
|
|
314
|
+
? h(DropZone, { disabled: blocked, onFiles: receive, onActiveChange: setDragging }, body)
|
|
315
|
+
: body,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
FileUpload.displayName = "FileUpload";
|
|
320
|
+
return FileUpload;
|
|
321
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// `<FileUpload>` (web). Binds ./file-upload-view.js to react-native-web and
|
|
2
|
+
// the web `<FilePicker>`, and adds the one web extra: files dropped onto the
|
|
3
|
+
// field are taken as if picked. Keyboard and screen-reader users have the
|
|
4
|
+
// button, which is the whole control on native.
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, Pressable, Platform } from "react-native-web";
|
|
8
|
+
import { makeFileUpload } from "./file-upload-view.js";
|
|
9
|
+
import { FilePicker } from "./filepicker.js";
|
|
10
|
+
import { Button } from "./button.js";
|
|
11
|
+
import { Spinner } from "./loading.js";
|
|
12
|
+
|
|
13
|
+
const DROP_ZONE_STYLE = { display: "flex", flexDirection: "column", minWidth: 0 };
|
|
14
|
+
|
|
15
|
+
function carriesFiles(event) {
|
|
16
|
+
const types = event.dataTransfer && event.dataTransfer.types;
|
|
17
|
+
return Boolean(types) && Array.from(types).includes("Files");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The field's drop target. A drop is always claimed, even while disabled,
|
|
22
|
+
* so the browser never navigates away to the dropped file.
|
|
23
|
+
*/
|
|
24
|
+
function DropZone({ disabled, onFiles, onActiveChange, children }) {
|
|
25
|
+
// Entering a child fires dragleave on the parent; count the depth.
|
|
26
|
+
const depth = React.useRef(0);
|
|
27
|
+
return React.createElement(
|
|
28
|
+
"div",
|
|
29
|
+
{
|
|
30
|
+
style: DROP_ZONE_STYLE,
|
|
31
|
+
onDragEnter: (event) => {
|
|
32
|
+
if (!carriesFiles(event)) return;
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
depth.current += 1;
|
|
35
|
+
if (!disabled) onActiveChange(true);
|
|
36
|
+
},
|
|
37
|
+
onDragOver: (event) => {
|
|
38
|
+
if (!carriesFiles(event)) return;
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
event.dataTransfer.dropEffect = disabled ? "none" : "copy";
|
|
41
|
+
},
|
|
42
|
+
onDragLeave: (event) => {
|
|
43
|
+
if (!carriesFiles(event)) return;
|
|
44
|
+
depth.current = Math.max(0, depth.current - 1);
|
|
45
|
+
if (depth.current === 0) onActiveChange(false);
|
|
46
|
+
},
|
|
47
|
+
onDrop: (event) => {
|
|
48
|
+
if (!carriesFiles(event)) return;
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
depth.current = 0;
|
|
51
|
+
onActiveChange(false);
|
|
52
|
+
if (!disabled) onFiles(Array.from(event.dataTransfer.files || []));
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
children,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const FileUpload = makeFileUpload(
|
|
60
|
+
{ View, Text, Pressable, Platform, FilePicker, Button, Spinner },
|
|
61
|
+
{ DropZone },
|
|
62
|
+
);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// `<FileUpload>` (native). Binds ./file-upload-view.js to React Native and the
|
|
2
|
+
// native `<FilePicker>` (the OS document picker). No drop target: React Native
|
|
3
|
+
// has no events for files dragged onto an app, so the button is the control.
|
|
4
|
+
|
|
5
|
+
import { View, Text, Pressable, Platform } from "react-native";
|
|
6
|
+
import { makeFileUpload } from "./file-upload-view.js";
|
|
7
|
+
import { FilePicker } from "./filepicker.native.js";
|
|
8
|
+
import { Button } from "./button.native.js";
|
|
9
|
+
import { Spinner } from "./loading.native.js";
|
|
10
|
+
|
|
11
|
+
export const FileUpload = makeFileUpload({ View, Text, Pressable, Platform, FilePicker, Button, Spinner });
|
package/dist/filepicker.js
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
// `<label>` so the click reaches the hidden input
|
|
22
22
|
// everywhere — no manual ref / `.click()` plumbing needed.
|
|
23
23
|
// accessibilityLabel: string? — written to the wrapper label.
|
|
24
|
+
// onFocus / onBlur: () => void — the hidden input's focus, so a
|
|
25
|
+
// caller can draw a focus ring on its visible trigger.
|
|
26
|
+
// aria-describedby / aria-invalid — tie the picker to a field's note.
|
|
24
27
|
|
|
25
28
|
import React, { useRef, useCallback } from "react";
|
|
26
29
|
|
|
@@ -54,6 +57,10 @@ export function FilePicker({
|
|
|
54
57
|
onPick,
|
|
55
58
|
children,
|
|
56
59
|
accessibilityLabel,
|
|
60
|
+
onFocus,
|
|
61
|
+
onBlur,
|
|
62
|
+
"aria-describedby": describedBy,
|
|
63
|
+
"aria-invalid": invalid,
|
|
57
64
|
}) {
|
|
58
65
|
const inputRef = useRef(null);
|
|
59
66
|
|
|
@@ -84,6 +91,10 @@ export function FilePicker({
|
|
|
84
91
|
multiple,
|
|
85
92
|
disabled,
|
|
86
93
|
onChange: handleChange,
|
|
94
|
+
onFocus,
|
|
95
|
+
onBlur,
|
|
96
|
+
"aria-describedby": describedBy,
|
|
97
|
+
"aria-invalid": invalid || undefined,
|
|
87
98
|
style: HIDDEN_INPUT_STYLE,
|
|
88
99
|
}),
|
|
89
100
|
children,
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// `<input type="file">` because react-native-web has no equivalent of the
|
|
7
7
|
// system file picker; this native variant calls the OS document picker
|
|
8
8
|
// directly. Both honour the same public contract — `accept` / `multiple`
|
|
9
|
-
// / `disabled` / `onPick` / `children` / `accessibilityLabel
|
|
9
|
+
// / `disabled` / `onPick` / `children` / `accessibilityLabel` / `onFocus`
|
|
10
|
+
// / `onBlur` / `aria-describedby` / `aria-invalid`.
|
|
10
11
|
//
|
|
11
12
|
// `onPick` receives the same shape on both platforms:
|
|
12
13
|
// - web → the browser's `File` object (a `Blob` with name + type).
|
|
@@ -73,6 +74,10 @@ export function FilePicker({
|
|
|
73
74
|
onPick,
|
|
74
75
|
children,
|
|
75
76
|
accessibilityLabel,
|
|
77
|
+
onFocus,
|
|
78
|
+
onBlur,
|
|
79
|
+
"aria-describedby": describedBy,
|
|
80
|
+
"aria-invalid": invalid,
|
|
76
81
|
}) {
|
|
77
82
|
const type = useMemo(() => _mapAccept(accept), [accept]);
|
|
78
83
|
|
|
@@ -103,10 +108,14 @@ export function FilePicker({
|
|
|
103
108
|
Pressable,
|
|
104
109
|
{
|
|
105
110
|
onPress: handlePress,
|
|
111
|
+
onFocus,
|
|
112
|
+
onBlur,
|
|
106
113
|
disabled,
|
|
107
114
|
accessibilityRole: "button",
|
|
108
115
|
accessibilityLabel,
|
|
109
116
|
accessibilityState: { disabled },
|
|
117
|
+
"aria-describedby": describedBy,
|
|
118
|
+
"aria-invalid": invalid || undefined,
|
|
110
119
|
},
|
|
111
120
|
React.createElement(View, { style: disabled ? { opacity: 0.5 } : undefined }, children),
|
|
112
121
|
);
|
|
@@ -26,6 +26,8 @@ export function makeFieldFrame(rn) {
|
|
|
26
26
|
* @param {() => void} [props.onLabelPress] — a text label then acts for the control.
|
|
27
27
|
* @param {boolean} [props.noteDisabled] — dim the note alone, when the
|
|
28
28
|
* control dims itself and `disabled` would dim it twice.
|
|
29
|
+
* @param {boolean} [props.alertOnError] — an error interrupts, as an alert,
|
|
30
|
+
* for one that follows an action rather than typing.
|
|
29
31
|
*/
|
|
30
32
|
function FieldFrame({
|
|
31
33
|
t,
|
|
@@ -36,6 +38,7 @@ export function makeFieldFrame(rn) {
|
|
|
36
38
|
noteId,
|
|
37
39
|
onLabelPress,
|
|
38
40
|
noteDisabled,
|
|
41
|
+
alertOnError,
|
|
39
42
|
style,
|
|
40
43
|
children,
|
|
41
44
|
}) {
|
|
@@ -79,7 +82,11 @@ export function makeFieldFrame(rn) {
|
|
|
79
82
|
{
|
|
80
83
|
nativeID: noteId,
|
|
81
84
|
// Announced where the host can: the web and Android.
|
|
82
|
-
...(message
|
|
85
|
+
...(message
|
|
86
|
+
? alertOnError
|
|
87
|
+
? { accessibilityRole: "alert", accessibilityLiveRegion: "assertive" }
|
|
88
|
+
: { accessibilityLiveRegion: "polite" }
|
|
89
|
+
: null),
|
|
83
90
|
style: {
|
|
84
91
|
color: message ? t.errorColor : t.helperColor,
|
|
85
92
|
fontFamily: t.fontFamily,
|