@eifi1/ui-kit 0.6.1 → 0.7.1
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 +69 -33
- package/dist/chart.d.ts +1 -0
- package/dist/components/account-settings.js +3 -2
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +5 -0
- package/dist/components/amount-input.js +6 -4
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +33 -6
- package/dist/components/autocomplete.js +62 -16
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/calculator.d.ts +5 -0
- package/dist/components/calculator.js +2 -1
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.js +2 -1
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chart.d.ts +33 -2
- package/dist/components/chart.js +20 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +34 -14
- package/dist/components/chip.js +63 -28
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +18 -1
- package/dist/components/choice-card.js +21 -8
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.d.ts +61 -2
- package/dist/components/combobox-core.js +68 -17
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +11 -7
- package/dist/components/combobox.js +51 -28
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.js +3 -3
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +30 -6
- package/dist/components/danger-confirm.js +51 -32
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +6 -3
- package/dist/components/data-table-pagination.js +6 -5
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +332 -248
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +17 -7
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +28 -3
- package/dist/components/dialog-frame.js +47 -11
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +22 -2
- package/dist/components/disclosure.js +41 -8
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.d.ts +7 -3
- package/dist/components/dropdown.js +6 -2
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +17 -8
- package/dist/components/entity-combobox.js +7 -5
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/file-button.d.ts +38 -5
- package/dist/components/file-button.js +23 -2
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +11 -5
- package/dist/components/file-dropzone.js +14 -19
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/grouped-picker.js +2 -2
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +8 -3
- package/dist/components/hover-menu.js +88 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +5 -1
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/measured-grid.d.ts +165 -0
- package/dist/components/measured-grid.js +559 -0
- package/dist/components/measured-grid.js.map +1 -0
- package/dist/components/mini-calendar.js +7 -8
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/month-picker.js +13 -9
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +9 -1
- package/dist/components/multi-select.js +22 -8
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +5 -0
- package/dist/components/number-input.d.ts +5 -0
- package/dist/components/number-input.js +3 -3
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +5 -0
- package/dist/components/numpad-sheet.js +3 -3
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +11 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/series-chart-ticks.d.ts +13 -0
- package/dist/components/series-chart-ticks.js +29 -0
- package/dist/components/series-chart-ticks.js.map +1 -0
- package/dist/components/series-chart.js +33 -27
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +2 -0
- package/dist/components/swatch-picker.d.ts +10 -1
- package/dist/components/swatch-picker.js +2 -0
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +11 -2
- package/dist/components/swipeable-row.js +5 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/tile-radio.js +5 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +2 -0
- package/dist/components/toggle-group.d.ts +13 -3
- package/dist/components/toggle-group.js +21 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +1 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +10 -4
- package/dist/components/tooltip.js +23 -6
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/treemap.d.ts +6 -1
- package/dist/components/treemap.js +16 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +36 -8
- package/dist/components/ui.js +27 -19
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-anchor-dir.d.ts +15 -0
- package/dist/components/use-anchor-dir.js +14 -0
- package/dist/components/use-anchor-dir.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +8 -1
- package/dist/components/use-mobile-reveal.js +3 -2
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +4 -4
- package/dist/components/use-table-state.js +12 -5
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
- package/dist/data-table.d.ts +1 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +22 -3
- package/dist/feedback/feedback-attachment.js +15 -7
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +3 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-windowed-rows.d.ts +29 -0
- package/dist/hooks/use-windowed-rows.js +31 -0
- package/dist/hooks/use-windowed-rows.js.map +1 -0
- package/dist/i18n/defaults.d.ts +5 -0
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +7 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH.d.ts +45 -0
- package/dist/i18n/locales/de-CH.js +21 -0
- package/dist/i18n/locales/de-CH.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +45 -0
- package/dist/i18n/locales/de.js +344 -0
- package/dist/i18n/locales/de.js.map +1 -0
- package/dist/i18n/locales/es.d.ts +45 -0
- package/dist/i18n/locales/es.js +339 -0
- package/dist/i18n/locales/es.js.map +1 -0
- package/dist/i18n/locales/fr.d.ts +45 -0
- package/dist/i18n/locales/fr.js +344 -0
- package/dist/i18n/locales/fr.js.map +1 -0
- package/dist/i18n/locales/hu.d.ts +45 -0
- package/dist/i18n/locales/hu.js +343 -0
- package/dist/i18n/locales/hu.js.map +1 -0
- package/dist/i18n/locales/it.d.ts +45 -0
- package/dist/i18n/locales/it.js +342 -0
- package/dist/i18n/locales/it.js.map +1 -0
- package/dist/i18n/locales/zh.d.ts +45 -0
- package/dist/i18n/locales/zh.js +338 -0
- package/dist/i18n/locales/zh.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/direction.d.ts +24 -0
- package/dist/lib/direction.js +19 -0
- package/dist/lib/direction.js.map +1 -0
- package/dist/rhf/form.d.ts +2 -0
- package/dist/rhf/form.js.map +1 -1
- package/dist/rhf.d.ts +2 -0
- package/dist/search/command-palette.d.ts +5 -0
- package/dist/search/command-palette.js +80 -50
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +13 -1
- package/dist/shell/app-shell.js +134 -85
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +7 -3
- package/dist/shell/topbar-controls.js +38 -31
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +7 -1
- package/dist/theme/chart-palette.js +2 -1
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/tour/tour.d.ts +11 -5
- package/dist/tour/tour.js +124 -58
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/use-wizard.js +30 -13
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.js +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +12 -2
- package/src/components/account-settings.tsx +6 -2
- package/src/components/amount-input.tsx +10 -8
- package/src/components/autocomplete.tsx +110 -24
- package/src/components/calculator.tsx +5 -1
- package/src/components/chart-zoom.tsx +8 -1
- package/src/components/chart.tsx +68 -7
- package/src/components/chip.tsx +131 -49
- package/src/components/choice-card.tsx +77 -36
- package/src/components/combobox-core.tsx +147 -20
- package/src/components/combobox.tsx +100 -44
- package/src/components/currency-select.tsx +6 -6
- package/src/components/danger-confirm.tsx +90 -36
- package/src/components/data-table-pagination.tsx +32 -19
- package/src/components/data-table.tsx +198 -36
- package/src/components/date-picker.tsx +24 -7
- package/src/components/dialog-frame.tsx +97 -22
- package/src/components/disclosure.tsx +75 -13
- package/src/components/dropdown.tsx +17 -5
- package/src/components/entity-combobox.tsx +26 -13
- package/src/components/file-button.tsx +74 -5
- package/src/components/file-dropzone.tsx +44 -35
- package/src/components/grouped-picker.tsx +2 -2
- package/src/components/hover-menu.tsx +139 -10
- package/src/components/icon-picker.tsx +14 -1
- package/src/components/measured-grid.tsx +852 -0
- package/src/components/mini-calendar.tsx +11 -8
- package/src/components/month-picker.tsx +18 -11
- package/src/components/multi-entity-combobox.tsx +2 -2
- package/src/components/multi-select.tsx +34 -7
- package/src/components/number-input.tsx +4 -4
- package/src/components/numpad-sheet.tsx +5 -3
- package/src/components/picker-sheet.tsx +1 -1
- package/src/components/popover.tsx +18 -3
- package/src/components/series-chart-ticks.ts +52 -0
- package/src/components/series-chart.tsx +39 -31
- package/src/components/swatch-picker.tsx +11 -0
- package/src/components/swipeable-row.tsx +21 -6
- package/src/components/tile-radio.tsx +5 -7
- package/src/components/toggle-group.tsx +39 -3
- package/src/components/toggle-legend.tsx +5 -2
- package/src/components/tooltip.tsx +40 -12
- package/src/components/treemap.tsx +28 -5
- package/src/components/ui.tsx +79 -33
- package/src/components/use-anchor-dir.ts +20 -0
- package/src/components/use-mobile-reveal.ts +10 -2
- package/src/components/use-table-state.ts +44 -7
- package/src/data-table.ts +1 -0
- package/src/feedback/feedback-attachment.tsx +36 -9
- package/src/feedback/feedback-dialog.tsx +3 -2
- package/src/hooks/use-windowed-rows.ts +69 -0
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/de-CH.ts +30 -0
- package/src/i18n/locales/de.ts +365 -0
- package/src/i18n/locales/es.ts +358 -0
- package/src/i18n/locales/fr.ts +371 -0
- package/src/i18n/locales/hu.ts +359 -0
- package/src/i18n/locales/it.ts +360 -0
- package/src/i18n/locales/zh.ts +348 -0
- package/src/index.ts +10 -1
- package/src/lib/direction.ts +37 -0
- package/src/rhf/form.tsx +21 -0
- package/src/search/command-palette.tsx +77 -19
- package/src/shell/app-shell.tsx +201 -130
- package/src/shell/topbar-action-menu.tsx +1 -1
- package/src/shell/topbar-controls.tsx +19 -8
- package/src/theme/chart-palette.ts +9 -2
- package/src/tour/tour.tsx +119 -18
- package/src/wizard/use-wizard.ts +55 -17
- package/src/wizard/wizard-summary.tsx +1 -1
package/src/tour/tour.tsx
CHANGED
|
@@ -13,8 +13,15 @@ import { cn } from "../lib/cn";
|
|
|
13
13
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
14
14
|
import { Button } from "../components/ui";
|
|
15
15
|
import { useFocusTrap } from "../hooks/use-focus-trap";
|
|
16
|
+
import { dirOf, horizontalStep } from "../lib/direction";
|
|
17
|
+
import type { Direction } from "../lib/direction";
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
/**
|
|
20
|
+
* `start`/`end` follow the reading direction (`end` is the right side in LTR and the
|
|
21
|
+
* left side in RTL) and are what a translated app wants. `left`/`right` stay physical
|
|
22
|
+
* for the steps that genuinely mean a screen side.
|
|
23
|
+
*/
|
|
24
|
+
export type TourPlacement = "top" | "right" | "bottom" | "left" | "start" | "end" | "center";
|
|
18
25
|
|
|
19
26
|
export interface TourStep {
|
|
20
27
|
/** CSS selector for the element to spotlight. Omit for a centered step
|
|
@@ -28,13 +35,14 @@ export interface TourStep {
|
|
|
28
35
|
padding?: number;
|
|
29
36
|
/** Where the card sits relative to the target. Omit for auto placement
|
|
30
37
|
* (below → above → centered). "center" ignores the target and centers on
|
|
31
|
-
* screen.
|
|
32
|
-
*
|
|
38
|
+
* screen. "start"/"end" are the logical sides and flip in RTL; "left"/"right"
|
|
39
|
+
* are physical. An explicit side flips to the opposite side if it would
|
|
40
|
+
* overflow, then clamps into the viewport. */
|
|
33
41
|
placement?: TourPlacement;
|
|
34
42
|
/** Advance only when the user clicks the spotlighted target itself (not the
|
|
35
43
|
* card's Next button). Requires `target`: the real control's own handler runs
|
|
36
|
-
* first, then the tour steps forward on a microtask. The
|
|
37
|
-
* click-through to the target. */
|
|
44
|
+
* first, then the tour steps forward on a microtask. The spotlight hole stays
|
|
45
|
+
* click-through to the target; the rest of the page does not. */
|
|
38
46
|
awaitClick?: boolean;
|
|
39
47
|
/** Runs before the step is shown — e.g. navigate to a route, open a panel.
|
|
40
48
|
* May be async; the step waits for it, then locates the target. */
|
|
@@ -269,6 +277,11 @@ function TourOverlay({
|
|
|
269
277
|
const isLast = index === steps.length - 1;
|
|
270
278
|
const [rect, setRect] = useState<Rect | null>(null);
|
|
271
279
|
const [ready, setReady] = useState(false);
|
|
280
|
+
// The overlay is portalled to <body>, out of whatever `dir` subtree the app set, so it
|
|
281
|
+
// takes the direction of the element it spotlights (the document's for a centered
|
|
282
|
+
// step). Arrow keys and start/end placement read it.
|
|
283
|
+
const [dir, setDir] = useState<Direction>(() => dirOf(null));
|
|
284
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
272
285
|
|
|
273
286
|
// Run beforeStep, then locate the target (retrying across route/render lag).
|
|
274
287
|
useEffect(() => {
|
|
@@ -280,6 +293,7 @@ function TourOverlay({
|
|
|
280
293
|
await step.beforeStep?.();
|
|
281
294
|
if (cancelled) return;
|
|
282
295
|
if (!step.target) {
|
|
296
|
+
setDir(dirOf(null));
|
|
283
297
|
setReady(true);
|
|
284
298
|
return;
|
|
285
299
|
}
|
|
@@ -291,11 +305,15 @@ function TourOverlay({
|
|
|
291
305
|
el.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
292
306
|
const r = el.getBoundingClientRect();
|
|
293
307
|
setRect({ top: r.top, left: r.left, width: r.width, height: r.height });
|
|
308
|
+
setDir(dirOf(el));
|
|
294
309
|
setReady(true);
|
|
295
310
|
return;
|
|
296
311
|
}
|
|
297
312
|
if (tries++ < 45) raf = requestAnimationFrame(find);
|
|
298
|
-
else
|
|
313
|
+
else {
|
|
314
|
+
setDir(dirOf(null));
|
|
315
|
+
setReady(true); // give up → render centered
|
|
316
|
+
}
|
|
299
317
|
};
|
|
300
318
|
find();
|
|
301
319
|
})();
|
|
@@ -326,8 +344,8 @@ function TourOverlay({
|
|
|
326
344
|
};
|
|
327
345
|
}, [index, step]);
|
|
328
346
|
|
|
329
|
-
// awaitClick: advance when the *real* target is clicked. The
|
|
330
|
-
// click-through (
|
|
347
|
+
// awaitClick: advance when the *real* target is clicked. The spotlight hole is
|
|
348
|
+
// click-through (see the shields in the render below) so the element's own
|
|
331
349
|
// handler runs first; we step forward on the next macrotask.
|
|
332
350
|
useEffect(() => {
|
|
333
351
|
if (!ready || !step.awaitClick || !step.target) return;
|
|
@@ -338,21 +356,34 @@ function TourOverlay({
|
|
|
338
356
|
return () => el.removeEventListener("click", onClick);
|
|
339
357
|
}, [ready, index, step, onNext]);
|
|
340
358
|
|
|
341
|
-
// Keyboard: Esc skips,
|
|
342
|
-
//
|
|
343
|
-
// escape
|
|
359
|
+
// Keyboard: Esc skips, Enter and the reading-direction arrow advance (→ in LTR,
|
|
360
|
+
// ← in RTL), the other arrow goes back. On awaitClick steps Enter is ignored (it
|
|
361
|
+
// would fire the target's click AND advance) — the forward arrow stays as an escape
|
|
362
|
+
// hatch.
|
|
363
|
+
//
|
|
364
|
+
// Enter on a focused control belongs to that control. This used to advance from
|
|
365
|
+
// anywhere, so Enter on a focused Skip or Back ran the button's own click AND stepped
|
|
366
|
+
// forward: Back moved nowhere, Skip skipped and then called `next` on a dead tour.
|
|
367
|
+
// The same goes for arrows in a text field a step's `action` might contain.
|
|
344
368
|
useEffect(() => {
|
|
345
369
|
const onKey = (e: KeyboardEvent) => {
|
|
346
370
|
if (e.key === "Escape") {
|
|
347
371
|
onSkip();
|
|
348
|
-
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (e.key === "Enter") {
|
|
375
|
+
if (step.awaitClick || ownsKey(e.target, "Enter")) return;
|
|
349
376
|
e.preventDefault();
|
|
350
377
|
onNext();
|
|
351
|
-
|
|
352
|
-
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (ownsKey(e.target, e.key)) return;
|
|
381
|
+
// The overlay root carries `dir`; before it paints, the document's answers.
|
|
382
|
+
const delta = horizontalStep(e.key, rootRef.current);
|
|
383
|
+
if (delta === 1) {
|
|
353
384
|
e.preventDefault();
|
|
354
385
|
onNext();
|
|
355
|
-
} else if (
|
|
386
|
+
} else if (delta === -1) {
|
|
356
387
|
if (!isFirst) onPrev();
|
|
357
388
|
}
|
|
358
389
|
};
|
|
@@ -367,8 +398,24 @@ function TourOverlay({
|
|
|
367
398
|
? { top: rect.top - pad, left: rect.left - pad, width: rect.width + pad * 2, height: rect.height + pad * 2 }
|
|
368
399
|
: null;
|
|
369
400
|
|
|
401
|
+
// `aria-modal` promises the page behind is out of reach, and the card's focus trap
|
|
402
|
+
// keeps that promise for the keyboard. The pointer used to get no such treatment: the
|
|
403
|
+
// root is `pointer-events-none` so that the spotlight hole can stay live for
|
|
404
|
+
// `awaitClick`, and the scrim is a box-shadow, which takes no clicks — so every
|
|
405
|
+
// control on the dimmed page stayed clickable. Four shields now tile the viewport
|
|
406
|
+
// around the hole: the page is blocked, the card and the spotlighted target are not.
|
|
407
|
+
// Keeping `aria-modal` (rather than dropping it) is the coherent half: the tour
|
|
408
|
+
// really is modal — focus is trapped and Esc/Skip is the way out.
|
|
370
409
|
return createPortal(
|
|
371
|
-
<div
|
|
410
|
+
<div
|
|
411
|
+
ref={rootRef}
|
|
412
|
+
className="pointer-events-none fixed inset-0 z-[60]"
|
|
413
|
+
role="dialog"
|
|
414
|
+
aria-modal="true"
|
|
415
|
+
aria-label={step.title}
|
|
416
|
+
dir={dir}
|
|
417
|
+
>
|
|
418
|
+
{spot && <SpotlightShields spot={spot} />}
|
|
372
419
|
{spot ? (
|
|
373
420
|
<div
|
|
374
421
|
className="pointer-events-none fixed rounded-lg transition-all duration-200 ease-out"
|
|
@@ -410,6 +457,7 @@ function TourOverlay({
|
|
|
410
457
|
isFirst={isFirst}
|
|
411
458
|
isLast={isLast}
|
|
412
459
|
rect={spot}
|
|
460
|
+
dir={dir}
|
|
413
461
|
onBack={onPrev}
|
|
414
462
|
onNext={onNext}
|
|
415
463
|
onSkip={onSkip}
|
|
@@ -419,6 +467,53 @@ function TourOverlay({
|
|
|
419
467
|
);
|
|
420
468
|
}
|
|
421
469
|
|
|
470
|
+
/** Does the focused element handle `key` itself? Enter on a button or link, and any
|
|
471
|
+
* editing key in a text field or select, is the control's — not the tour's. */
|
|
472
|
+
function ownsKey(target: EventTarget | null, key: string): boolean {
|
|
473
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
474
|
+
if (target.isContentEditable) return true;
|
|
475
|
+
const tag = target.tagName;
|
|
476
|
+
if (tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
477
|
+
if (tag === "INPUT") {
|
|
478
|
+
const type = (target as HTMLInputElement).type;
|
|
479
|
+
// Enter on a checkbox does nothing natively, but ←/→ on a radio moves it.
|
|
480
|
+
return key !== "Enter" || type !== "checkbox";
|
|
481
|
+
}
|
|
482
|
+
if (key !== "Enter") return false;
|
|
483
|
+
return (
|
|
484
|
+
tag === "BUTTON" ||
|
|
485
|
+
(tag === "A" && target.hasAttribute("href")) ||
|
|
486
|
+
target.getAttribute("role") === "button" ||
|
|
487
|
+
target.getAttribute("role") === "link"
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Transparent pointer blockers tiling the viewport around the spotlight hole: one
|
|
493
|
+
* full-width band above and below, one on each side at the hole's own height. The hole
|
|
494
|
+
* itself stays uncovered, so the target keeps its clicks.
|
|
495
|
+
*/
|
|
496
|
+
function SpotlightShields({ spot }: { spot: Rect }) {
|
|
497
|
+
const shield = "pointer-events-auto fixed";
|
|
498
|
+
const bottom = spot.top + spot.height;
|
|
499
|
+
return (
|
|
500
|
+
<>
|
|
501
|
+
<div data-tour-shield="" className={shield} style={{ top: 0, left: 0, right: 0, height: Math.max(0, spot.top) }} />
|
|
502
|
+
<div data-tour-shield="" className={shield} style={{ top: bottom, left: 0, right: 0, bottom: 0 }} />
|
|
503
|
+
<div
|
|
504
|
+
data-tour-shield=""
|
|
505
|
+
className={shield}
|
|
506
|
+
style={{ top: spot.top, left: 0, width: Math.max(0, spot.left), height: spot.height }}
|
|
507
|
+
/>
|
|
508
|
+
<div
|
|
509
|
+
data-tour-shield=""
|
|
510
|
+
className={shield}
|
|
511
|
+
style={{ top: spot.top, left: spot.left + spot.width, right: 0, height: spot.height }}
|
|
512
|
+
/>
|
|
513
|
+
</>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
422
517
|
/**
|
|
423
518
|
* Card position (viewport px) for the given placement relative to the padded
|
|
424
519
|
* spotlight `rect`. Omitted placement keeps the original auto behavior
|
|
@@ -434,7 +529,11 @@ function placeCard(
|
|
|
434
529
|
vh: number,
|
|
435
530
|
m: number,
|
|
436
531
|
placement?: TourPlacement,
|
|
532
|
+
dir: Direction = "ltr",
|
|
437
533
|
): { top: number; left: number } {
|
|
534
|
+
// The logical sides resolve to a physical one here, and only here.
|
|
535
|
+
if (placement === "start") placement = dir === "rtl" ? "right" : "left";
|
|
536
|
+
else if (placement === "end") placement = dir === "rtl" ? "left" : "right";
|
|
438
537
|
if (!rect || placement === "center") {
|
|
439
538
|
return { top: Math.max(m, (vh - ch) / 2), left: Math.max(m, (vw - cw) / 2) };
|
|
440
539
|
}
|
|
@@ -475,6 +574,7 @@ function TourCard({
|
|
|
475
574
|
isFirst,
|
|
476
575
|
isLast,
|
|
477
576
|
rect,
|
|
577
|
+
dir,
|
|
478
578
|
onBack,
|
|
479
579
|
onNext,
|
|
480
580
|
onSkip,
|
|
@@ -486,6 +586,7 @@ function TourCard({
|
|
|
486
586
|
isFirst: boolean;
|
|
487
587
|
isLast: boolean;
|
|
488
588
|
rect: Rect | null;
|
|
589
|
+
dir: Direction;
|
|
489
590
|
onBack: () => void;
|
|
490
591
|
onNext: () => void;
|
|
491
592
|
onSkip: () => void;
|
|
@@ -524,9 +625,9 @@ function TourCard({
|
|
|
524
625
|
const card = ref.current;
|
|
525
626
|
if (!card) return;
|
|
526
627
|
setStyle(
|
|
527
|
-
placeCard(rect, card.offsetWidth, card.offsetHeight, window.innerWidth, window.innerHeight, 12, step.placement),
|
|
628
|
+
placeCard(rect, card.offsetWidth, card.offsetHeight, window.innerWidth, window.innerHeight, 12, step.placement, dir),
|
|
528
629
|
);
|
|
529
|
-
}, [rect, index, step.placement]);
|
|
630
|
+
}, [rect, index, step.placement, dir]);
|
|
530
631
|
|
|
531
632
|
return (
|
|
532
633
|
<div
|
package/src/wizard/use-wizard.ts
CHANGED
|
@@ -33,10 +33,13 @@ interface WizardState<TData> {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
type WizardAction<TData> =
|
|
36
|
-
|
|
36
|
+
// `from` is the step the move was decided ON. A move whose step is no longer the
|
|
37
|
+
// current one is stale — see `goNext` — and is dropped rather than applied relative
|
|
38
|
+
// to wherever the wizard has got to since.
|
|
39
|
+
| { type: "GO_NEXT"; from: number }
|
|
37
40
|
| { type: "GO_BACK" }
|
|
38
41
|
| { type: "GO_TO_STEP"; index: number }
|
|
39
|
-
| { type: "SKIP" }
|
|
42
|
+
| { type: "SKIP"; from: number }
|
|
40
43
|
| { type: "UPDATE_DATA"; partial: Partial<TData> }
|
|
41
44
|
| { type: "SET_SUBMITTING"; value: boolean }
|
|
42
45
|
| { type: "SET_ERROR"; error: string | null }
|
|
@@ -50,6 +53,7 @@ function createReducer<TData>(stepCount: number) {
|
|
|
50
53
|
): WizardState<TData> {
|
|
51
54
|
switch (action.type) {
|
|
52
55
|
case "GO_NEXT": {
|
|
56
|
+
if (action.from !== state.currentStepIndex) return state;
|
|
53
57
|
const completed = new Set(state.completedSteps);
|
|
54
58
|
completed.add(state.currentStepIndex);
|
|
55
59
|
const nextIndex = Math.min(state.currentStepIndex + 1, stepCount - 1);
|
|
@@ -67,6 +71,7 @@ function createReducer<TData>(stepCount: number) {
|
|
|
67
71
|
return { ...state, currentStepIndex: action.index };
|
|
68
72
|
}
|
|
69
73
|
case "SKIP": {
|
|
74
|
+
if (action.from !== state.currentStepIndex) return state;
|
|
70
75
|
const skipped = new Set(state.skippedSteps);
|
|
71
76
|
skipped.add(state.currentStepIndex);
|
|
72
77
|
const nextIndex = Math.min(state.currentStepIndex + 1, stepCount - 1);
|
|
@@ -243,11 +248,32 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
243
248
|
return true;
|
|
244
249
|
}, [state.currentStepIndex, missingRequired, onValidationFailed]);
|
|
245
250
|
|
|
251
|
+
/**
|
|
252
|
+
* One forward move at a time. A double-click on Next used to validate the CURRENT
|
|
253
|
+
* step twice and then advance twice — the second GO_NEXT landing on a step whose
|
|
254
|
+
* own validators never ran, so a required field two steps in could be walked past.
|
|
255
|
+
* Two layers, because either alone leaves a gap:
|
|
256
|
+
*
|
|
257
|
+
* - this ref refuses a second `goNext`/`skip`/`finish` while one is in flight,
|
|
258
|
+
* which covers an async validator (a server check) of any length;
|
|
259
|
+
* - the `from` on each action drops a move decided on a step that is no longer
|
|
260
|
+
* current, which covers the click that arrives after the first move resolved
|
|
261
|
+
* but before React re-rendered — its closure still holds the old step index.
|
|
262
|
+
*/
|
|
263
|
+
const advancingRef = useRef(false);
|
|
264
|
+
|
|
246
265
|
const goNext = useCallback(async () => {
|
|
247
|
-
if (
|
|
248
|
-
|
|
266
|
+
if (advancingRef.current) return;
|
|
267
|
+
advancingRef.current = true;
|
|
268
|
+
const from = state.currentStepIndex;
|
|
269
|
+
try {
|
|
270
|
+
if (await runStepValidators()) {
|
|
271
|
+
dispatch({ type: "GO_NEXT", from });
|
|
272
|
+
}
|
|
273
|
+
} finally {
|
|
274
|
+
advancingRef.current = false;
|
|
249
275
|
}
|
|
250
|
-
}, [runStepValidators]);
|
|
276
|
+
}, [runStepValidators, state.currentStepIndex]);
|
|
251
277
|
|
|
252
278
|
const goBack = useCallback(() => {
|
|
253
279
|
dispatch({ type: "GO_BACK" });
|
|
@@ -258,8 +284,11 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
258
284
|
}, []);
|
|
259
285
|
|
|
260
286
|
const skip = useCallback(() => {
|
|
261
|
-
|
|
262
|
-
|
|
287
|
+
// Not while Next is validating: the skip would land first and the pending GO_NEXT
|
|
288
|
+
// would then be stale anyway — but the user asked for one move, not a race.
|
|
289
|
+
if (advancingRef.current) return;
|
|
290
|
+
dispatch({ type: "SKIP", from: state.currentStepIndex });
|
|
291
|
+
}, [state.currentStepIndex]);
|
|
263
292
|
|
|
264
293
|
const cancel = useCallback(() => {
|
|
265
294
|
dispatch({ type: "SHOW_CANCEL_DIALOG", show: true });
|
|
@@ -288,18 +317,27 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
288
317
|
|
|
289
318
|
const finish = useCallback(async () => {
|
|
290
319
|
if (!onComplete || !canFinish) return;
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
320
|
+
// The same re-entry guard as `goNext`: a double-click on Finish would otherwise
|
|
321
|
+
// validate twice and call `onComplete` twice — `isSubmitting` is only set once
|
|
322
|
+
// validation has passed, too late to stop the second click.
|
|
323
|
+
if (advancingRef.current) return;
|
|
324
|
+
advancingRef.current = true;
|
|
296
325
|
try {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
dispatch({ type: "
|
|
326
|
+
// Validate the (possibly form-bearing) last step before submitting, matching
|
|
327
|
+
// the Next gate — review-only steps register no validators and pass through.
|
|
328
|
+
if (!(await runStepValidators())) return;
|
|
329
|
+
dispatch({ type: "SET_SUBMITTING", value: true });
|
|
330
|
+
dispatch({ type: "SET_ERROR", error: null });
|
|
331
|
+
try {
|
|
332
|
+
await onComplete(state.data);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
const message = err instanceof Error && err.message ? err.message : genericError;
|
|
335
|
+
dispatch({ type: "SET_ERROR", error: message });
|
|
336
|
+
} finally {
|
|
337
|
+
dispatch({ type: "SET_SUBMITTING", value: false });
|
|
338
|
+
}
|
|
301
339
|
} finally {
|
|
302
|
-
|
|
340
|
+
advancingRef.current = false;
|
|
303
341
|
}
|
|
304
342
|
}, [onComplete, canFinish, state.data, runStepValidators, genericError]);
|
|
305
343
|
|
|
@@ -54,7 +54,7 @@ export function WizardSummary({
|
|
|
54
54
|
)}
|
|
55
55
|
<div className="flex items-start justify-between gap-4">
|
|
56
56
|
<dt className="text-[var(--text-muted)]">{item.label}</dt>
|
|
57
|
-
<dd className="text-
|
|
57
|
+
<dd className="text-end font-medium text-[var(--text-primary)]">
|
|
58
58
|
{item.value}
|
|
59
59
|
</dd>
|
|
60
60
|
</div>
|