@eifi1/ui-kit 0.20.1 → 0.22.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/dist/chart.d.ts +10 -8
- package/dist/components/account-chips.d.ts +10 -3
- package/dist/components/account-chips.js +5 -2
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings.d.ts +33 -2
- package/dist/components/account-settings.js +43 -21
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +27 -8
- package/dist/components/amount-input.js +15 -28
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +8 -0
- package/dist/components/autocomplete.js +9 -4
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +10 -8
- package/dist/components/calculator.d.ts +10 -8
- package/dist/components/checkbox-group.d.ts +104 -0
- package/dist/components/checkbox-group.js +111 -0
- package/dist/components/checkbox-group.js.map +1 -0
- package/dist/components/checkbox.d.ts +19 -0
- package/dist/components/checkbox.js +30 -7
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +18 -0
- package/dist/components/chip.js +47 -10
- package/dist/components/chip.js.map +1 -1
- package/dist/components/combobox.d.ts +13 -2
- package/dist/components/combobox.js +17 -8
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +66 -161
- package/dist/components/copy-button.d.ts +10 -8
- package/dist/components/country-select.d.ts +124 -0
- package/dist/components/country-select.js +237 -0
- package/dist/components/country-select.js.map +1 -0
- package/dist/components/currency-select.d.ts +17 -1
- package/dist/components/currency-select.js +56 -23
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +66 -166
- package/dist/components/danger-confirm.js +133 -49
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -2
- package/dist/components/data-table-filter-popover.d.ts +1 -2
- package/dist/components/data-table-filters.d.ts +1 -2
- package/dist/components/data-table-labels.d.ts +4 -97
- package/dist/components/data-table-labels.js +9 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table.d.ts +1 -2
- package/dist/components/data-table.js +98 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +25 -5
- package/dist/components/date-picker.js +110 -59
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +8 -1
- package/dist/components/entity-combobox.js +9 -4
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +10 -8
- package/dist/components/field-anatomy.d.ts +95 -0
- package/dist/components/field-anatomy.js +84 -0
- package/dist/components/field-anatomy.js.map +1 -0
- package/dist/components/field-parts.d.ts +114 -0
- package/dist/components/field-parts.js +72 -0
- package/dist/components/field-parts.js.map +1 -0
- package/dist/components/file-button.d.ts +10 -8
- package/dist/components/file-button.js +13 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +32 -9
- package/dist/components/file-dropzone.js +57 -34
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +10 -8
- package/dist/components/form-actions.js +61 -3
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.d.ts +3 -0
- package/dist/components/hover-menu.js +2 -2
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/iban-input.d.ts +66 -0
- package/dist/components/iban-input.js +125 -0
- package/dist/components/iban-input.js.map +1 -0
- package/dist/components/icon-picker.d.ts +12 -2
- package/dist/components/icon-picker.js +9 -2
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-edit-field.d.ts +167 -0
- package/dist/components/inline-edit-field.js +293 -0
- package/dist/components/inline-edit-field.js.map +1 -0
- package/dist/components/language-select.d.ts +127 -0
- package/dist/components/language-select.js +18 -0
- package/dist/components/language-select.js.map +1 -0
- package/dist/components/line-items.d.ts +25 -3
- package/dist/components/line-items.js +55 -46
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +25 -0
- package/dist/components/list.js +16 -5
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +20 -8
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.d.ts +52 -4
- package/dist/components/month-picker.js +290 -86
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-select.d.ts +8 -1
- package/dist/components/multi-select.js +9 -4
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +25 -13
- package/dist/components/number-field.js +6 -20
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +70 -13
- package/dist/components/number-input.js +90 -38
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +10 -8
- package/dist/components/one-time-code-input.d.ts +100 -0
- package/dist/components/one-time-code-input.js +137 -0
- package/dist/components/one-time-code-input.js.map +1 -0
- package/dist/components/phone-input.d.ts +66 -0
- package/dist/components/phone-input.js +171 -0
- package/dist/components/phone-input.js.map +1 -0
- package/dist/components/series-chart.d.ts +10 -8
- package/dist/components/settings-fields.d.ts +10 -8
- package/dist/components/share-card.d.ts +8 -6
- package/dist/components/sign-chip.d.ts +66 -0
- package/dist/components/sign-chip.js +39 -0
- package/dist/components/sign-chip.js.map +1 -0
- package/dist/components/swatch-picker.d.ts +27 -2
- package/dist/components/swatch-picker.js +73 -31
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/switch.d.ts +19 -0
- package/dist/components/switch.js +24 -6
- package/dist/components/switch.js.map +1 -1
- package/dist/components/text-link.d.ts +10 -8
- package/dist/components/tile-radio.d.ts +68 -13
- package/dist/components/tile-radio.js +3 -3
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +15 -9
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toggle-group.d.ts +47 -1
- package/dist/components/toggle-group.js +110 -69
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/ui.d.ts +10 -8
- package/dist/components/ui.js +280 -78
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -2
- package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
- package/dist/data-table.d.ts +1 -2
- package/dist/data-table.js +2 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +210 -3
- 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 +1 -1
- package/dist/feedback/feedback-thread.d.ts +37 -3
- package/dist/feedback/feedback-thread.js +38 -9
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
- package/dist/feedback.d.ts +2 -2
- package/dist/hooks/use-file-drop.d.ts +10 -8
- package/dist/i18n/defaults.d.ts +10 -8
- package/dist/i18n/defaults.js +17 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +10 -8
- package/dist/i18n/german.js +50 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +10 -8
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +10 -8
- package/dist/i18n/locales/de-CH.d.ts +10 -8
- package/dist/i18n/locales/en.d.ts +10 -8
- package/dist/i18n/locales/en.js +9 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +10 -8
- package/dist/i18n/locales/es.js +48 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +10 -8
- package/dist/i18n/locales/fr.js +48 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +10 -8
- package/dist/i18n/locales/hu.js +49 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +10 -8
- package/dist/i18n/locales/it.js +49 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +10 -8
- package/dist/i18n/locales/zh.js +48 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +10 -8
- package/dist/i18n/review.js +8 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +17 -10
- package/dist/index.js +57 -2
- package/dist/index.js.map +1 -1
- package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
- package/dist/lib/countries.d.ts +43 -0
- package/dist/lib/countries.js +39 -0
- package/dist/lib/countries.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +46 -0
- package/dist/lib/decimal-marks.js +40 -0
- package/dist/lib/decimal-marks.js.map +1 -0
- package/dist/lib/iban.d.ts +98 -0
- package/dist/lib/iban.js +154 -0
- package/dist/lib/iban.js.map +1 -0
- package/dist/lib/isin.d.ts +30 -0
- package/dist/lib/isin.js +36 -0
- package/dist/lib/isin.js.map +1 -0
- package/dist/lib/phone.d.ts +69 -0
- package/dist/lib/phone.js +121 -0
- package/dist/lib/phone.js.map +1 -0
- package/dist/rhf/fields.d.ts +148 -11
- package/dist/rhf/fields.js +332 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +10 -8
- package/dist/rhf.d.ts +13 -9
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +10 -8
- package/dist/shell/top-bar-brand.d.ts +10 -8
- package/dist/shell/topbar-action-menu.d.ts +13 -1
- package/dist/shell/topbar-action-menu.js +31 -28
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +10 -8
- package/dist/tour/tour.js +20 -24
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +10 -8
- package/dist/wizard.d.ts +10 -8
- package/package.json +1 -1
- package/src/components/account-chips.tsx +17 -3
- package/src/components/account-settings.tsx +92 -26
- package/src/components/amount-input.tsx +49 -56
- package/src/components/autocomplete.tsx +133 -110
- package/src/components/checkbox-group.tsx +229 -0
- package/src/components/checkbox.tsx +72 -8
- package/src/components/chip.tsx +80 -10
- package/src/components/combobox.tsx +292 -253
- package/src/components/country-select.tsx +443 -0
- package/src/components/currency-select.tsx +109 -33
- package/src/components/danger-confirm.tsx +345 -77
- package/src/components/data-table-labels.ts +16 -1
- package/src/components/data-table.tsx +175 -6
- package/src/components/date-picker.tsx +173 -62
- package/src/components/entity-combobox.tsx +107 -87
- package/src/components/field-anatomy.tsx +190 -0
- package/src/components/field-parts.tsx +184 -0
- package/src/components/file-button.tsx +36 -3
- package/src/components/file-dropzone.tsx +106 -38
- package/src/components/form-actions.tsx +115 -2
- package/src/components/hover-menu.tsx +7 -4
- package/src/components/iban-input.tsx +231 -0
- package/src/components/icon-picker.tsx +26 -4
- package/src/components/inline-edit-field.tsx +533 -0
- package/src/components/language-select.tsx +78 -0
- package/src/components/line-items.tsx +35 -2
- package/src/components/list.tsx +55 -6
- package/src/components/money-field.tsx +10 -0
- package/src/components/month-picker.tsx +399 -63
- package/src/components/multi-select.tsx +80 -58
- package/src/components/number-field.tsx +28 -46
- package/src/components/number-input.tsx +210 -43
- package/src/components/one-time-code-input.tsx +288 -0
- package/src/components/phone-input.tsx +299 -0
- package/src/components/sign-chip.tsx +103 -0
- package/src/components/swatch-picker.tsx +118 -30
- package/src/components/switch.tsx +58 -7
- package/src/components/tile-radio.tsx +71 -16
- package/src/components/time-input.tsx +8 -1
- package/src/components/toggle-group.tsx +206 -73
- package/src/components/ui.tsx +472 -105
- package/src/data-table.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +313 -9
- package/src/feedback/feedback-dialog.tsx +2 -0
- package/src/feedback/feedback-thread.tsx +66 -5
- package/src/i18n/defaults.ts +16 -1
- package/src/i18n/german.ts +47 -0
- package/src/i18n/kit-labels.tsx +78 -2
- package/src/i18n/locales/en.ts +10 -0
- package/src/i18n/locales/es.ts +46 -0
- package/src/i18n/locales/fr.ts +45 -0
- package/src/i18n/locales/hu.ts +46 -0
- package/src/i18n/locales/it.ts +47 -0
- package/src/i18n/locales/zh.ts +44 -0
- package/src/i18n/review.ts +7 -0
- package/src/index.ts +53 -3
- package/src/lib/countries.ts +91 -0
- package/src/lib/decimal-marks.ts +99 -0
- package/src/lib/iban.ts +171 -0
- package/src/lib/isin.ts +62 -0
- package/src/lib/phone.ts +211 -0
- package/src/rhf/fields.tsx +606 -9
- package/src/rhf.ts +2 -1
- package/src/shell/topbar-action-menu.tsx +53 -29
- package/src/tour/tour.tsx +66 -28
|
@@ -5,6 +5,7 @@ import { useAnnounce } from "../hooks/use-announce";
|
|
|
5
5
|
import { useKitFileLabels, useKitLabels } from "../i18n/kit-labels";
|
|
6
6
|
import { Button, Spinner } from "./ui";
|
|
7
7
|
import { Tooltip } from "./tooltip";
|
|
8
|
+
import { useCommitReason } from "./write-lock";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* A button that opens the file picker — the shape all three apps kept writing by hand
|
|
@@ -428,6 +429,27 @@ export interface FileButtonProps
|
|
|
428
429
|
* file leaves it as it was. Off by default, so the button's layout is unchanged.
|
|
429
430
|
*/
|
|
430
431
|
showFileName?: boolean;
|
|
432
|
+
/**
|
|
433
|
+
* Why picking is not available — {@link Button}'s `disabledReason`, with the picker's
|
|
434
|
+
* half added (keksdose K3): the button is `aria-disabled` but stays focusable, the
|
|
435
|
+
* reason is in the kit Tooltip and its description, a click opens no picker, and a
|
|
436
|
+
* `droppable` button takes no drop (the drag is still cancelled, so the browser does
|
|
437
|
+
* not navigate to the file). Wins over `disabled`, as on Button.
|
|
438
|
+
*/
|
|
439
|
+
disabledReason?: ReactNode;
|
|
440
|
+
/**
|
|
441
|
+
* This pick COMMITS — an upload that starts on pick, an attachment saved at once.
|
|
442
|
+
* Under a locked {@link WriteLockProvider} it takes the `disabledReason` path with
|
|
443
|
+
* the lock's reason (which wins over one of its own). No provider, or an unlocked
|
|
444
|
+
* one: no effect. A picker that only fills a form the user saves later is not a
|
|
445
|
+
* commit, and is left live — the form's Save carries the lock.
|
|
446
|
+
*/
|
|
447
|
+
commit?: boolean;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** A reason that is really there — `null`, `false` and `""` are no reason. */
|
|
451
|
+
function hasReason(reason: ReactNode): boolean {
|
|
452
|
+
return reason !== undefined && reason !== null && reason !== false && reason !== "";
|
|
431
453
|
}
|
|
432
454
|
|
|
433
455
|
/**
|
|
@@ -459,6 +481,8 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
|
|
|
459
481
|
droppable,
|
|
460
482
|
showFileName = false,
|
|
461
483
|
disabled,
|
|
484
|
+
disabledReason: ownDisabledReason,
|
|
485
|
+
commit,
|
|
462
486
|
children,
|
|
463
487
|
className,
|
|
464
488
|
onClick,
|
|
@@ -470,7 +494,13 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
|
|
|
470
494
|
},
|
|
471
495
|
ref,
|
|
472
496
|
) {
|
|
497
|
+
const disabledReason = useCommitReason(commit, ownDisabledReason);
|
|
498
|
+
const locked = hasReason(disabledReason);
|
|
473
499
|
const inert = Boolean(disabled || pending);
|
|
500
|
+
// What takes no files: busy, disabled, or locked with a reason. Kept apart from
|
|
501
|
+
// `inert`, which is what reaches the button's NATIVE `disabled` — a locked button
|
|
502
|
+
// stays focusable (Button's `disabledReason` path), so `locked` must not get there.
|
|
503
|
+
const refusing = inert || locked;
|
|
474
504
|
const [picked, setPicked] = useState<readonly File[]>([]);
|
|
475
505
|
const pickedId = useId();
|
|
476
506
|
const pickerLabels = useKitLabels("filePicker", DEFAULT_FILE_PICKER_LABELS, labels);
|
|
@@ -491,7 +521,7 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
|
|
|
491
521
|
onPick,
|
|
492
522
|
onReject,
|
|
493
523
|
labels,
|
|
494
|
-
disabled:
|
|
524
|
+
disabled: refusing,
|
|
495
525
|
});
|
|
496
526
|
const pickedText =
|
|
497
527
|
picked.length === 0 ? "" : picked.length === 1 ? picked[0].name : pickerLabels.selected(picked.length, picked[0].name);
|
|
@@ -505,7 +535,7 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
|
|
|
505
535
|
onDragEnter?.(e);
|
|
506
536
|
if (!hasFiles(e)) return;
|
|
507
537
|
e.preventDefault();
|
|
508
|
-
if (!
|
|
538
|
+
if (!refusing) setDragOver(true);
|
|
509
539
|
},
|
|
510
540
|
onDragOver: (e: DragEvent<HTMLButtonElement>) => {
|
|
511
541
|
onDragOver?.(e);
|
|
@@ -513,7 +543,7 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
|
|
|
513
543
|
// Always cancel for a file drag, busy or not: an uncancelled drop makes the
|
|
514
544
|
// browser NAVIGATE to the file, which loses the page.
|
|
515
545
|
e.preventDefault();
|
|
516
|
-
e.dataTransfer.dropEffect =
|
|
546
|
+
e.dataTransfer.dropEffect = refusing ? "none" : "copy";
|
|
517
547
|
},
|
|
518
548
|
onDragLeave: (e: DragEvent<HTMLButtonElement>) => {
|
|
519
549
|
onDragLeave?.(e);
|
|
@@ -542,6 +572,9 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
|
|
|
542
572
|
{...dropHandlers}
|
|
543
573
|
type="button"
|
|
544
574
|
disabled={inert}
|
|
575
|
+
// Resolved here rather than passed on as `commit`: the picker above has to know
|
|
576
|
+
// it is locked too, and one resolution keeps the two from disagreeing.
|
|
577
|
+
disabledReason={locked ? disabledReason : undefined}
|
|
545
578
|
aria-busy={pending || undefined}
|
|
546
579
|
aria-describedby={
|
|
547
580
|
showFileName && pickedText
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import type { FilePickHandler, FilePickerLabels, FileRejection } from "./file-button";
|
|
16
16
|
import { useDragTarget } from "../hooks/use-file-drop";
|
|
17
17
|
import { toast } from "./toast";
|
|
18
|
+
import { useCommitReason } from "./write-lock";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Where a refused file's message goes.
|
|
@@ -35,6 +36,11 @@ export interface FileDropzoneState {
|
|
|
35
36
|
dragOver: boolean;
|
|
36
37
|
disabled: boolean;
|
|
37
38
|
busy: boolean;
|
|
39
|
+
/** 0.22: locked with a reason (`disabledReason`, or `commit` under a locked
|
|
40
|
+
* {@link WriteLockProvider}) — takes no files, like `disabled`. */
|
|
41
|
+
locked: boolean;
|
|
42
|
+
/** The reason while `locked`, for a custom body to show; `undefined` otherwise. */
|
|
43
|
+
disabledReason: ReactNode | undefined;
|
|
38
44
|
/** What is chosen: `files` in multiple mode, `[file]` or `[]` in single mode. */
|
|
39
45
|
files: readonly File[];
|
|
40
46
|
/** The inline refusal, when `rejectionFeedback="inline"` has one to show. */
|
|
@@ -139,6 +145,22 @@ export interface FileDropzoneProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
139
145
|
* text. The other half of why keksdose's invoice zone was hand-rolled.
|
|
140
146
|
*/
|
|
141
147
|
busy?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Why the zone is not taking files — the write lock's sentence, a quota, a closed
|
|
150
|
+
* period (keksdose K3). Takes no drop, no picker and no removal, like `disabled`, and
|
|
151
|
+
* SAYS so: the Browse button (and every remove button) stays focusable,
|
|
152
|
+
* `aria-disabled`, with the reason in the kit Tooltip and its description, the zone's
|
|
153
|
+
* group is described by it, and the default body shows it where the hint was. Not
|
|
154
|
+
* dimmed: what is chosen stays readable. Wins over `disabled`'s silent look.
|
|
155
|
+
*/
|
|
156
|
+
disabledReason?: ReactNode;
|
|
157
|
+
/**
|
|
158
|
+
* This zone COMMITS — an upload that starts on drop. Under a locked
|
|
159
|
+
* {@link WriteLockProvider} it takes the `disabledReason` path with the lock's
|
|
160
|
+
* reason (which wins over one of its own). No provider, or an unlocked one: no
|
|
161
|
+
* effect. A zone that only stages a file for a form saved later is not a commit.
|
|
162
|
+
*/
|
|
163
|
+
commit?: boolean;
|
|
142
164
|
/**
|
|
143
165
|
* Replace the zone's body — the icon, the text and the chosen-file list — with your
|
|
144
166
|
* own, given the zone's state: keksdose's spinner-plus-"uploading", its multi-shot
|
|
@@ -182,6 +204,8 @@ export function FileDropzone({
|
|
|
182
204
|
hint: hintProp,
|
|
183
205
|
disabled,
|
|
184
206
|
busy,
|
|
207
|
+
disabledReason: ownDisabledReason,
|
|
208
|
+
commit,
|
|
185
209
|
renderBody,
|
|
186
210
|
className,
|
|
187
211
|
"aria-label": ariaLabel,
|
|
@@ -189,7 +213,12 @@ export function FileDropzone({
|
|
|
189
213
|
...rest
|
|
190
214
|
}: FileDropzoneProps) {
|
|
191
215
|
const [error, setError] = useState<string | null>(null);
|
|
192
|
-
const
|
|
216
|
+
const disabledReason = useCommitReason(commit, ownDisabledReason);
|
|
217
|
+
const locked = hasReason(disabledReason);
|
|
218
|
+
// Locked counts as inert for every file path (drop, click, Browse, remove); only the
|
|
219
|
+
// LOOK and the native `disabled` keep it apart, so its controls stay focusable.
|
|
220
|
+
const inert = Boolean(disabled || busy) || locked;
|
|
221
|
+
const reasonId = useId();
|
|
193
222
|
// State rather than a ref: `open` is handed to `renderBody` during render, and a
|
|
194
223
|
// function that reads a ref there is exactly what the refs rule cannot prove safe.
|
|
195
224
|
const [fileInput, setFileInput] = useState<HTMLInputElement | null>(null);
|
|
@@ -278,7 +307,16 @@ export function FileDropzone({
|
|
|
278
307
|
|
|
279
308
|
const chosen: readonly File[] = multiple ? (files ?? []) : file ? [file] : [];
|
|
280
309
|
const showError = feedback === "inline" && error !== null;
|
|
281
|
-
const describedBy = cn(describedByProp, showError && errorId) || undefined;
|
|
310
|
+
const describedBy = cn(describedByProp, showError && errorId, locked && reasonId) || undefined;
|
|
311
|
+
// The controls inside: natively disabled while disabled or busy; locked, they take
|
|
312
|
+
// the reason instead and stay reachable (Button's `disabledReason` path).
|
|
313
|
+
const controlDisabled = Boolean(disabled || busy);
|
|
314
|
+
const controlReason = locked ? disabledReason : undefined;
|
|
315
|
+
const reasonLine = locked && (
|
|
316
|
+
<div data-slot="file-dropzone-reason" className="text-xs text-[var(--text-muted)]">
|
|
317
|
+
{disabledReason}
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
282
320
|
|
|
283
321
|
return (
|
|
284
322
|
<>
|
|
@@ -311,7 +349,7 @@ export function FileDropzone({
|
|
|
311
349
|
// reached through the description instead, and spoken when it appears.
|
|
312
350
|
aria-describedby={describedBy}
|
|
313
351
|
data-invalid={showError || undefined}
|
|
314
|
-
aria-disabled={disabled || undefined}
|
|
352
|
+
aria-disabled={disabled || locked || undefined}
|
|
315
353
|
aria-busy={busy || undefined}
|
|
316
354
|
data-drag-over={dragOver || undefined}
|
|
317
355
|
className={cn(
|
|
@@ -336,7 +374,9 @@ export function FileDropzone({
|
|
|
336
374
|
? "cursor-not-allowed border-[var(--border)] opacity-60"
|
|
337
375
|
: busy
|
|
338
376
|
? "cursor-progress border-[var(--border)]"
|
|
339
|
-
:
|
|
377
|
+
: locked
|
|
378
|
+
? "cursor-not-allowed border-[var(--border)]"
|
|
379
|
+
: dragOver
|
|
340
380
|
? "cursor-pointer border-[var(--border-strong)] bg-[var(--bg-active)]"
|
|
341
381
|
: showError
|
|
342
382
|
? "cursor-pointer border-[var(--danger-border-strong)]"
|
|
@@ -349,6 +389,8 @@ export function FileDropzone({
|
|
|
349
389
|
dragOver,
|
|
350
390
|
disabled: Boolean(disabled),
|
|
351
391
|
busy: Boolean(busy),
|
|
392
|
+
locked,
|
|
393
|
+
disabledReason: locked ? disabledReason : undefined,
|
|
352
394
|
files: chosen,
|
|
353
395
|
error: showError ? error : null,
|
|
354
396
|
open,
|
|
@@ -365,34 +407,38 @@ export function FileDropzone({
|
|
|
365
407
|
<>
|
|
366
408
|
<Upload aria-hidden className="size-5 text-[var(--text-muted)]" />
|
|
367
409
|
{multiple && chosen.length > 0 ? (
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
410
|
+
<>
|
|
411
|
+
<ul className="flex w-full max-w-sm flex-col gap-1 text-start">
|
|
412
|
+
{chosen.map((f, i) => (
|
|
413
|
+
<li
|
|
414
|
+
// Name + size + position: two files may share a name, and the index
|
|
415
|
+
// alone would re-key every row below a removal.
|
|
416
|
+
key={`${f.name}-${f.size}-${i}`}
|
|
417
|
+
className="flex items-center gap-2 text-sm text-[var(--text-secondary)]"
|
|
418
|
+
>
|
|
419
|
+
<span className="min-w-0 flex-1 truncate font-medium">{f.name}</span>
|
|
420
|
+
<span className="shrink-0 text-xs text-[var(--text-muted)]">{fileText.size(f.size)}</span>
|
|
421
|
+
{onRemove && (
|
|
422
|
+
<IconButton
|
|
423
|
+
type="button"
|
|
424
|
+
size="sm"
|
|
425
|
+
disabled={controlDisabled}
|
|
426
|
+
disabledReason={controlReason}
|
|
427
|
+
aria-label={labels.remove(f.name)}
|
|
428
|
+
onClick={(e) => {
|
|
429
|
+
e.stopPropagation();
|
|
430
|
+
onRemove(f, i);
|
|
431
|
+
afterRemoval(labels.removed(f.name));
|
|
432
|
+
}}
|
|
433
|
+
>
|
|
434
|
+
<X aria-hidden />
|
|
435
|
+
</IconButton>
|
|
436
|
+
)}
|
|
437
|
+
</li>
|
|
438
|
+
))}
|
|
439
|
+
</ul>
|
|
440
|
+
{reasonLine}
|
|
441
|
+
</>
|
|
396
442
|
) : (
|
|
397
443
|
<>
|
|
398
444
|
<div className="flex items-center gap-1 text-sm text-[var(--text-secondary)]">
|
|
@@ -403,7 +449,8 @@ export function FileDropzone({
|
|
|
403
449
|
<IconButton
|
|
404
450
|
type="button"
|
|
405
451
|
size="sm"
|
|
406
|
-
disabled={
|
|
452
|
+
disabled={controlDisabled}
|
|
453
|
+
disabledReason={controlReason}
|
|
407
454
|
aria-label={labels.remove(file.name)}
|
|
408
455
|
onClick={(e) => {
|
|
409
456
|
e.stopPropagation();
|
|
@@ -415,9 +462,16 @@ export function FileDropzone({
|
|
|
415
462
|
</IconButton>
|
|
416
463
|
)}
|
|
417
464
|
</div>
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
465
|
+
{/* Locked, the reason takes the hint's place: what may be dropped is moot
|
|
466
|
+
while nothing may. A chosen file keeps its size, the reason below. */}
|
|
467
|
+
{locked && !(file && !multiple) ? (
|
|
468
|
+
reasonLine
|
|
469
|
+
) : (
|
|
470
|
+
<div className="text-xs text-[var(--text-muted)]">
|
|
471
|
+
{file && !multiple ? fileText.size(file.size) : hint}
|
|
472
|
+
</div>
|
|
473
|
+
)}
|
|
474
|
+
{locked && file && !multiple && reasonLine}
|
|
421
475
|
</>
|
|
422
476
|
)}
|
|
423
477
|
</>
|
|
@@ -427,7 +481,8 @@ export function FileDropzone({
|
|
|
427
481
|
ref={browseRef}
|
|
428
482
|
type="button"
|
|
429
483
|
variant="secondary"
|
|
430
|
-
disabled={
|
|
484
|
+
disabled={controlDisabled}
|
|
485
|
+
disabledReason={controlReason}
|
|
431
486
|
onClick={(e) => {
|
|
432
487
|
e.stopPropagation();
|
|
433
488
|
open();
|
|
@@ -439,7 +494,8 @@ export function FileDropzone({
|
|
|
439
494
|
<Button
|
|
440
495
|
type="button"
|
|
441
496
|
variant="ghost"
|
|
442
|
-
disabled={
|
|
497
|
+
disabled={controlDisabled}
|
|
498
|
+
disabledReason={controlReason}
|
|
443
499
|
onClick={(e) => {
|
|
444
500
|
e.stopPropagation();
|
|
445
501
|
onClear();
|
|
@@ -455,6 +511,13 @@ export function FileDropzone({
|
|
|
455
511
|
{error}
|
|
456
512
|
</p>
|
|
457
513
|
)}
|
|
514
|
+
{/* The group's description while locked: a copy of its own, so a custom body
|
|
515
|
+
that does not show the reason still has the zone say it. */}
|
|
516
|
+
{locked && (
|
|
517
|
+
<span id={reasonId} hidden>
|
|
518
|
+
{disabledReason}
|
|
519
|
+
</span>
|
|
520
|
+
)}
|
|
458
521
|
<input
|
|
459
522
|
ref={setFileInput}
|
|
460
523
|
type="file"
|
|
@@ -484,6 +547,11 @@ export function FileDropzone({
|
|
|
484
547
|
);
|
|
485
548
|
}
|
|
486
549
|
|
|
550
|
+
/** A reason that is really there — `null`, `false` and `""` are no reason. */
|
|
551
|
+
function hasReason(reason: ReactNode): boolean {
|
|
552
|
+
return reason !== undefined && reason !== null && reason !== false && reason !== "";
|
|
553
|
+
}
|
|
554
|
+
|
|
487
555
|
/** A click that landed on a control inside the zone (a custom body's own button, a
|
|
488
556
|
* link in the hint) is that control's, not a request to open the picker. */
|
|
489
557
|
function fromControl(e: MouseEvent<HTMLElement>): boolean {
|
|
@@ -1,22 +1,35 @@
|
|
|
1
|
-
import { isValidElement } from "react";
|
|
2
|
-
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
1
|
+
import { isValidElement, useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, ReactNode, Ref } from "react";
|
|
3
3
|
import type { LucideIcon } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
6
6
|
import { useMediaQuery } from "../hooks/use-media-query";
|
|
7
|
+
import { isApplePlatform } from "../hooks/use-hotkey";
|
|
7
8
|
import { Button, Spinner, type ButtonProps, type ButtonVariant } from "./ui";
|
|
9
|
+
import { useWriteLock } from "./write-lock";
|
|
8
10
|
|
|
9
11
|
/** The words {@link FormActions} renders on its own behalf — the `form` namespace. */
|
|
10
12
|
export interface FormActionsLabels {
|
|
11
13
|
save: string;
|
|
12
14
|
cancel: string;
|
|
15
|
+
/**
|
|
16
|
+
* The {@link FormActionsProps.submitShortcut} hint beside Save's text, when
|
|
17
|
+
* {@link FormActionsProps.submitShortcutHint} shows it. `apple` is true on a Mac,
|
|
18
|
+
* iPhone or iPad, where the key is Cmd: "⌘ Enter" there, "Ctrl+Enter" elsewhere
|
|
19
|
+
* (a German keyboard says "Strg").
|
|
20
|
+
*/
|
|
21
|
+
submitShortcut: (apple: boolean) => string;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
export const DEFAULT_FORM_ACTIONS_LABELS: FormActionsLabels = {
|
|
16
25
|
save: "Save",
|
|
17
26
|
cancel: "Cancel",
|
|
27
|
+
submitShortcut: (apple) => (apple ? "⌘ Enter" : "Ctrl+Enter"),
|
|
18
28
|
};
|
|
19
29
|
|
|
30
|
+
/** See {@link FormActionsProps.submitShortcut}. */
|
|
31
|
+
export type FormActionsSubmitShortcut = "mod-enter";
|
|
32
|
+
|
|
20
33
|
export type FormActionsAlign = "start" | "center" | "end" | "between";
|
|
21
34
|
|
|
22
35
|
/**
|
|
@@ -229,10 +242,60 @@ export interface FormActionsProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
229
242
|
/** The `id` of the `<form>` the save button submits, when the row is rendered
|
|
230
243
|
* outside it (a dialog's footer slot). */
|
|
231
244
|
form?: string;
|
|
245
|
+
/**
|
|
246
|
+
* A keyboard shortcut that presses Save from anywhere in the form. `"mod-enter"`:
|
|
247
|
+
* Ctrl+Enter, or Cmd+Enter on a Mac — both are accepted everywhere, as keksdose's
|
|
248
|
+
* editors always have. keksdose K14 (inputs audit 2026-10-02, S5): its transaction
|
|
249
|
+
* editor, category editor and create card each wrote this as an `onKeyDown` on a
|
|
250
|
+
* container, re-stating `!pending && !saveDisabled && !lock.locked` every time, and
|
|
251
|
+
* the one that forgot the lock let the demo's refused save through from the keyboard.
|
|
252
|
+
*
|
|
253
|
+
* What it does is what pressing Save does — the same button is clicked, so a form
|
|
254
|
+
* submits with its validation and its submitter, and `onSubmit` is called in the
|
|
255
|
+
* `type="button"` mode — and it does NOTHING while Save could not be pressed:
|
|
256
|
+
* `pending`, `submitDisabled`, or a `commit` under a locked {@link WriteLockProvider}.
|
|
257
|
+
*
|
|
258
|
+
* Scoped to the form Save submits (the `form` prop's, else the enclosing `<form>`):
|
|
259
|
+
* the chord counts only when the focus is inside it, so two editors on one page each
|
|
260
|
+
* answer for their own fields. Without a form (an `onSubmit` row in a plain container)
|
|
261
|
+
* the scope is the nearest `role="dialog"`, else the row's parent element — the
|
|
262
|
+
* container the row ends.
|
|
263
|
+
*
|
|
264
|
+
* Listened for on the document, so it runs AFTER every React handler: a field that
|
|
265
|
+
* takes Ctrl+Enter for itself calls `preventDefault()` or `stopPropagation()` and
|
|
266
|
+
* the row stays out of it (keksdose live #202 — the create card's Ctrl+Shift+Enter
|
|
267
|
+
* must create ONE row; the chord here is exactly Ctrl/Cmd+Enter, never with Shift or
|
|
268
|
+
* Alt). Not during IME composition, and not on a held key's repeats.
|
|
269
|
+
*
|
|
270
|
+
* The save button announces it with `aria-keyshortcuts`; {@link submitShortcutHint}
|
|
271
|
+
* also shows it.
|
|
272
|
+
*/
|
|
273
|
+
submitShortcut?: FormActionsSubmitShortcut;
|
|
274
|
+
/** Show the {@link submitShortcut} beside Save's text ("Ctrl+Enter", "⌘ Enter"; the
|
|
275
|
+
* `form.submitShortcut` label), dimmed. Hidden on a touch-only device, which has no
|
|
276
|
+
* keyboard to press it on. Default `false`. */
|
|
277
|
+
submitShortcutHint?: boolean;
|
|
232
278
|
/** Extra actions, placed before Cancel. */
|
|
233
279
|
children?: ReactNode;
|
|
234
280
|
}
|
|
235
281
|
|
|
282
|
+
/** Hand `el` to a caller's ref, whichever kind it is. */
|
|
283
|
+
function assignRef<E>(ref: Ref<E> | undefined, el: E | null): void {
|
|
284
|
+
if (typeof ref === "function") ref(el);
|
|
285
|
+
else if (ref) (ref as { current: E | null }).current = el;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Where {@link FormActionsProps.submitShortcut} listens: the form Save submits (named
|
|
290
|
+
* by `formId`, else the enclosing one), else the nearest dialog, else the row's parent.
|
|
291
|
+
*/
|
|
292
|
+
function shortcutScope(row: HTMLElement | null, formId: string | undefined): Element | null {
|
|
293
|
+
if (!row) return null;
|
|
294
|
+
const named = formId ? row.ownerDocument.getElementById(formId) : null;
|
|
295
|
+
if (named instanceof HTMLFormElement) return named;
|
|
296
|
+
return row.closest("form") ?? row.closest('[role="dialog"]') ?? row.parentElement;
|
|
297
|
+
}
|
|
298
|
+
|
|
236
299
|
function isDestructiveData(value: unknown): value is FormActionsDestructive {
|
|
237
300
|
return typeof value === "object" && value !== null && !isValidElement(value) && "onClick" in value;
|
|
238
301
|
}
|
|
@@ -272,12 +335,47 @@ export function FormActions({
|
|
|
272
335
|
stickyWithin = "viewport",
|
|
273
336
|
bleed,
|
|
274
337
|
form,
|
|
338
|
+
submitShortcut,
|
|
339
|
+
submitShortcutHint = false,
|
|
275
340
|
children,
|
|
276
341
|
className,
|
|
277
342
|
style,
|
|
278
343
|
...rest
|
|
279
344
|
}: FormActionsProps) {
|
|
280
345
|
const labels = useKitLabels("form", DEFAULT_FORM_ACTIONS_LABELS);
|
|
346
|
+
const lock = useWriteLock();
|
|
347
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
348
|
+
const saveRef = useRef<HTMLButtonElement | null>(null);
|
|
349
|
+
// The caller's own `submitProps.ref` still gets the element.
|
|
350
|
+
const callerSaveRef = submitProps?.ref;
|
|
351
|
+
const setSaveRef = useCallback(
|
|
352
|
+
(el: HTMLButtonElement | null) => {
|
|
353
|
+
saveRef.current = el;
|
|
354
|
+
assignRef(callerSaveRef, el);
|
|
355
|
+
},
|
|
356
|
+
[callerSaveRef],
|
|
357
|
+
);
|
|
358
|
+
// Read by the shortcut's listener at the moment of the key press, so the listener
|
|
359
|
+
// itself is attached once and never sees a stale `pending`.
|
|
360
|
+
const canSave = !pending && !submitDisabled && !(commit && lock.locked);
|
|
361
|
+
const canSaveRef = useRef(canSave);
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
canSaveRef.current = canSave;
|
|
364
|
+
});
|
|
365
|
+
useEffect(() => {
|
|
366
|
+
if (submitShortcut !== "mod-enter") return;
|
|
367
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
368
|
+
if (e.key !== "Enter" || !(e.ctrlKey || e.metaKey) || e.shiftKey || e.altKey) return;
|
|
369
|
+
if (e.repeat || e.isComposing || e.defaultPrevented || !canSaveRef.current) return;
|
|
370
|
+
const scope = shortcutScope(rootRef.current, form);
|
|
371
|
+
if (!scope || !(e.target instanceof Node) || !scope.contains(e.target)) return;
|
|
372
|
+
e.preventDefault();
|
|
373
|
+
saveRef.current?.click();
|
|
374
|
+
};
|
|
375
|
+
document.addEventListener("keydown", onKeyDown);
|
|
376
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
377
|
+
}, [submitShortcut, form]);
|
|
378
|
+
const apple = submitShortcut ? isApplePlatform() : false;
|
|
281
379
|
const placement = useResolvedPlacement(placementProp);
|
|
282
380
|
const bleedLength = bleed === undefined || bleed === "" ? undefined : cssLength(bleed);
|
|
283
381
|
const hasDestructive = destructive !== undefined && destructive !== null && destructive !== false;
|
|
@@ -302,6 +400,7 @@ export function FormActions({
|
|
|
302
400
|
|
|
303
401
|
return (
|
|
304
402
|
<div
|
|
403
|
+
ref={rootRef}
|
|
305
404
|
data-slot="form-actions"
|
|
306
405
|
data-placement={placement}
|
|
307
406
|
data-sticky-within={placement === "sticky" ? stickyWithin : undefined}
|
|
@@ -342,6 +441,10 @@ export function FormActions({
|
|
|
342
441
|
)}
|
|
343
442
|
<Button
|
|
344
443
|
{...submitProps}
|
|
444
|
+
ref={setSaveRef}
|
|
445
|
+
aria-keyshortcuts={
|
|
446
|
+
submitShortcut === "mod-enter" ? "Control+Enter Meta+Enter" : submitProps?.["aria-keyshortcuts"]
|
|
447
|
+
}
|
|
345
448
|
type={onSubmit ? "button" : "submit"}
|
|
346
449
|
form={form}
|
|
347
450
|
onClick={onSubmit}
|
|
@@ -359,6 +462,16 @@ export function FormActions({
|
|
|
359
462
|
SubmitIcon && <SubmitIcon aria-hidden className="size-4 shrink-0" />
|
|
360
463
|
)}
|
|
361
464
|
{pending && pendingLabel !== undefined ? pendingLabel : saveLabel}
|
|
465
|
+
{submitShortcut && submitShortcutHint && (
|
|
466
|
+
// Hidden from the accessibility tree: `aria-keyshortcuts` already says it,
|
|
467
|
+
// and in the name it would make the button "Save Ctrl+Enter".
|
|
468
|
+
<kbd
|
|
469
|
+
aria-hidden
|
|
470
|
+
className="ms-1 font-sans text-[0.85em] font-normal whitespace-nowrap opacity-70 pointer-coarse:hidden"
|
|
471
|
+
>
|
|
472
|
+
{labels.submitShortcut(apple)}
|
|
473
|
+
</kbd>
|
|
474
|
+
)}
|
|
362
475
|
</Button>
|
|
363
476
|
</div>
|
|
364
477
|
</div>
|
|
@@ -12,7 +12,9 @@ import { useEscapeKey, useOutsideClick } from "../hooks/use-dismiss";
|
|
|
12
12
|
* the panel's `close`, not a `ReactNode`.
|
|
13
13
|
*/
|
|
14
14
|
export interface HoverMenuProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
15
|
-
trigger
|
|
15
|
+
/** The trigger. `toggle` for a button's click; `close` for a trigger that is a LINK —
|
|
16
|
+
* following it should shut a panel hover opened (TopBarActionMenu `href`). */
|
|
17
|
+
trigger: (state: { open: boolean; toggle: () => void; close: () => void }) => ReactNode;
|
|
16
18
|
children: (close: () => void) => ReactNode;
|
|
17
19
|
/**
|
|
18
20
|
* Which edge of the trigger the panel lines up with. `start`/`end` follow the
|
|
@@ -209,8 +211,9 @@ export function HoverMenu({
|
|
|
209
211
|
if (target !== triggerEl()) return;
|
|
210
212
|
if (e.key === "Enter" || e.key === " ") {
|
|
211
213
|
// The trigger's own click handler does the toggling; this only records that
|
|
212
|
-
// the press came from a keyboard.
|
|
213
|
-
|
|
214
|
+
// the press came from a keyboard. Not for a link trigger: Enter follows it, and
|
|
215
|
+
// a mark left here would pull focus into the panel the next time hover opens it.
|
|
216
|
+
if (!open && !target.matches("a[href]")) focusOnOpenRef.current = "first";
|
|
214
217
|
} else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
215
218
|
e.preventDefault();
|
|
216
219
|
const edge = e.key === "ArrowDown" ? "first" : "last";
|
|
@@ -289,7 +292,7 @@ export function HoverMenu({
|
|
|
289
292
|
onKeyDown(e);
|
|
290
293
|
}}
|
|
291
294
|
>
|
|
292
|
-
{trigger({ open, toggle })}
|
|
295
|
+
{trigger({ open, toggle, close })}
|
|
293
296
|
{open && (
|
|
294
297
|
<div
|
|
295
298
|
ref={panelRef}
|