@eifi1/ui-kit 0.22.0 → 0.23.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 +42 -25
- package/dist/chart.d.ts +11 -8
- package/dist/components/amount-input.d.ts +28 -8
- package/dist/components/amount-input.js +3 -1
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +21 -0
- package/dist/components/autocomplete.js +128 -103
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +11 -8
- package/dist/components/calculator.d.ts +11 -8
- package/dist/components/column-mapper.d.ts +182 -0
- package/dist/components/column-mapper.js +375 -0
- package/dist/components/column-mapper.js.map +1 -0
- package/dist/components/combobox-core.d.ts +39 -2
- package/dist/components/combobox-core.js +29 -6
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +80 -2
- package/dist/components/combobox.js +448 -311
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +11 -8
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +8 -5
- package/dist/components/country-select.d.ts +50 -7
- package/dist/components/country-select.js +65 -9
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +11 -8
- package/dist/components/danger-confirm.js +13 -3
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/date-picker.d.ts +41 -0
- package/dist/components/date-picker.js +269 -8
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +35 -1
- package/dist/components/entity-combobox.js +148 -112
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +11 -8
- package/dist/components/field-parts.d.ts +116 -6
- package/dist/components/field-parts.js +84 -0
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-strip.d.ts +105 -0
- package/dist/components/field-strip.js +46 -0
- package/dist/components/field-strip.js.map +1 -0
- package/dist/components/file-button.d.ts +11 -8
- package/dist/components/file-dropzone.d.ts +11 -8
- package/dist/components/form-actions.d.ts +10 -7
- package/dist/components/form-actions.js +8 -2
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +11 -8
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +12 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/language-select.d.ts +11 -8
- package/dist/components/money-field.d.ts +14 -8
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +2 -1
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +26 -1
- package/dist/components/multi-entity-combobox.js +143 -102
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +11 -8
- package/dist/components/number-input.d.ts +11 -8
- package/dist/components/numpad-sheet.d.ts +11 -8
- package/dist/components/phone-input.d.ts +11 -8
- package/dist/components/reauth-dialog.d.ts +2 -2
- package/dist/components/reauth-dialog.js +6 -8
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/series-chart.d.ts +11 -8
- package/dist/components/settings-fields.d.ts +11 -8
- package/dist/components/share-card.d.ts +10 -7
- package/dist/components/swatch-picker.d.ts +17 -1
- package/dist/components/swatch-picker.js +10 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/text-link.d.ts +10 -7
- package/dist/components/tile-radio.d.ts +20 -0
- package/dist/components/tile-radio.js +25 -8
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +11 -8
- package/dist/components/toggle-group.js +2 -2
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/trigger-aria.d.ts +3 -0
- package/dist/components/trigger-aria.js +3 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/ui.d.ts +8 -5
- package/dist/components/ui.js +5 -11
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +156 -56
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +3 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +68 -287
- package/dist/feedback/feedback-thread.js +8 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-BxeQVzwq.d.ts} +405 -22
- package/dist/{feedback-attachment-NhUm7Zga.d.ts → feedback-attachment-fGAzZPf0.d.ts} +112 -10
- package/dist/feedback.d.ts +66 -2
- package/dist/hooks/use-file-drop.d.ts +11 -8
- package/dist/i18n/defaults.d.ts +11 -8
- package/dist/i18n/defaults.js +3 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +11 -8
- package/dist/i18n/german.js +36 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +7 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +11 -8
- package/dist/i18n/locales/de-CH.d.ts +11 -8
- package/dist/i18n/locales/en.d.ts +11 -8
- package/dist/i18n/locales/en.js +7 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +11 -8
- package/dist/i18n/locales/es.js +37 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +11 -8
- package/dist/i18n/locales/fr.js +36 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +11 -8
- package/dist/i18n/locales/hu.js +38 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +11 -8
- package/dist/i18n/locales/it.js +37 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +11 -8
- package/dist/i18n/locales/zh.js +34 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +11 -8
- package/dist/i18n/review.js +20 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/column-mapping.d.ts +81 -0
- package/dist/lib/column-mapping.js +108 -0
- package/dist/lib/column-mapping.js.map +1 -0
- package/dist/lib/table-text.d.ts +109 -1
- package/dist/lib/table-text.js +122 -1
- package/dist/lib/table-text.js.map +1 -1
- package/dist/rhf/fields.d.ts +89 -11
- package/dist/rhf/fields.js +124 -0
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +11 -8
- package/dist/rhf.d.ts +12 -9
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +10 -7
- package/dist/shell/top-bar-brand.d.ts +11 -8
- package/dist/shell/topbar-action-menu.d.ts +36 -3
- package/dist/shell/topbar-action-menu.js +74 -33
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +10 -7
- package/dist/table-text.d.ts +1 -1
- package/dist/wizard/stepper-nav.d.ts +41 -10
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +11 -8
- package/package.json +1 -1
- package/src/components/amount-input.tsx +20 -1
- package/src/components/autocomplete.tsx +172 -111
- package/src/components/column-mapper.tsx +666 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +614 -332
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +135 -22
- package/src/components/danger-confirm.tsx +91 -12
- package/src/components/date-picker.tsx +423 -10
- package/src/components/entity-combobox.tsx +217 -126
- package/src/components/field-parts.tsx +224 -5
- package/src/components/field-strip.tsx +149 -0
- package/src/components/form-actions.tsx +32 -4
- package/src/components/icon-picker.tsx +23 -4
- package/src/components/money-field.tsx +3 -0
- package/src/components/month-picker.tsx +2 -1
- package/src/components/multi-entity-combobox.tsx +207 -120
- package/src/components/reauth-dialog.tsx +17 -18
- package/src/components/swatch-picker.tsx +29 -5
- package/src/components/tile-radio.tsx +74 -13
- package/src/components/toggle-group.tsx +2 -2
- package/src/components/trigger-aria.ts +5 -0
- package/src/components/ui.tsx +7 -33
- package/src/feedback/feedback-attachment.tsx +308 -61
- package/src/feedback/feedback-dialog.tsx +7 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +47 -4
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +42 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/en.ts +27 -5
- package/src/i18n/locales/es.ts +41 -0
- package/src/i18n/locales/fr.ts +42 -0
- package/src/i18n/locales/hu.ts +37 -0
- package/src/i18n/locales/it.ts +41 -0
- package/src/i18n/locales/zh.ts +32 -0
- package/src/i18n/review.ts +19 -0
- package/src/index.ts +18 -0
- package/src/lib/column-mapping.ts +234 -0
- package/src/lib/table-text.ts +271 -0
- package/src/rhf/fields.tsx +254 -0
- package/src/rhf.ts +2 -1
- package/src/shell/topbar-action-menu.tsx +134 -38
- package/src/wizard/stepper-nav.tsx +35 -1
- package/dist/components/field-anatomy.d.ts +0 -95
- package/dist/components/field-anatomy.js +0 -84
- package/dist/components/field-anatomy.js.map +0 -1
- package/src/components/field-anatomy.tsx +0 -190
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { cn } from "../lib/cn";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type ClipboardEvent as ReactClipboardEvent,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
type RefObject,
|
|
10
|
+
} from "react";
|
|
3
11
|
import { Camera, FileText, Image as ImageIcon, Paperclip, X } from "lucide-react";
|
|
4
|
-
import { Button, Spinner } from "../components/ui";
|
|
12
|
+
import { Button, IconButton, Spinner } from "../components/ui";
|
|
13
|
+
import { DEFAULT_FILE_PICKER_LABELS, formatAccept } from "../components/file-button";
|
|
14
|
+
import { useCommitReason } from "../components/write-lock";
|
|
5
15
|
import { useKitFileLabels, useKitLabels } from "../i18n/kit-labels";
|
|
6
16
|
import type { FeedbackAttachmentLabels } from "./feedback-dialog";
|
|
7
17
|
|
|
@@ -47,6 +57,11 @@ export interface FeedbackAttachmentFieldLabels {
|
|
|
47
57
|
* running, where an uploaded one shows its size — "Uploading…". Optional, like the
|
|
48
58
|
* keys above, so a `UiKitLabels` typed before it still compiles. */
|
|
49
59
|
attachmentUploading?: string;
|
|
60
|
+
/** `refs` mode (0.23.0, keksdose G5a): {@link FeedbackAttachmentErrorInfo.message} for
|
|
61
|
+
* an `"upload"` refusal — "“huge.png” could not be uploaded". The other refusals'
|
|
62
|
+
* sentences are the `filePicker` namespace's, shared with FileButton. Optional, like
|
|
63
|
+
* the keys above. */
|
|
64
|
+
attachmentUploadFailed?: (name: string) => string;
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
export const DEFAULT_FEEDBACK_ATTACHMENT_LABELS: FeedbackAttachmentFieldLabels = {
|
|
@@ -60,6 +75,7 @@ export const DEFAULT_FEEDBACK_ATTACHMENT_LABELS: FeedbackAttachmentFieldLabels =
|
|
|
60
75
|
attachmentLimit: (max) =>
|
|
61
76
|
`Up to ${max} ${max === 1 ? "attachment" : "attachments"} — remove one to add another.`,
|
|
62
77
|
attachmentUploading: "Uploading…",
|
|
78
|
+
attachmentUploadFailed: (name) => `“${name}” could not be uploaded`,
|
|
63
79
|
};
|
|
64
80
|
|
|
65
81
|
/** Why a file was turned away. `"count"` only ever comes from `multiple` (and `refs`)
|
|
@@ -72,6 +88,58 @@ export type FeedbackAttachmentError = "type" | "size" | "count";
|
|
|
72
88
|
* own so a host's exhaustive switch over the File modes' reasons stays exhaustive. */
|
|
73
89
|
export type FeedbackAttachmentRefsError = FeedbackAttachmentError | "upload";
|
|
74
90
|
|
|
91
|
+
/**
|
|
92
|
+
* What `onError` knows about a refusal beyond its kind (0.23.0, keksdose G5a): the file
|
|
93
|
+
* it was about, the limit that refused it, and a sentence that says both.
|
|
94
|
+
*
|
|
95
|
+
* keksdose's support chat toasted "File too large" with no name — the field's `onError`
|
|
96
|
+
* carried a kind and nothing else, so a refusal could not say WHICH of three pasted
|
|
97
|
+
* screenshots went, nor what the ceiling was, and its own `AttachmentPicker` (a
|
|
98
|
+
* FileButton, whose `onReject` hands over `{ file, message }`) said more than the kit's
|
|
99
|
+
* field would have. This is that, for every mode.
|
|
100
|
+
*
|
|
101
|
+
* It arrives as an EXTRA argument, after the ones `onError` already had, so a handler
|
|
102
|
+
* written for 0.22 — `(kind) => …`, or `refs` mode's `(kind, error) => …` — compiles
|
|
103
|
+
* and behaves as before:
|
|
104
|
+
*
|
|
105
|
+
* - single and `multiple`: `onError(kind, info)`;
|
|
106
|
+
* - `refs`: `onError(kind, error, info)` — the second place is `onUpload`'s rejection
|
|
107
|
+
* since 0.22 (`undefined` for the other kinds, as it always was), so the info comes
|
|
108
|
+
* third rather than changing what a 0.22 handler reads there. It also carries that
|
|
109
|
+
* `error`, so one helper `(kind, info) => …` serves all three modes.
|
|
110
|
+
*
|
|
111
|
+
* An object rather than more positional arguments because what a host reaches for
|
|
112
|
+
* differs by kind — the bytes for `"size"`, `max` for `"count"`, the server's answer for
|
|
113
|
+
* `"upload"` — and a field added later must not shift anyone's arguments.
|
|
114
|
+
*/
|
|
115
|
+
export interface FeedbackAttachmentErrorInfo {
|
|
116
|
+
/** The file refused — under the name the field would have given it (a pasted image
|
|
117
|
+
* is already "pasted.png"). For `"count"`, the first of the surplus; for
|
|
118
|
+
* `"upload"`, the file whose upload failed. */
|
|
119
|
+
file: File;
|
|
120
|
+
/** `"count"`: every file of the surplus (several can arrive in one pick). Otherwise
|
|
121
|
+
* `[file]`. */
|
|
122
|
+
files: File[];
|
|
123
|
+
/**
|
|
124
|
+
* One translated sentence naming the file and the limit — the ones FileButton's
|
|
125
|
+
* `onReject` hands over, from the same `filePicker` namespace: "“huge.png” is larger
|
|
126
|
+
* than 10 MB", "Only image/png, image/jpeg files", "“c.png” was not added: at most 5
|
|
127
|
+
* files" (several at once: "3 files were not added"). `"upload"`: the field's own
|
|
128
|
+
* `attachmentUploadFailed`, "“huge.png” could not be uploaded" — a host with a better
|
|
129
|
+
* sentence from the server's answer uses `error`. Ready for a toast as it is.
|
|
130
|
+
*/
|
|
131
|
+
message: string;
|
|
132
|
+
/** The field's `maxBytes` — the ceiling a `"size"` refusal went over, in bytes. */
|
|
133
|
+
maxBytes: number;
|
|
134
|
+
/** The field's `accept` — the types a `"type"` refusal was not among. */
|
|
135
|
+
accept: string[];
|
|
136
|
+
/** The field's `max` — the ceiling a `"count"` refusal hit. Absent in single mode,
|
|
137
|
+
* which holds one file by definition. */
|
|
138
|
+
max?: number;
|
|
139
|
+
/** `"upload"` only: what `onUpload` rejected with — `onError`'s second argument. */
|
|
140
|
+
error?: unknown;
|
|
141
|
+
}
|
|
142
|
+
|
|
75
143
|
/**
|
|
76
144
|
* One uploaded attachment in `refs` mode: what the host's upload answered with. `key`
|
|
77
145
|
* identifies it (a storage key, an upload id) and is what a remove goes by; `name` is
|
|
@@ -88,9 +156,25 @@ export interface FeedbackAttachmentRef {
|
|
|
88
156
|
interface FeedbackAttachmentFieldBaseProps {
|
|
89
157
|
/** Prop > `<UiKitProvider labels={{ feedbackAttachment }}>` > English. Optional
|
|
90
158
|
* since 0.7.0; `attachment` (the heading) is only ever read from here. */
|
|
91
|
-
labels?: Partial<FeedbackAttachmentLabels> &
|
|
159
|
+
labels?: Partial<FeedbackAttachmentLabels> &
|
|
160
|
+
Pick<Partial<FeedbackAttachmentFieldLabels>, "attachmentUploading" | "attachmentUploadFailed">;
|
|
92
161
|
accept?: string[];
|
|
93
162
|
maxBytes?: number;
|
|
163
|
+
/**
|
|
164
|
+
* Take no files for now (0.23.0, keksdose G5b): the add and capture buttons, the
|
|
165
|
+
* hidden file input and every chip's remove button are natively disabled, and a paste
|
|
166
|
+
* is let through untouched. The chips stay as they are — what is about to be sent
|
|
167
|
+
* stays readable.
|
|
168
|
+
*
|
|
169
|
+
* For the moment a send is in flight. keksdose's support picker is `disabled` while
|
|
170
|
+
* the message goes out, because the message carries the attachments AS THEY WERE at
|
|
171
|
+
* the press: a chip removed mid-send has gone with it all the same, and one added
|
|
172
|
+
* mid-send looks as if it had. The field has no drop target; the paste is its drop.
|
|
173
|
+
*
|
|
174
|
+
* This is a pause, not a lock that explains itself — for that, `refs` mode (the mode
|
|
175
|
+
* that commits) takes `commit` and `disabledReason`.
|
|
176
|
+
*/
|
|
177
|
+
disabled?: boolean;
|
|
94
178
|
/** Snapshot the app view behind this and return it as a File. A "Capture
|
|
95
179
|
* screenshot" button appears only when it is given. */
|
|
96
180
|
onCaptureScreenshot?: () => Promise<File | null>;
|
|
@@ -111,11 +195,16 @@ export interface FeedbackAttachmentFieldSingleProps extends FeedbackAttachmentFi
|
|
|
111
195
|
refs?: false;
|
|
112
196
|
value: File | null;
|
|
113
197
|
onChange: (file: File | null) => void;
|
|
114
|
-
|
|
198
|
+
/** A file was refused. `info` (0.23.0) names it and the limit — see
|
|
199
|
+
* {@link FeedbackAttachmentErrorInfo}. */
|
|
200
|
+
onError?: (kind: "type" | "size", info: FeedbackAttachmentErrorInfo) => void;
|
|
115
201
|
// Multiple-mode props, refused here: without `multiple` they would be silently ignored.
|
|
116
202
|
max?: never;
|
|
117
203
|
screenshot?: never;
|
|
118
204
|
onScreenshotChange?: never;
|
|
205
|
+
// `refs` mode's lock: a staged File is not a commit (see the refs props).
|
|
206
|
+
commit?: never;
|
|
207
|
+
disabledReason?: never;
|
|
119
208
|
}
|
|
120
209
|
|
|
121
210
|
/**
|
|
@@ -131,8 +220,10 @@ export interface FeedbackAttachmentFieldMultipleProps extends FeedbackAttachment
|
|
|
131
220
|
/** How many files `value` may hold. Default {@link DEFAULT_MAX_ATTACHMENTS} (5). The
|
|
132
221
|
* add buttons and the paste hint are offered only while there is room. */
|
|
133
222
|
max?: number;
|
|
134
|
-
/** Per file: a rejected one is reported and the others are still added.
|
|
135
|
-
|
|
223
|
+
/** Per file: a rejected one is reported and the others are still added. `"count"` is
|
|
224
|
+
* once per pick, with the whole surplus in `info.files`. `info` (0.23.0): see
|
|
225
|
+
* {@link FeedbackAttachmentErrorInfo}. */
|
|
226
|
+
onError?: (kind: FeedbackAttachmentError, info: FeedbackAttachmentErrorInfo) => void;
|
|
136
227
|
/**
|
|
137
228
|
* A separate slot for THE screenshot, outside `value` and `max`. Given
|
|
138
229
|
* `onScreenshotChange`, what `onCaptureScreenshot` returns lands here instead of in
|
|
@@ -143,6 +234,9 @@ export interface FeedbackAttachmentFieldMultipleProps extends FeedbackAttachment
|
|
|
143
234
|
*/
|
|
144
235
|
screenshot?: File | null;
|
|
145
236
|
onScreenshotChange?: (file: File | null) => void;
|
|
237
|
+
// `refs` mode's lock: a staged File is not a commit (see the refs props).
|
|
238
|
+
commit?: never;
|
|
239
|
+
disabledReason?: never;
|
|
146
240
|
}
|
|
147
241
|
|
|
148
242
|
/**
|
|
@@ -180,8 +274,28 @@ export interface FeedbackAttachmentFieldRefsProps extends FeedbackAttachmentFiel
|
|
|
180
274
|
/** How many refs `value` may hold, uploads in flight included. Default
|
|
181
275
|
* {@link DEFAULT_MAX_ATTACHMENTS} (5). */
|
|
182
276
|
max?: number;
|
|
183
|
-
/** Per file. `"upload"` carries what `onUpload` rejected with
|
|
184
|
-
|
|
277
|
+
/** Per file. `"upload"` carries what `onUpload` rejected with as `error` (`undefined`
|
|
278
|
+
* for the other kinds). `info` (0.23.0) comes third so a 0.22 handler's `error` is
|
|
279
|
+
* still the second argument — see {@link FeedbackAttachmentErrorInfo}. */
|
|
280
|
+
onError?: (kind: FeedbackAttachmentRefsError, error: unknown, info: FeedbackAttachmentErrorInfo) => void;
|
|
281
|
+
/**
|
|
282
|
+
* This field COMMITS (0.23.0): each file is uploaded the moment it is chosen, and a
|
|
283
|
+
* remove may delete the upload (`onRemove`) — a save, unlike the File modes, which
|
|
284
|
+
* only stage a file for a form sent later (and so stay open under a lock, like every
|
|
285
|
+
* field). Under a locked {@link WriteLockProvider} it takes the `disabledReason` path
|
|
286
|
+
* with the lock's reason, which wins over one of its own. No provider, or an unlocked
|
|
287
|
+
* one: no effect.
|
|
288
|
+
*/
|
|
289
|
+
commit?: boolean;
|
|
290
|
+
/**
|
|
291
|
+
* Why no file can be added or removed — the write lock's sentence, a quota, a thread
|
|
292
|
+
* closed to replies. Unlike `disabled`, it SAYS so, the kit's commit-control way: the
|
|
293
|
+
* buttons (add, capture, every chip's remove) stay focusable, `aria-disabled`, with
|
|
294
|
+
* the reason in the kit Tooltip and their description; no pick, paste or remove gets
|
|
295
|
+
* through; and the reason stands where the paste hint was, for a phone that never
|
|
296
|
+
* hovers. The chips stay readable.
|
|
297
|
+
*/
|
|
298
|
+
disabledReason?: ReactNode;
|
|
185
299
|
screenshot?: never;
|
|
186
300
|
onScreenshotChange?: never;
|
|
187
301
|
}
|
|
@@ -239,6 +353,7 @@ function SingleField({
|
|
|
239
353
|
onCaptureScreenshot,
|
|
240
354
|
documentPaste = false,
|
|
241
355
|
pasteFrom,
|
|
356
|
+
disabled = false,
|
|
242
357
|
className,
|
|
243
358
|
}: FeedbackAttachmentFieldSingleProps) {
|
|
244
359
|
// The object URL is keyed to the file it was made for, so a stale one (from the
|
|
@@ -249,6 +364,7 @@ function SingleField({
|
|
|
249
364
|
// `file.size` from `<UiKitProvider labels>`, formatted in its locale — see FileDropzone.
|
|
250
365
|
const fileText = useKitFileLabels();
|
|
251
366
|
const text = useKitLabels("feedbackAttachment", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);
|
|
367
|
+
const errorInfo = useErrorInfo(accept, maxBytes, undefined, text);
|
|
252
368
|
|
|
253
369
|
// Object-URL preview lifecycle (create on change, revoke on cleanup).
|
|
254
370
|
useEffect(() => {
|
|
@@ -266,7 +382,7 @@ function SingleField({
|
|
|
266
382
|
if (!file) return;
|
|
267
383
|
const problem = rejection(file, accept, maxBytes);
|
|
268
384
|
if (problem) {
|
|
269
|
-
onError?.(problem);
|
|
385
|
+
onError?.(problem, errorInfo(problem, [file]));
|
|
270
386
|
return;
|
|
271
387
|
}
|
|
272
388
|
onChange(file);
|
|
@@ -282,6 +398,7 @@ function SingleField({
|
|
|
282
398
|
},
|
|
283
399
|
documentPaste,
|
|
284
400
|
pasteFrom,
|
|
401
|
+
disabled,
|
|
285
402
|
);
|
|
286
403
|
|
|
287
404
|
return (
|
|
@@ -315,23 +432,26 @@ function SingleField({
|
|
|
315
432
|
{fileText.size(value.size)}
|
|
316
433
|
</div>
|
|
317
434
|
</div>
|
|
318
|
-
<
|
|
319
|
-
type="button"
|
|
320
|
-
onClick={() => onChange(null)}
|
|
321
|
-
aria-label={text.attachmentRemove}
|
|
322
|
-
className="rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
|
|
323
|
-
>
|
|
324
|
-
<X className="size-4" />
|
|
325
|
-
</button>
|
|
435
|
+
<RemoveButton label={text.attachmentRemove} onClick={() => onChange(null)} disabled={disabled} />
|
|
326
436
|
</div>
|
|
327
437
|
) : (
|
|
328
438
|
<div className="space-y-1.5">
|
|
329
439
|
<div className="flex flex-wrap gap-2">
|
|
330
|
-
<Button
|
|
440
|
+
<Button
|
|
441
|
+
type="button"
|
|
442
|
+
variant="secondary"
|
|
443
|
+
disabled={disabled}
|
|
444
|
+
onClick={() => fileInputRef.current?.click()}
|
|
445
|
+
>
|
|
331
446
|
<Paperclip className="size-4" /> {text.attachmentAdd}
|
|
332
447
|
</Button>
|
|
333
448
|
{onCaptureScreenshot && (
|
|
334
|
-
<CaptureButton
|
|
449
|
+
<CaptureButton
|
|
450
|
+
capture={onCaptureScreenshot}
|
|
451
|
+
onFile={pick}
|
|
452
|
+
label={text.attachmentCapture}
|
|
453
|
+
disabled={disabled}
|
|
454
|
+
/>
|
|
335
455
|
)}
|
|
336
456
|
</div>
|
|
337
457
|
{/* Said out loud, because a gesture with no affordance is a gesture
|
|
@@ -346,6 +466,7 @@ function SingleField({
|
|
|
346
466
|
type="file"
|
|
347
467
|
accept={accept.join(",")}
|
|
348
468
|
className="sr-only"
|
|
469
|
+
disabled={disabled}
|
|
349
470
|
onChange={(e) => {
|
|
350
471
|
pick(e.target.files?.[0]);
|
|
351
472
|
e.target.value = "";
|
|
@@ -368,6 +489,7 @@ function MultipleField({
|
|
|
368
489
|
onCaptureScreenshot,
|
|
369
490
|
documentPaste = false,
|
|
370
491
|
pasteFrom,
|
|
492
|
+
disabled = false,
|
|
371
493
|
className,
|
|
372
494
|
}: FeedbackAttachmentFieldMultipleProps) {
|
|
373
495
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
@@ -377,6 +499,7 @@ function MultipleField({
|
|
|
377
499
|
const removeLabel = text.attachmentRemoveFile ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentRemoveFile!;
|
|
378
500
|
const limitLabel = text.attachmentLimit ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentLimit!;
|
|
379
501
|
const screenshotLabel = text.attachmentScreenshot ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentScreenshot!;
|
|
502
|
+
const errorInfo = useErrorInfo(accept, maxBytes, max, text);
|
|
380
503
|
|
|
381
504
|
// What an add or a remove builds on: the `value` prop, plus whatever this field has
|
|
382
505
|
// already handed to `onChange` since the parent last rendered. The field is
|
|
@@ -405,27 +528,28 @@ function MultipleField({
|
|
|
405
528
|
const accepted: File[] = [];
|
|
406
529
|
const taken = new Set([...current, ...(screenshot ? [screenshot] : [])].map((f) => f.name));
|
|
407
530
|
for (const raw of incoming) {
|
|
408
|
-
const problem = rejection(raw, accept, maxBytes);
|
|
409
|
-
if (problem) {
|
|
410
|
-
onError?.(problem);
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
531
|
// Two pastes are two files called "pasted.png", and a backend that stores by
|
|
414
532
|
// name keeps one of them — the very loss this mode exists to end. A picked
|
|
415
|
-
// file keeps its own name: it is the user's, and they may look for it.
|
|
533
|
+
// file keeps its own name: it is the user's, and they may look for it. Named
|
|
534
|
+
// before the check, so a refusal names it as the single field's does.
|
|
416
535
|
const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;
|
|
536
|
+
const problem = rejection(file, accept, maxBytes);
|
|
537
|
+
if (problem) {
|
|
538
|
+
onError?.(problem, errorInfo(problem, [file]));
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
417
541
|
taken.add(file.name);
|
|
418
542
|
// The same File object twice is one file chosen twice, not two.
|
|
419
543
|
if (!current.includes(file)) accepted.push(file);
|
|
420
544
|
}
|
|
421
545
|
if (accepted.length === 0) return;
|
|
422
546
|
const space = Math.max(0, max - current.length);
|
|
423
|
-
if (accepted.length > space) onError?.("count");
|
|
547
|
+
if (accepted.length > space) onError?.("count", errorInfo("count", accepted.slice(space)));
|
|
424
548
|
const kept = accepted.slice(0, space);
|
|
425
549
|
if (kept.length > 0) commit([...current, ...kept]);
|
|
426
550
|
};
|
|
427
551
|
|
|
428
|
-
const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom);
|
|
552
|
+
const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom, disabled);
|
|
429
553
|
|
|
430
554
|
const capture = (file: File) => {
|
|
431
555
|
if (!hasSlot) {
|
|
@@ -433,7 +557,7 @@ function MultipleField({
|
|
|
433
557
|
return;
|
|
434
558
|
}
|
|
435
559
|
const problem = rejection(file, accept, maxBytes);
|
|
436
|
-
if (problem) onError?.(problem);
|
|
560
|
+
if (problem) onError?.(problem, errorInfo(problem, [file]));
|
|
437
561
|
else onScreenshotChange(file);
|
|
438
562
|
};
|
|
439
563
|
|
|
@@ -489,18 +613,14 @@ function MultipleField({
|
|
|
489
613
|
{chip.screenshot ? `${chip.file.name} · ${fileText.size(chip.file.size)}` : fileText.size(chip.file.size)}
|
|
490
614
|
</div>
|
|
491
615
|
</div>
|
|
492
|
-
<
|
|
493
|
-
|
|
494
|
-
|
|
616
|
+
<RemoveButton
|
|
617
|
+
label={removeLabel(chip.name)}
|
|
618
|
+
disabled={disabled}
|
|
495
619
|
onClick={() => {
|
|
496
620
|
pendingFocus.current = index;
|
|
497
621
|
chip.remove();
|
|
498
622
|
}}
|
|
499
|
-
|
|
500
|
-
className="shrink-0 rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
|
|
501
|
-
>
|
|
502
|
-
<X className="size-4" />
|
|
503
|
-
</button>
|
|
623
|
+
/>
|
|
504
624
|
</li>
|
|
505
625
|
))}
|
|
506
626
|
</ul>
|
|
@@ -512,6 +632,7 @@ function MultipleField({
|
|
|
512
632
|
type="button"
|
|
513
633
|
variant="secondary"
|
|
514
634
|
data-attachment-action=""
|
|
635
|
+
disabled={disabled}
|
|
515
636
|
onClick={() => fileInputRef.current?.click()}
|
|
516
637
|
>
|
|
517
638
|
<Paperclip className="size-4" /> {text.attachmentAdd}
|
|
@@ -522,6 +643,7 @@ function MultipleField({
|
|
|
522
643
|
capture={onCaptureScreenshot}
|
|
523
644
|
onFile={capture}
|
|
524
645
|
label={text.attachmentCapture}
|
|
646
|
+
disabled={disabled}
|
|
525
647
|
data-attachment-action=""
|
|
526
648
|
/>
|
|
527
649
|
)}
|
|
@@ -540,6 +662,7 @@ function MultipleField({
|
|
|
540
662
|
// the way in, and a second, unlabelled "Choose files" stop is noise.
|
|
541
663
|
tabIndex={-1}
|
|
542
664
|
aria-hidden
|
|
665
|
+
disabled={disabled}
|
|
543
666
|
onChange={(e) => {
|
|
544
667
|
add(Array.from(e.target.files ?? []));
|
|
545
668
|
e.target.value = "";
|
|
@@ -571,6 +694,9 @@ function RefsField({
|
|
|
571
694
|
onCaptureScreenshot,
|
|
572
695
|
documentPaste = false,
|
|
573
696
|
pasteFrom,
|
|
697
|
+
disabled = false,
|
|
698
|
+
commit: commits,
|
|
699
|
+
disabledReason: ownDisabledReason,
|
|
574
700
|
className,
|
|
575
701
|
}: FeedbackAttachmentFieldRefsProps) {
|
|
576
702
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
@@ -580,6 +706,14 @@ function RefsField({
|
|
|
580
706
|
const removeLabel = text.attachmentRemoveFile ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentRemoveFile!;
|
|
581
707
|
const limitLabel = text.attachmentLimit ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentLimit!;
|
|
582
708
|
const uploadingLabel = text.attachmentUploading ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentUploading!;
|
|
709
|
+
const errorInfo = useErrorInfo(accept, maxBytes, max, text);
|
|
710
|
+
// The lock's reason (under `commit`) over the field's own; with either, the controls
|
|
711
|
+
// take the focusable `aria-disabled` path and say why — Button's and IconButton's
|
|
712
|
+
// `disabledReason`, handed down as is.
|
|
713
|
+
const disabledReason = useCommitReason(commits, ownDisabledReason);
|
|
714
|
+
const locked = hasReason(disabledReason);
|
|
715
|
+
const reason = locked ? disabledReason : undefined;
|
|
716
|
+
const inert = disabled || locked;
|
|
583
717
|
|
|
584
718
|
// As in `multiple` mode: `value` plus whatever has been handed to `onChange` since
|
|
585
719
|
// the parent last rendered — two uploads landing in one tick must both survive.
|
|
@@ -637,7 +771,7 @@ function RefsField({
|
|
|
637
771
|
},
|
|
638
772
|
(error: unknown) => {
|
|
639
773
|
settle();
|
|
640
|
-
if (mounted.current) onError?.("upload", error);
|
|
774
|
+
if (mounted.current) onError?.("upload", error, errorInfo("upload", [file], error));
|
|
641
775
|
},
|
|
642
776
|
);
|
|
643
777
|
};
|
|
@@ -645,28 +779,29 @@ function RefsField({
|
|
|
645
779
|
const add = (incoming: File[], pasted = false) => {
|
|
646
780
|
let space = Math.max(0, max - latest.current.length - inFlight.current.length);
|
|
647
781
|
const taken = new Set([...latest.current.map((r) => r.name), ...inFlight.current.map((u) => u.file.name)]);
|
|
648
|
-
|
|
782
|
+
const dropped: File[] = [];
|
|
649
783
|
for (const raw of incoming) {
|
|
650
|
-
|
|
784
|
+
// Pasted images are all "image.png" to the clipboard; as in `multiple` mode,
|
|
785
|
+
// each gets a name of its own before it is checked and uploaded under it.
|
|
786
|
+
const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;
|
|
787
|
+
const problem = rejection(file, accept, maxBytes);
|
|
651
788
|
if (problem) {
|
|
652
|
-
|
|
789
|
+
// `undefined` in `error`'s place, as in 0.22: only an upload has one.
|
|
790
|
+
onError?.(problem, undefined, errorInfo(problem, [file]));
|
|
653
791
|
continue;
|
|
654
792
|
}
|
|
793
|
+
taken.add(file.name);
|
|
655
794
|
if (space === 0) {
|
|
656
|
-
dropped
|
|
795
|
+
dropped.push(file);
|
|
657
796
|
continue;
|
|
658
797
|
}
|
|
659
|
-
// Pasted images are all "image.png" to the clipboard; as in `multiple` mode,
|
|
660
|
-
// each gets a name of its own before it is uploaded under it.
|
|
661
|
-
const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;
|
|
662
|
-
taken.add(file.name);
|
|
663
798
|
space -= 1;
|
|
664
799
|
start(file);
|
|
665
800
|
}
|
|
666
|
-
if (dropped) onError?.("count");
|
|
801
|
+
if (dropped.length > 0) onError?.("count", undefined, errorInfo("count", dropped));
|
|
667
802
|
};
|
|
668
803
|
|
|
669
|
-
const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom);
|
|
804
|
+
const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom, inert);
|
|
670
805
|
|
|
671
806
|
// Focus after a remove, as in `multiple` mode: the next chip, else the first action.
|
|
672
807
|
const pendingFocus = useRef<number | null>(null);
|
|
@@ -683,6 +818,7 @@ function RefsField({
|
|
|
683
818
|
}, [value]);
|
|
684
819
|
|
|
685
820
|
const remove = (key: string, index: number) => {
|
|
821
|
+
if (inert) return;
|
|
686
822
|
pendingFocus.current = index;
|
|
687
823
|
commit(latest.current.filter((r) => r.key !== key));
|
|
688
824
|
onRemove?.(key);
|
|
@@ -707,15 +843,12 @@ function RefsField({
|
|
|
707
843
|
<div className="truncate text-xs text-[var(--text-muted)]">{fileText.size(ref.size)}</div>
|
|
708
844
|
)}
|
|
709
845
|
</div>
|
|
710
|
-
<
|
|
711
|
-
|
|
712
|
-
data-attachment-remove=""
|
|
846
|
+
<RemoveButton
|
|
847
|
+
label={removeLabel(ref.name)}
|
|
713
848
|
onClick={() => remove(ref.key, index)}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
<X className="size-4" />
|
|
718
|
-
</button>
|
|
849
|
+
disabled={disabled}
|
|
850
|
+
reason={reason}
|
|
851
|
+
/>
|
|
719
852
|
</li>
|
|
720
853
|
))}
|
|
721
854
|
{uploads.map((upload) => (
|
|
@@ -742,6 +875,8 @@ function RefsField({
|
|
|
742
875
|
type="button"
|
|
743
876
|
variant="secondary"
|
|
744
877
|
data-attachment-action=""
|
|
878
|
+
disabled={disabled}
|
|
879
|
+
disabledReason={reason}
|
|
745
880
|
onClick={() => fileInputRef.current?.click()}
|
|
746
881
|
>
|
|
747
882
|
<Paperclip className="size-4" /> {text.attachmentAdd}
|
|
@@ -751,12 +886,22 @@ function RefsField({
|
|
|
751
886
|
capture={onCaptureScreenshot}
|
|
752
887
|
onFile={(file) => add([file])}
|
|
753
888
|
label={text.attachmentCapture}
|
|
889
|
+
disabled={disabled}
|
|
890
|
+
disabledReason={reason}
|
|
754
891
|
data-attachment-action=""
|
|
755
892
|
/>
|
|
756
893
|
)}
|
|
757
894
|
</div>
|
|
758
895
|
)}
|
|
759
|
-
|
|
896
|
+
{locked ? (
|
|
897
|
+
// Where the paste hint was: a phone never hovers, so the Tooltip alone would
|
|
898
|
+
// leave a touch user with buttons that do nothing and no word why.
|
|
899
|
+
<div data-attachment-reason="" className="text-xs text-[var(--text-muted)]">
|
|
900
|
+
{disabledReason}
|
|
901
|
+
</div>
|
|
902
|
+
) : (
|
|
903
|
+
<p className="text-xs text-[var(--text-muted)]">{room > 0 ? text.attachmentPaste : limitLabel(max)}</p>
|
|
904
|
+
)}
|
|
760
905
|
<input
|
|
761
906
|
ref={fileInputRef}
|
|
762
907
|
type="file"
|
|
@@ -765,6 +910,7 @@ function RefsField({
|
|
|
765
910
|
className="sr-only"
|
|
766
911
|
tabIndex={-1}
|
|
767
912
|
aria-hidden
|
|
913
|
+
disabled={inert}
|
|
768
914
|
onChange={(e) => {
|
|
769
915
|
add(Array.from(e.target.files ?? []));
|
|
770
916
|
e.target.value = "";
|
|
@@ -797,11 +943,15 @@ function CaptureButton({
|
|
|
797
943
|
capture,
|
|
798
944
|
onFile,
|
|
799
945
|
label,
|
|
946
|
+
disabled = false,
|
|
947
|
+
disabledReason,
|
|
800
948
|
...rest
|
|
801
949
|
}: {
|
|
802
950
|
capture: () => Promise<File | null>;
|
|
803
951
|
onFile: (file: File) => void;
|
|
804
952
|
label: string;
|
|
953
|
+
disabled?: boolean;
|
|
954
|
+
disabledReason?: ReactNode;
|
|
805
955
|
"data-attachment-action"?: string;
|
|
806
956
|
}) {
|
|
807
957
|
const [capturing, setCapturing] = useState(false);
|
|
@@ -809,10 +959,11 @@ function CaptureButton({
|
|
|
809
959
|
<Button
|
|
810
960
|
type="button"
|
|
811
961
|
variant="secondary"
|
|
812
|
-
disabled={capturing}
|
|
962
|
+
disabled={capturing || disabled}
|
|
963
|
+
disabledReason={disabledReason}
|
|
813
964
|
{...rest}
|
|
814
965
|
onClick={() => {
|
|
815
|
-
if (capturing) return;
|
|
966
|
+
if (capturing || disabled) return;
|
|
816
967
|
setCapturing(true);
|
|
817
968
|
void capture()
|
|
818
969
|
.then((file) => {
|
|
@@ -826,6 +977,93 @@ function CaptureButton({
|
|
|
826
977
|
);
|
|
827
978
|
}
|
|
828
979
|
|
|
980
|
+
/**
|
|
981
|
+
* A chip's (or the single file's) remove button: the kit's IconButton at the 28px the
|
|
982
|
+
* hand-built one had, so `disabled` (0.23.0) dims and disables it the kit way and a
|
|
983
|
+
* `reason` takes IconButton's `disabledReason` path — focusable, `aria-disabled`, the
|
|
984
|
+
* reason in the Tooltip — rather than a third copy of either here.
|
|
985
|
+
*/
|
|
986
|
+
function RemoveButton({
|
|
987
|
+
label,
|
|
988
|
+
onClick,
|
|
989
|
+
disabled,
|
|
990
|
+
reason,
|
|
991
|
+
}: {
|
|
992
|
+
label: string;
|
|
993
|
+
onClick: () => void;
|
|
994
|
+
disabled: boolean;
|
|
995
|
+
reason?: ReactNode;
|
|
996
|
+
}) {
|
|
997
|
+
return (
|
|
998
|
+
<IconButton
|
|
999
|
+
type="button"
|
|
1000
|
+
size="xs"
|
|
1001
|
+
tone="muted"
|
|
1002
|
+
data-attachment-remove=""
|
|
1003
|
+
aria-label={label}
|
|
1004
|
+
disabled={disabled}
|
|
1005
|
+
disabledReason={reason}
|
|
1006
|
+
onClick={onClick}
|
|
1007
|
+
className="shrink-0"
|
|
1008
|
+
>
|
|
1009
|
+
<X aria-hidden />
|
|
1010
|
+
</IconButton>
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/** A reason worth showing: `null`, `false` and `""` are a caller's "no reason". */
|
|
1015
|
+
function hasReason(reason: ReactNode): boolean {
|
|
1016
|
+
return reason !== undefined && reason !== null && reason !== false && reason !== "";
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* The {@link FeedbackAttachmentErrorInfo} builder of one field: its limits, and the
|
|
1021
|
+
* sentence for each refusal. The sentences are the `filePicker` namespace's — the ones
|
|
1022
|
+
* FileButton and FileDropzone say — so an app that translated those has these too, and
|
|
1023
|
+
* a refusal reads the same whichever of the kit's pickers made it. Only the upload
|
|
1024
|
+
* failure, which no other picker has, is this field's own (`attachmentUploadFailed`).
|
|
1025
|
+
*/
|
|
1026
|
+
function useErrorInfo(
|
|
1027
|
+
accept: string[],
|
|
1028
|
+
maxBytes: number,
|
|
1029
|
+
max: number | undefined,
|
|
1030
|
+
text: FeedbackAttachmentFieldLabels,
|
|
1031
|
+
) {
|
|
1032
|
+
const picker = useKitLabels("filePicker", DEFAULT_FILE_PICKER_LABELS);
|
|
1033
|
+
const fileText = useKitFileLabels();
|
|
1034
|
+
const uploadFailed = text.attachmentUploadFailed ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentUploadFailed!;
|
|
1035
|
+
return (kind: FeedbackAttachmentRefsError, files: File[], error?: unknown): FeedbackAttachmentErrorInfo => {
|
|
1036
|
+
const file = files[0];
|
|
1037
|
+
let message: string;
|
|
1038
|
+
if (kind === "type") {
|
|
1039
|
+
// FileButton's rule: name what IS accepted ("Only image/png, image/jpeg files"),
|
|
1040
|
+
// unless the app translated `rejectedType` but not yet `rejectedTypeOnly` — its
|
|
1041
|
+
// own sentence beats an English one.
|
|
1042
|
+
const acceptText = formatAccept(accept.join(","));
|
|
1043
|
+
const useOnly =
|
|
1044
|
+
acceptText !== "" &&
|
|
1045
|
+
(picker.rejectedTypeOnly !== DEFAULT_FILE_PICKER_LABELS.rejectedTypeOnly ||
|
|
1046
|
+
picker.rejectedType === DEFAULT_FILE_PICKER_LABELS.rejectedType);
|
|
1047
|
+
message = useOnly ? picker.rejectedTypeOnly(acceptText, file.name) : picker.rejectedType(file.name);
|
|
1048
|
+
} else if (kind === "size") {
|
|
1049
|
+
message = picker.rejectedSize(file.name, fileText.size(maxBytes));
|
|
1050
|
+
} else if (kind === "count") {
|
|
1051
|
+
message = files.length === 1 ? picker.rejectedCount(file.name, max ?? 1) : picker.rejectedMany(files.length);
|
|
1052
|
+
} else {
|
|
1053
|
+
message = uploadFailed(file.name);
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
file,
|
|
1057
|
+
files,
|
|
1058
|
+
message,
|
|
1059
|
+
maxBytes,
|
|
1060
|
+
accept,
|
|
1061
|
+
...(max !== undefined ? { max } : null),
|
|
1062
|
+
...(kind === "upload" ? { error } : null),
|
|
1063
|
+
};
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
829
1067
|
/** A chip's picture: the image itself, or a file glyph for anything that is not one.
|
|
830
1068
|
* Decorative (`alt=""`) — the name stands beside it. Its own component so the object
|
|
831
1069
|
* URL lives exactly as long as the chip: removing the chip unmounts it and revokes. */
|
|
@@ -903,10 +1141,15 @@ function usePaste(
|
|
|
903
1141
|
onImages: (images: File[]) => void,
|
|
904
1142
|
documentPaste: boolean,
|
|
905
1143
|
pasteFrom: RefObject<HTMLElement | null> | undefined,
|
|
1144
|
+
/** `disabled` / locked (0.23.0): the paste goes where it was going, untouched — not
|
|
1145
|
+
* swallowed, so a text half still lands in the box it was made in. */
|
|
1146
|
+
off: boolean,
|
|
906
1147
|
) {
|
|
907
|
-
|
|
1148
|
+
// `null` while off, so the listeners below leave the event alone entirely.
|
|
1149
|
+
const handler = off ? null : onImages;
|
|
1150
|
+
const latest = useRef(handler);
|
|
908
1151
|
useEffect(() => {
|
|
909
|
-
latest.current =
|
|
1152
|
+
latest.current = handler;
|
|
910
1153
|
});
|
|
911
1154
|
|
|
912
1155
|
// The element listened on, where it is not this field's own subtree: the
|
|
@@ -917,6 +1160,7 @@ function usePaste(
|
|
|
917
1160
|
const target: EventTarget | null = documentPaste ? document : (pasteFrom?.current ?? null);
|
|
918
1161
|
if (!target) return;
|
|
919
1162
|
const onPaste = (event: Event) => {
|
|
1163
|
+
if (!latest.current) return;
|
|
920
1164
|
const clipboard = (event as ClipboardEvent).clipboardData;
|
|
921
1165
|
takeImages(clipboard, () => event.preventDefault(), latest.current);
|
|
922
1166
|
};
|
|
@@ -927,7 +1171,10 @@ function usePaste(
|
|
|
927
1171
|
const listensElsewhere = documentPaste || pasteFrom !== undefined;
|
|
928
1172
|
return listensElsewhere
|
|
929
1173
|
? undefined
|
|
930
|
-
: (event: ReactClipboardEvent) =>
|
|
1174
|
+
: (event: ReactClipboardEvent) => {
|
|
1175
|
+
if (!latest.current) return;
|
|
1176
|
+
takeImages(event.clipboardData, () => event.preventDefault(), latest.current);
|
|
1177
|
+
};
|
|
931
1178
|
}
|
|
932
1179
|
|
|
933
1180
|
function takeImages(clipboard: DataTransfer | null, stop: () => void, onImages: (images: File[]) => void) {
|