@bitrise/bitkit-v2 0.3.382-beta.2439 → 0.3.383
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/AGENTS.md
CHANGED
|
@@ -159,9 +159,11 @@ Bitkit components use consistent prop names across the library:
|
|
|
159
159
|
- **`BitkitTour` is a hook plus one element** — `useBitkitTour` owns the steps and the flow, `<BitkitTour>` renders the card; render it once, anywhere in the tree. Every step is a card anchored to an element, so `target` is required and there is no step `type` to choose. `open` is how a tour is opened — from a value you already have — and each step carries its own `actions`, the last one rendering as the primary button:
|
|
160
160
|
```tsx
|
|
161
161
|
const startBuild = useRef<HTMLElement>(null);
|
|
162
|
+
const [isTourOpen, setTourOpen] = useState(shouldShowTour);
|
|
162
163
|
const tour = useBitkitTour({
|
|
164
|
+
onClose: () => setTourOpen(false),
|
|
163
165
|
onStatusChange: ({ status }) => status === 'dismissed' && persistDismissal(),
|
|
164
|
-
open:
|
|
166
|
+
open: isTourOpen,
|
|
165
167
|
steps: [
|
|
166
168
|
{
|
|
167
169
|
actions: [{ action: 'dismiss', label: 'Got it' }],
|
|
@@ -176,6 +178,8 @@ Bitkit components use consistent prop names across the library:
|
|
|
176
178
|
],
|
|
177
179
|
});
|
|
178
180
|
```
|
|
181
|
+
`open` counts transitions, not the value: a tour that ends on its own — `Skip tour`, the last step, a target that never appeared — stays closed while your value is still true, and starts again on the next `false → true`. `onClose` is where you turn it false, and it fires however the tour ended, so you never have to know which of Zag's statuses mean "over"; `onStatusChange` is only for deciding whether to record it as seen.
|
|
182
|
+
|
|
179
183
|
A single-step tour drops the `n of m` label, which is what turns the card into the single-feature presentation. Clicking outside never closes it — the close button, the step's own actions and Escape do. It dims the page and cuts the highlight out of the backdrop itself, so don't add your own overlay, ring or z-index around the target.
|
|
180
184
|
- **A one-step tour announces a feature, and behaves the same wherever it's used.** It's the pattern every one of these has so far, so match it rather than inventing a variant:
|
|
181
185
|
- it appears on its own — `open` turns true once the user is eligible, and nothing has to be clicked to bring it up;
|
|
@@ -44,6 +44,14 @@ export type BitkitTourStep = Omit<TourStepDetails, 'type'> & {
|
|
|
44
44
|
target: NonNullable<TourStepDetails['target']>;
|
|
45
45
|
};
|
|
46
46
|
export interface BitkitTourOptions extends Omit<UseTourProps, 'closeOnInteractOutside' | 'onStepsChange' | 'steps'> {
|
|
47
|
+
/**
|
|
48
|
+
* Called whenever the tour ends, whichever way it ended — the close button, `Skip tour`, the last
|
|
49
|
+
* step's action, or a target that never appeared. It is the counterpart to `open`: turn your own
|
|
50
|
+
* value false here and the tour can be opened again, without having to know which of Zag's five
|
|
51
|
+
* statuses mean "over". `onStatusChange` still reports which one it was, for deciding whether to
|
|
52
|
+
* record the tour as seen.
|
|
53
|
+
*/
|
|
54
|
+
onClose?: () => void;
|
|
47
55
|
/** Refined so the callback reports the Bitkit slots too, like the rest of the api does. */
|
|
48
56
|
onStepsChange?: (details: {
|
|
49
57
|
steps: BitkitTourStep[];
|
|
@@ -52,6 +60,9 @@ export interface BitkitTourOptions extends Omit<UseTourProps, 'closeOnInteractOu
|
|
|
52
60
|
* Drives the tour from a value the caller already has — it starts when this turns true and closes
|
|
53
61
|
* when it turns false. This is how a tour is opened: it announces something, so it appears once
|
|
54
62
|
* the user is eligible for it, never in answer to the interaction it would interrupt.
|
|
63
|
+
*
|
|
64
|
+
* Only the transitions count: a tour that ends on its own — `Skip tour`, a lost target, the last
|
|
65
|
+
* step — stays closed while this is still true, and starts again on the next `false → true`.
|
|
55
66
|
*/
|
|
56
67
|
open?: boolean;
|
|
57
68
|
steps: BitkitTourStep[];
|
|
@@ -6,6 +6,8 @@ import { useCallback, useEffect, useRef } from "react";
|
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import { Portal } from "@chakra-ui/react/portal";
|
|
8
8
|
import { Tour, useTour } from "@ark-ui/react/tour";
|
|
9
|
+
import { useEnvironmentContext } from "@chakra-ui/react/environment";
|
|
10
|
+
import { isIos } from "@zag-js/dom-query";
|
|
9
11
|
import { preventBodyScroll } from "@zag-js/remove-scroll";
|
|
10
12
|
//#region lib/components/BitkitTour/BitkitTour.tsx
|
|
11
13
|
/**
|
|
@@ -25,20 +27,35 @@ import { preventBodyScroll } from "@zag-js/remove-scroll";
|
|
|
25
27
|
* dismissal is recorded for the user in `onStatusChange`, not kept in page state.
|
|
26
28
|
*/
|
|
27
29
|
var useBitkitTour = (options) => {
|
|
30
|
+
const { getDocument } = useEnvironmentContext();
|
|
31
|
+
const { onClose, ...tourOptions } = options;
|
|
32
|
+
const onCloseRef = useRef(onClose);
|
|
33
|
+
onCloseRef.current = onClose;
|
|
34
|
+
const hasClosedRef = useRef(false);
|
|
28
35
|
const tour = useTour({
|
|
29
36
|
spotlightOffset: {
|
|
30
37
|
x: 8,
|
|
31
38
|
y: 8
|
|
32
39
|
},
|
|
33
40
|
spotlightRadius: 4,
|
|
34
|
-
...
|
|
41
|
+
...tourOptions,
|
|
35
42
|
closeOnInteractOutside: false,
|
|
36
|
-
|
|
43
|
+
onStatusChange: (details) => {
|
|
44
|
+
tourOptions.onStatusChange?.(details);
|
|
45
|
+
if (details.status === "started") {
|
|
46
|
+
hasClosedRef.current = false;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (hasClosedRef.current) return;
|
|
50
|
+
hasClosedRef.current = true;
|
|
51
|
+
onCloseRef.current?.();
|
|
52
|
+
},
|
|
53
|
+
onStepsChange: tourOptions.onStepsChange,
|
|
37
54
|
translations: {
|
|
38
55
|
close: "Close tour",
|
|
39
|
-
...
|
|
56
|
+
...tourOptions.translations
|
|
40
57
|
},
|
|
41
|
-
steps:
|
|
58
|
+
steps: tourOptions.steps.map((step) => ({
|
|
42
59
|
offset: { mainAxis: 6 },
|
|
43
60
|
...step,
|
|
44
61
|
type: "tooltip"
|
|
@@ -55,21 +72,25 @@ var useBitkitTour = (options) => {
|
|
|
55
72
|
if (!tour.open || !isSingleStep) return;
|
|
56
73
|
const onDocumentClick = (event) => {
|
|
57
74
|
const target = tour.step?.target?.();
|
|
58
|
-
if (target && event.
|
|
75
|
+
if (target && event.composedPath().includes(target)) dismissRef.current();
|
|
59
76
|
};
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
const doc = getDocument();
|
|
78
|
+
doc.addEventListener("click", onDocumentClick, true);
|
|
79
|
+
return () => doc.removeEventListener("click", onDocumentClick, true);
|
|
62
80
|
}, [
|
|
81
|
+
getDocument,
|
|
63
82
|
isSingleStep,
|
|
64
83
|
tour.open,
|
|
65
84
|
tour.step?.id
|
|
66
85
|
]);
|
|
67
|
-
const { open } =
|
|
86
|
+
const { open } = tourOptions;
|
|
87
|
+
const previousOpen = useRef(void 0);
|
|
68
88
|
useEffect(() => {
|
|
69
|
-
if (open === void 0 || open ===
|
|
89
|
+
if (open === void 0 || open === previousOpen.current) return;
|
|
90
|
+
previousOpen.current = open;
|
|
70
91
|
if (open) tour.start();
|
|
71
92
|
else dismissRef.current();
|
|
72
|
-
}, [open
|
|
93
|
+
}, [open]);
|
|
73
94
|
return {
|
|
74
95
|
...tour,
|
|
75
96
|
dismiss,
|
|
@@ -124,19 +145,24 @@ var BitkitTour = ({ tour }) => {
|
|
|
124
145
|
const step = tour.step;
|
|
125
146
|
const actions = step?.actions ?? [];
|
|
126
147
|
const { open } = tour;
|
|
127
|
-
|
|
148
|
+
const { getDocument } = useEnvironmentContext();
|
|
149
|
+
useEffect(() => open && !isIos() ? preventBodyScroll(getDocument()) : void 0, [getDocument, open]);
|
|
128
150
|
const contentId = tour.getContentProps().id;
|
|
129
151
|
useEffect(() => {
|
|
130
152
|
if (!open || contentId === void 0) return;
|
|
131
|
-
const contentEl =
|
|
153
|
+
const contentEl = getDocument().getElementById(contentId);
|
|
132
154
|
if (!contentEl) return;
|
|
133
155
|
const takeFocus = () => {
|
|
134
156
|
contentEl.removeEventListener("focusin", takeFocus);
|
|
135
|
-
contentEl.focus();
|
|
157
|
+
contentEl.focus({ preventScroll: true });
|
|
136
158
|
};
|
|
137
159
|
contentEl.addEventListener("focusin", takeFocus);
|
|
138
160
|
return () => contentEl.removeEventListener("focusin", takeFocus);
|
|
139
|
-
}, [
|
|
161
|
+
}, [
|
|
162
|
+
contentId,
|
|
163
|
+
getDocument,
|
|
164
|
+
open
|
|
165
|
+
]);
|
|
140
166
|
return /* @__PURE__ */ jsx(Tour.Root, {
|
|
141
167
|
unmountOnExit: true,
|
|
142
168
|
tour,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BitkitTour.js","names":[],"sources":["../../../lib/components/BitkitTour/BitkitTour.tsx"],"sourcesContent":["import {\n Tour as ArkTour,\n type TourStepDetails,\n useTour,\n type UseTourProps,\n type UseTourReturn,\n} from '@ark-ui/react/tour';\nimport { Box } from '@chakra-ui/react/box';\nimport { Portal } from '@chakra-ui/react/portal';\nimport { chakra, useSlotRecipe } from '@chakra-ui/react/styled-system';\nimport { preventBodyScroll } from '@zag-js/remove-scroll';\nimport { type ReactNode, useCallback, useEffect, useRef } from 'react';\n\nimport BitkitButton from '../BitkitButton/BitkitButton';\nimport BitkitCloseButton from '../BitkitCloseButton/BitkitCloseButton';\n\ntype BitkitTourStepAction = NonNullable<TourStepDetails['actions']>[number];\n\n/**\n * The link-style action in the footer, e.g. `Skip tour` or `Learn more`. It either drives the tour\n * (`action`) or leaves the page (`href`) — the union makes passing both, or neither, a type error.\n */\nexport type BitkitTourSecondaryAction =\n | {\n /**\n * A tour action — `'skip'`, `'dismiss'`, `'next'`, `'prev'`, or a function that receives the\n * action map (`{ next, prev, dismiss, skip, goto }`) to do something else first.\n */\n action: NonNullable<BitkitTourStepAction['action']>;\n href?: never;\n label: string;\n }\n | {\n action?: never;\n /** Renders the action as a link out. */\n href: string;\n /** Opens the link in a new tab, with the matching `rel`. */\n isExternal?: boolean;\n label: string;\n };\n\n/**\n * A single step of the tour. On top of Ark's step details it carries the three Bitkit slots —\n * `badge`, `content` and `secondaryAction`. Zag spreads the whole step object when it normalises\n * it, so these reach `tour.step` untouched. (Zag also offers a `meta` bag for this; flat fields\n * read better at the call site, at the cost of a name clash if Zag ever adds one of these.)\n *\n * Ark's step `type` is not part of this: every step is a card anchored to an element, so the hook\n * sets it. `target` is required for the same reason — Zag turns a step without one into a centred\n * dialog, which is a presentation this component doesn't have.\n */\nexport type BitkitTourStep = Omit<TourStepDetails, 'type'> & {\n /**\n * Rendered next to the title — a `BitkitBadge` in practice, e.g. `BETA` or `NEW`. Nothing about\n * the badge is fixed here beyond its placement: the element itself comes from the caller.\n */\n badge?: ReactNode;\n /** Rendered between the description and the footer — an image, a video, a table, anything. */\n content?: ReactNode;\n secondaryAction?: BitkitTourSecondaryAction;\n /** The element the step points at. */\n target: NonNullable<TourStepDetails['target']>;\n};\n\nexport interface BitkitTourOptions extends Omit<UseTourProps, 'closeOnInteractOutside' | 'onStepsChange' | 'steps'> {\n /** Refined so the callback reports the Bitkit slots too, like the rest of the api does. */\n onStepsChange?: (details: { steps: BitkitTourStep[] }) => void;\n /**\n * Drives the tour from a value the caller already has — it starts when this turns true and closes\n * when it turns false. This is how a tour is opened: it announces something, so it appears once\n * the user is eligible for it, never in answer to the interaction it would interrupt.\n */\n open?: boolean;\n steps: BitkitTourStep[];\n}\n/** The tour api returned by `useBitkitTour` — pass it around to control the tour from anywhere. */\nexport type BitkitTourInstance = Omit<UseTourReturn, 'addStep' | 'setSteps' | 'step' | 'updateStep'> & {\n addStep: (step: BitkitTourStep) => void;\n setSteps: (steps: BitkitTourStep[]) => void;\n /** The current step, with the Bitkit slots (`badge`, `content`, `secondaryAction`) still typed on it. */\n step: BitkitTourStep | null;\n /** Closes the tour, the same way its close button does. */\n dismiss: () => void;\n updateStep: (id: string, stepOverrides: Partial<BitkitTourStep>) => void;\n};\nexport type BitkitTourProps = {\n /** The tour instance returned by `useBitkitTour`. */\n tour: BitkitTourInstance;\n};\n\n/**\n * Creates the tour instance that drives `BitkitTour`. Takes the steps plus the tour-level options\n * (`onStepChange`, `onStatusChange`, `closeOnEscape`, `preventInteraction`, `translations`, …) and\n * returns the api that reports and navigates it: `step`, `stepIndex`, `totalSteps`, `next()`,\n * `prev()`, `setStep()`. Closing on a click outside isn't among them — see below.\n *\n * A one-step tour announces a feature, and behaves the same wherever it's used:\n *\n * - it appears on its own — `open` turns true once the user is eligible, and nothing has to be\n * clicked to bring it up;\n * - dismissing it closes it, and it doesn't come back;\n * - acting on the control it points at closes it too, reported as a dismissal like any other, so\n * the target keeps doing whatever it did before;\n * - until one of those two happens, a reload or a new session brings it back — which means the\n * dismissal is recorded for the user in `onStatusChange`, not kept in page state.\n */\nexport const useBitkitTour = (options: BitkitTourOptions): BitkitTourInstance => {\n const tour = useTour({\n // The highlight sits 8 around the target, and the card 12 from the highlight — the 8 the arrow\n // takes plus the 4 gap the design asks for. Both are measured from the highlight, since Zag\n // grows the anchor rect by `spotlightOffset` before positioning. `offset.mainAxis` replaces\n // Zag's own gutter, and Popper adds half the arrow's box on top of it: 6 + 5.65 ≈ the 12.\n spotlightOffset: { x: 8, y: 8 },\n // 4 is also Zag's current default, but the design asks for it and a third-party default is not\n // ours to rely on — the z-index contract already moved under us once between two minors.\n spotlightRadius: 4,\n ...options,\n // Not part of the options, and set after the spread so it can't be turned back on: a tour is a\n // guided flow with its own ways out — the close button, `Skip tour`, `Done` — and Zag would\n // otherwise end it on any click outside the card, losing the whole thing to a stray click on\n // the dimmed page. Escape is a deliberate keystroke rather than a stray click, so it keeps\n // closing the tour by default — and unlike this one, `closeOnEscape` stays in the options for\n // callers who want it off.\n closeOnInteractOutside: false,\n // Same reason as the cast on `Tour.Root` below: our step type isn't a supertype of Ark's, so a\n // callback taking ours doesn't match the signature Ark declares. The payload is Ark's.\n onStepsChange: options.onStepsChange as UseTourProps['onStepsChange'],\n // Zag's default is `close tour`, and this one is visible rather than screen-reader only: the\n // close button shows its own aria-label in a tooltip. The rest of Zag's translations\n // (`nextStep`, `prevStep`, `skip`) stay as they are — those only ever reach assistive tech.\n translations: { close: 'Close tour', ...options.translations },\n // `type` last: there is one presentation, so it isn't the caller's to pick.\n steps: options.steps.map((step) => ({ offset: { mainAxis: 6 }, ...step, type: 'tooltip' as const })),\n });\n\n // Zag's api can start a tour but not stop one. Its close trigger's click handler *is* the\n // machine's dismiss, so this borrows that instead of reaching into the machine. The cast says\n // what the handler is rather than manufacturing an event for it: `actionMap.dismiss` takes none.\n const dismiss = useCallback(() => {\n const closeTriggerClick = tour.getCloseTriggerProps().onClick as (() => void) | undefined;\n\n closeTriggerClick?.();\n }, [tour]);\n\n // A single-step tour points at one control, and operating that control is the same\n // acknowledgement its close button is — there is no next step to go to, so the tour is done. Zag\n // leaves the target clickable on purpose (it excludes it from interact-outside), so without this\n // every caller has to wire the dismissal into the target's own handler, and the ones that forget\n // show the tour again on the user's next visit. Multi-step tours are left alone: there, clicking\n // the highlighted control could as easily mean \"go on\" as \"I'm done\", which isn't ours to guess.\n //\n // The listener is on the document rather than the target, resolving the target when the click\n // happens: the target can re-render while the tour is open, and capture runs even when the\n // caller's own handler stops propagation. `click` rather than `pointerdown` — a right-click or an\n // abandoned drag isn't acting on it — and it still runs before a link's navigation, so a caller\n // that records the dismissal gets its request away.\n // `dismiss` closes over `tour`, which is a new object on every render, so listing it would put the\n // effects below on every render as well — attaching and detaching the listener for nothing.\n const dismissRef = useRef(dismiss);\n dismissRef.current = dismiss;\n\n const isSingleStep = options.steps.length === 1;\n useEffect(() => {\n if (!tour.open || !isSingleStep) {\n return undefined;\n }\n\n const onDocumentClick = (event: MouseEvent) => {\n const target = tour.step?.target?.();\n\n if (target && event.target instanceof Node && target.contains(event.target)) {\n dismissRef.current();\n }\n };\n\n document.addEventListener('click', onDocumentClick, true);\n\n return () => document.removeEventListener('click', onDocumentClick, true);\n // `tour` is a new object on every render; the machine's own state is what matters here.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isSingleStep, tour.open, tour.step?.id]);\n\n const { open } = options;\n useEffect(() => {\n if (open === undefined || open === tour.open) {\n return;\n }\n\n if (open) {\n tour.start();\n } else {\n dismissRef.current();\n }\n // `tour` is a new object on every render, so the machine's own `open` is the dependency that\n // matters here.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, tour.open]);\n\n return { ...tour, dismiss, step: tour.step as BitkitTourStep | null };\n};\n\n// The Ark parts aren't Chakra components, so they're wrapped with the `chakra()` factory to accept\n// `css`. They're deliberately NOT wrapped with `asChild`: each Ark part falls back to the current\n// step's content (title, description, progress text, action label) only when it renders its own\n// children, and `asChild` would replace those children with the wrapper element.\nconst TourBackdrop = chakra(ArkTour.Backdrop);\nconst TourSpotlight = chakra(ArkTour.Spotlight);\nconst TourPositioner = chakra(ArkTour.Positioner);\nconst TourContent = chakra(ArkTour.Content);\nconst TourArrow = chakra(ArkTour.Arrow);\nconst TourArrowTip = chakra(ArkTour.ArrowTip);\nconst TourTitle = chakra(ArkTour.Title);\nconst TourDescription = chakra(ArkTour.Description);\nconst TourControl = chakra(ArkTour.Control);\nconst TourProgressText = chakra(ArkTour.ProgressText);\n\nconst SecondaryAction = ({ secondaryAction }: { secondaryAction: BitkitTourSecondaryAction }) => {\n const { action, href, label } = secondaryAction;\n\n if (href !== undefined) {\n return (\n <BitkitButton href={href} isExternal={secondaryAction.isExternal} size=\"sm\" variant=\"tertiary\">\n {label}\n </BitkitButton>\n );\n }\n\n if (!action) return null;\n\n return (\n <ArkTour.ActionTrigger asChild action={{ action, label }} aria-label={label}>\n <BitkitButton size=\"sm\" variant=\"tertiary\">\n {label}\n </BitkitButton>\n </ArkTour.ActionTrigger>\n );\n};\n\n/**\n * A guided product tour: a step card anchored to an element on the page, with a spotlight cut out\n * of the dimmed backdrop around that element.\n *\n * The steps and the tour behaviour live on the instance — create it with `useBitkitTour` and render\n * `BitkitTour` once, anywhere in the tree. Each step declares its own action buttons; the last one\n * is rendered as the primary button. A single-step tour drops the `1 of n` label, which is what\n * turns the card into the single-feature presentation.\n */\nconst BitkitTour = ({ tour }: BitkitTourProps) => {\n const recipe = useSlotRecipe({ key: 'tour' });\n const styles = recipe();\n const step = tour.step;\n const actions = step?.actions ?? [];\n\n // Zag traps focus with fixed options, so it lands on the card's first tabbable — the close button,\n // which means a screen reader opens the tour on `Close tour` instead of on what the step says. The\n // card is what should take it: Zag gives the content `role=\"alertdialog\"`, `tabIndex: -1` and the\n // `aria-labelledby`/`aria-describedby` pair exactly so it can be focused and read out. Measured\n // alternatives that don't work: making the card tabbable (the trap picks a descendant either way)\n // and a ref (it doesn't survive the `chakra()`-wrapped Ark part). This corrects the trap's choice\n // instead of trying to beat it — the trap activates asynchronously too, and racing it with a frame\n // loses whenever the first paint is slow. The listener takes the first focus that lands anywhere\n // in the card, moves it to the card itself and then steps out of the way, so everything after it\n // (Tab, the buttons, a click) behaves normally. It arms on open and not per step, so stepping on\n // keeps the focus on the button that did it, rather than pulling the next keystroke back to the\n // card. The lookup goes through the machine's own content id, so two mounted tours can't reach\n // each other's card.\n //\n // The tour machine is the one Zag component that can't be told where to put focus: `dialog` takes\n // an `initialFocusEl` and hands it to the same trap, and even defaults an `alertdialog` to its\n // close trigger deliberately. Once the tour has that prop too, this effect goes away.\n const { open } = tour;\n\n // The tour machine has no scroll handling of its own — no `preventScroll` prop, no listener —\n // while Zag's dialog machine locks the body through this same helper. The tour needs it just as\n // much: everything it paints is positioned in document coordinates from a target it resolves once,\n // when the step opens, and never looks up again. Let the page scroll and any layout that swaps its\n // elements around — a sticky header that moves its buttons into a portal is the common one —\n // leaves the machine anchored to a node that is no longer in the document. A detached node\n // measures zero, so the card, the spotlight and the backdrop's cut-out all collapse into the\n // top-left corner of the page. Locking is the honest reading of the presentation anyway: the tour\n // dims the page and takes focus, exactly like a modal dialog.\n useEffect(() => (open ? preventBodyScroll() : undefined), [open]);\n\n const contentId = tour.getContentProps().id;\n useEffect(() => {\n if (!open || contentId === undefined) {\n return undefined;\n }\n\n const contentEl = document.getElementById(contentId);\n if (!contentEl) {\n return undefined;\n }\n\n const takeFocus = () => {\n contentEl.removeEventListener('focusin', takeFocus);\n contentEl.focus();\n };\n\n contentEl.addEventListener('focusin', takeFocus);\n\n return () => contentEl.removeEventListener('focusin', takeFocus);\n }, [contentId, open]);\n\n return (\n // `unmountOnExit` isn't only tidiness: Ark's backdrop applies `hidden: !tour.step?.backdrop`\n // after merging the presence props, which overrides the presence's own `hidden` — and since the\n // step is kept after the tour closes, the dimming would stay on screen for good. Unmounting on\n // exit takes the node out before that can happen.\n // The cast is the price of refining `step` and the step-mutating methods on the instance: our\n // step type isn't a supertype of Ark's, so the methods no longer match its api signature. The\n // object is Ark's api — only the types are ours.\n <ArkTour.Root unmountOnExit tour={tour as UseTourReturn}>\n <Portal>\n <TourBackdrop css={styles.backdrop} />\n <TourSpotlight css={styles.spotlight} />\n <TourPositioner css={styles.positioner}>\n <TourContent css={styles.content}>\n {/* `hidden={false}` overrides Zag, which hides the arrow until the card has been\n positioned — while Popper computes that position from the arrow's `clientHeight`, so\n a hidden arrow measures 0 and the card lands 6 short of the design's 12, then drops\n by that 6 as soon as anything recomputes. Nothing shows in the meantime: until it is\n positioned the card itself sits off-screen at `-100vh`. */}\n <TourArrow css={styles.arrow} hidden={false}>\n <TourArrowTip css={styles.arrowTip} />\n </TourArrow>\n <Box css={styles.header}>\n <Box css={styles.titleGroup}>\n {tour.totalSteps > 1 ? <TourProgressText css={styles.progressText} /> : null}\n <Box css={styles.titleRow}>\n <TourTitle css={styles.title} />\n {step?.badge ? <Box css={styles.badge}>{step.badge}</Box> : null}\n </Box>\n </Box>\n <ArkTour.CloseTrigger asChild>\n <BitkitCloseButton css={styles.closeTrigger} size=\"xs\" />\n </ArkTour.CloseTrigger>\n </Box>\n <TourDescription css={styles.description} />\n {step?.content ? <Box css={styles.media}>{step.content}</Box> : null}\n <TourControl css={styles.control}>\n {step?.secondaryAction ? <SecondaryAction secondaryAction={step.secondaryAction} /> : null}\n <Box css={styles.actions}>\n {actions.map((action, index) => (\n // Zag labels the trigger from its own translations (`close tour`, `next step`),\n // which would override the visible text as the accessible name — and put two\n // `Close tour` buttons on a card whose dismiss action reads `Got it`.\n <ArkTour.ActionTrigger key={action.label} asChild action={action} aria-label={action.label}>\n <BitkitButton size=\"sm\" variant={index === actions.length - 1 ? 'primary' : 'secondary'}>\n {action.label}\n </BitkitButton>\n </ArkTour.ActionTrigger>\n ))}\n </Box>\n </TourControl>\n </TourContent>\n </TourPositioner>\n </Portal>\n </ArkTour.Root>\n );\n};\n\nBitkitTour.displayName = 'BitkitTour';\n\nexport default BitkitTour;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0GA,IAAa,iBAAiB,YAAmD;CAC/E,MAAM,OAAO,QAAQ;EAKnB,iBAAiB;GAAE,GAAG;GAAG,GAAG;EAAE;EAG9B,iBAAiB;EACjB,GAAG;EAOH,wBAAwB;EAGxB,eAAe,QAAQ;EAIvB,cAAc;GAAE,OAAO;GAAc,GAAG,QAAQ;EAAa;EAE7D,OAAO,QAAQ,MAAM,KAAK,UAAU;GAAE,QAAQ,EAAE,UAAU,EAAE;GAAG,GAAG;GAAM,MAAM;EAAmB,EAAE;CACrG,CAAC;CAKD,MAAM,UAAU,kBAAkB;EAChC,MAAM,oBAAoB,KAAK,qBAAqB,CAAC,CAAC;EAEtD,oBAAoB;CACtB,GAAG,CAAC,IAAI,CAAC;CAgBT,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CAErB,MAAM,eAAe,QAAQ,MAAM,WAAW;CAC9C,gBAAgB;EACd,IAAI,CAAC,KAAK,QAAQ,CAAC,cACjB;EAGF,MAAM,mBAAmB,UAAsB;GAC7C,MAAM,SAAS,KAAK,MAAM,SAAS;GAEnC,IAAI,UAAU,MAAM,kBAAkB,QAAQ,OAAO,SAAS,MAAM,MAAM,GACxE,WAAW,QAAQ;EAEvB;EAEA,SAAS,iBAAiB,SAAS,iBAAiB,IAAI;EAExD,aAAa,SAAS,oBAAoB,SAAS,iBAAiB,IAAI;CAG1E,GAAG;EAAC;EAAc,KAAK;EAAM,KAAK,MAAM;CAAE,CAAC;CAE3C,MAAM,EAAE,SAAS;CACjB,gBAAgB;EACd,IAAI,SAAS,KAAA,KAAa,SAAS,KAAK,MACtC;EAGF,IAAI,MACF,KAAK,MAAM;OAEX,WAAW,QAAQ;CAKvB,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC;CAEpB,OAAO;EAAE,GAAG;EAAM;EAAS,MAAM,KAAK;CAA8B;AACtE;AAMA,IAAM,eAAe,OAAO,KAAQ,QAAQ;AAC5C,IAAM,gBAAgB,OAAO,KAAQ,SAAS;AAC9C,IAAM,iBAAiB,OAAO,KAAQ,UAAU;AAChD,IAAM,cAAc,OAAO,KAAQ,OAAO;AAC1C,IAAM,YAAY,OAAO,KAAQ,KAAK;AACtC,IAAM,eAAe,OAAO,KAAQ,QAAQ;AAC5C,IAAM,YAAY,OAAO,KAAQ,KAAK;AACtC,IAAM,kBAAkB,OAAO,KAAQ,WAAW;AAClD,IAAM,cAAc,OAAO,KAAQ,OAAO;AAC1C,IAAM,mBAAmB,OAAO,KAAQ,YAAY;AAEpD,IAAM,mBAAmB,EAAE,sBAAsE;CAC/F,MAAM,EAAE,QAAQ,MAAM,UAAU;CAEhC,IAAI,SAAS,KAAA,GACX,OACE,oBAAC,cAAD;EAAoB;EAAM,YAAY,gBAAgB;EAAY,MAAK;EAAK,SAAQ;EACjF,UAAA;CACW,CAAA;CAIlB,IAAI,CAAC,QAAQ,OAAO;CAEpB,OACE,oBAAC,KAAQ,eAAT;EAAuB,SAAA;EAAQ,QAAQ;GAAE;GAAQ;EAAM;EAAG,cAAY;EACpE,UAAA,oBAAC,cAAD;GAAc,MAAK;GAAK,SAAQ;GAC7B,UAAA;EACW,CAAA;CACO,CAAA;AAE3B;;;;;;;;;;AAWA,IAAM,cAAc,EAAE,WAA4B;CAEhD,MAAM,SADS,cAAc,EAAE,KAAK,OAAO,CAC5B,CAAA,CAAO;CACtB,MAAM,OAAO,KAAK;CAClB,MAAM,UAAU,MAAM,WAAW,CAAC;CAmBlC,MAAM,EAAE,SAAS;CAWjB,gBAAiB,OAAO,kBAAkB,IAAI,KAAA,GAAY,CAAC,IAAI,CAAC;CAEhE,MAAM,YAAY,KAAK,gBAAgB,CAAC,CAAC;CACzC,gBAAgB;EACd,IAAI,CAAC,QAAQ,cAAc,KAAA,GACzB;EAGF,MAAM,YAAY,SAAS,eAAe,SAAS;EACnD,IAAI,CAAC,WACH;EAGF,MAAM,kBAAkB;GACtB,UAAU,oBAAoB,WAAW,SAAS;GAClD,UAAU,MAAM;EAClB;EAEA,UAAU,iBAAiB,WAAW,SAAS;EAE/C,aAAa,UAAU,oBAAoB,WAAW,SAAS;CACjE,GAAG,CAAC,WAAW,IAAI,CAAC;CAEpB,OAQE,oBAAC,KAAQ,MAAT;EAAc,eAAA;EAAoB;EAChC,UAAA,qBAAC,QAAD,EAAA,UAAA;GACE,oBAAC,cAAD,EAAc,KAAK,OAAO,SAAW,CAAA;GACrC,oBAAC,eAAD,EAAe,KAAK,OAAO,UAAY,CAAA;GACvC,oBAAC,gBAAD;IAAgB,KAAK,OAAO;IAC1B,UAAA,qBAAC,aAAD;KAAa,KAAK,OAAO;KAAzB,UAAA;MAME,oBAAC,WAAD;OAAW,KAAK,OAAO;OAAO,QAAQ;OACpC,UAAA,oBAAC,cAAD,EAAc,KAAK,OAAO,SAAW,CAAA;MAC5B,CAAA;MACX,qBAAC,KAAD;OAAK,KAAK,OAAO;OAAjB,UAAA,CACE,qBAAC,KAAD;QAAK,KAAK,OAAO;QAAjB,UAAA,CACG,KAAK,aAAa,IAAI,oBAAC,kBAAD,EAAkB,KAAK,OAAO,aAAe,CAAA,IAAI,MACxE,qBAAC,KAAD;SAAK,KAAK,OAAO;SAAjB,UAAA,CACE,oBAAC,WAAD,EAAW,KAAK,OAAO,MAAQ,CAAA,GAC9B,MAAM,QAAQ,oBAAC,KAAD;UAAK,KAAK,OAAO;UAAQ,UAAA,KAAK;SAAW,CAAA,IAAI,IACzD;QACF,CAAA,CAAA;OACL,CAAA,GAAA,oBAAC,KAAQ,cAAT;QAAsB,SAAA;QACpB,UAAA,oBAAC,mBAAD;SAAmB,KAAK,OAAO;SAAc,MAAK;QAAM,CAAA;OACpC,CAAA,CACnB;;MACL,oBAAC,iBAAD,EAAiB,KAAK,OAAO,YAAc,CAAA;MAC1C,MAAM,UAAU,oBAAC,KAAD;OAAK,KAAK,OAAO;OAAQ,UAAA,KAAK;MAAa,CAAA,IAAI;MAChE,qBAAC,aAAD;OAAa,KAAK,OAAO;OAAzB,UAAA,CACG,MAAM,kBAAkB,oBAAC,iBAAD,EAAiB,iBAAiB,KAAK,gBAAkB,CAAA,IAAI,MACtF,oBAAC,KAAD;QAAK,KAAK,OAAO;QACd,UAAA,QAAQ,KAAK,QAAQ,UAIpB,oBAAC,KAAQ,eAAT;SAA0C,SAAA;SAAgB;SAAQ,cAAY,OAAO;SACnF,UAAA,oBAAC,cAAD;UAAc,MAAK;UAAK,SAAS,UAAU,QAAQ,SAAS,IAAI,YAAY;UACzE,UAAA,OAAO;SACI,CAAA;QACO,GAJK,OAAO,KAIZ,CACxB;OACE,CAAA,CACM;;KACF;;GACC,CAAA;EACV,EAAA,CAAA;CACI,CAAA;AAElB;AAEA,WAAW,cAAc"}
|
|
1
|
+
{"version":3,"file":"BitkitTour.js","names":[],"sources":["../../../lib/components/BitkitTour/BitkitTour.tsx"],"sourcesContent":["import {\n Tour as ArkTour,\n type TourStepDetails,\n useTour,\n type UseTourProps,\n type UseTourReturn,\n} from '@ark-ui/react/tour';\nimport { Box } from '@chakra-ui/react/box';\nimport { useEnvironmentContext } from '@chakra-ui/react/environment';\nimport { Portal } from '@chakra-ui/react/portal';\nimport { chakra, useSlotRecipe } from '@chakra-ui/react/styled-system';\nimport { isIos } from '@zag-js/dom-query';\nimport { preventBodyScroll } from '@zag-js/remove-scroll';\nimport { type ReactNode, useCallback, useEffect, useRef } from 'react';\n\nimport BitkitButton from '../BitkitButton/BitkitButton';\nimport BitkitCloseButton from '../BitkitCloseButton/BitkitCloseButton';\n\ntype BitkitTourStepAction = NonNullable<TourStepDetails['actions']>[number];\n\n/**\n * The link-style action in the footer, e.g. `Skip tour` or `Learn more`. It either drives the tour\n * (`action`) or leaves the page (`href`) — the union makes passing both, or neither, a type error.\n */\nexport type BitkitTourSecondaryAction =\n | {\n /**\n * A tour action — `'skip'`, `'dismiss'`, `'next'`, `'prev'`, or a function that receives the\n * action map (`{ next, prev, dismiss, skip, goto }`) to do something else first.\n */\n action: NonNullable<BitkitTourStepAction['action']>;\n href?: never;\n label: string;\n }\n | {\n action?: never;\n /** Renders the action as a link out. */\n href: string;\n /** Opens the link in a new tab, with the matching `rel`. */\n isExternal?: boolean;\n label: string;\n };\n\n/**\n * A single step of the tour. On top of Ark's step details it carries the three Bitkit slots —\n * `badge`, `content` and `secondaryAction`. Zag spreads the whole step object when it normalises\n * it, so these reach `tour.step` untouched. (Zag also offers a `meta` bag for this; flat fields\n * read better at the call site, at the cost of a name clash if Zag ever adds one of these.)\n *\n * Ark's step `type` is not part of this: every step is a card anchored to an element, so the hook\n * sets it. `target` is required for the same reason — Zag turns a step without one into a centred\n * dialog, which is a presentation this component doesn't have.\n */\nexport type BitkitTourStep = Omit<TourStepDetails, 'type'> & {\n /**\n * Rendered next to the title — a `BitkitBadge` in practice, e.g. `BETA` or `NEW`. Nothing about\n * the badge is fixed here beyond its placement: the element itself comes from the caller.\n */\n badge?: ReactNode;\n /** Rendered between the description and the footer — an image, a video, a table, anything. */\n content?: ReactNode;\n secondaryAction?: BitkitTourSecondaryAction;\n /** The element the step points at. */\n target: NonNullable<TourStepDetails['target']>;\n};\n\nexport interface BitkitTourOptions extends Omit<UseTourProps, 'closeOnInteractOutside' | 'onStepsChange' | 'steps'> {\n /**\n * Called whenever the tour ends, whichever way it ended — the close button, `Skip tour`, the last\n * step's action, or a target that never appeared. It is the counterpart to `open`: turn your own\n * value false here and the tour can be opened again, without having to know which of Zag's five\n * statuses mean \"over\". `onStatusChange` still reports which one it was, for deciding whether to\n * record the tour as seen.\n */\n onClose?: () => void;\n /** Refined so the callback reports the Bitkit slots too, like the rest of the api does. */\n onStepsChange?: (details: { steps: BitkitTourStep[] }) => void;\n /**\n * Drives the tour from a value the caller already has — it starts when this turns true and closes\n * when it turns false. This is how a tour is opened: it announces something, so it appears once\n * the user is eligible for it, never in answer to the interaction it would interrupt.\n *\n * Only the transitions count: a tour that ends on its own — `Skip tour`, a lost target, the last\n * step — stays closed while this is still true, and starts again on the next `false → true`.\n */\n open?: boolean;\n steps: BitkitTourStep[];\n}\n/** The tour api returned by `useBitkitTour` — pass it around to control the tour from anywhere. */\nexport type BitkitTourInstance = Omit<UseTourReturn, 'addStep' | 'setSteps' | 'step' | 'updateStep'> & {\n addStep: (step: BitkitTourStep) => void;\n setSteps: (steps: BitkitTourStep[]) => void;\n /** The current step, with the Bitkit slots (`badge`, `content`, `secondaryAction`) still typed on it. */\n step: BitkitTourStep | null;\n /** Closes the tour, the same way its close button does. */\n dismiss: () => void;\n updateStep: (id: string, stepOverrides: Partial<BitkitTourStep>) => void;\n};\nexport type BitkitTourProps = {\n /** The tour instance returned by `useBitkitTour`. */\n tour: BitkitTourInstance;\n};\n\n/**\n * Creates the tour instance that drives `BitkitTour`. Takes the steps plus the tour-level options\n * (`onStepChange`, `onStatusChange`, `closeOnEscape`, `preventInteraction`, `translations`, …) and\n * returns the api that reports and navigates it: `step`, `stepIndex`, `totalSteps`, `next()`,\n * `prev()`, `setStep()`. Closing on a click outside isn't among them — see below.\n *\n * A one-step tour announces a feature, and behaves the same wherever it's used:\n *\n * - it appears on its own — `open` turns true once the user is eligible, and nothing has to be\n * clicked to bring it up;\n * - dismissing it closes it, and it doesn't come back;\n * - acting on the control it points at closes it too, reported as a dismissal like any other, so\n * the target keeps doing whatever it did before;\n * - until one of those two happens, a reload or a new session brings it back — which means the\n * dismissal is recorded for the user in `onStatusChange`, not kept in page state.\n */\nexport const useBitkitTour = (options: BitkitTourOptions): BitkitTourInstance => {\n // The document the tour lives in — the global one unless a consumer renders under an\n // `EnvironmentProvider`, e.g. into an iframe. Same route Zag's own machines take.\n const { getDocument } = useEnvironmentContext();\n\n // `onClose` is ours, not Zag's, so it is kept out of what reaches the machine. The ref keeps an\n // inline callback from giving the machine a new `onStatusChange` on every render.\n const { onClose, ...tourOptions } = options;\n const onCloseRef = useRef(onClose);\n onCloseRef.current = onClose;\n // Zag reports the end of a tour more than once — a dismissal on the last step emits `dismissed`\n // and then `completed` — so the run, not the status, is what `onClose` is tied to. Guarding on the\n // status names instead would encode which pairs the machine currently emits together.\n const hasClosedRef = useRef(false);\n\n const tour = useTour({\n // The highlight sits 8 around the target, and the card 12 from the highlight — the 8 the arrow\n // takes plus the 4 gap the design asks for. Both are measured from the highlight, since Zag\n // grows the anchor rect by `spotlightOffset` before positioning. `offset.mainAxis` replaces\n // Zag's own gutter, and Popper adds half the arrow's box on top of it: 6 + 5.65 ≈ the 12.\n spotlightOffset: { x: 8, y: 8 },\n // 4 is also Zag's current default, but the design asks for it and a third-party default is not\n // ours to rely on — the z-index contract already moved under us once between two minors.\n spotlightRadius: 4,\n ...tourOptions,\n // Not part of the options, and set after the spread so it can't be turned back on: a tour is a\n // guided flow with its own ways out — the close button, `Skip tour`, `Done` — and Zag would\n // otherwise end it on any click outside the card, losing the whole thing to a stray click on\n // the dimmed page. Escape is a deliberate keystroke rather than a stray click, so it keeps\n // closing the tour by default — and unlike this one, `closeOnEscape` stays in the options for\n // callers who want it off.\n closeOnInteractOutside: false,\n // Every status other than `started` is the tour ending, and `onClose` is what saves callers from\n // knowing that. The caller's own `onStatusChange` runs first and unchanged — it reports every\n // status, including the ones `onClose` collapses.\n onStatusChange: (details) => {\n tourOptions.onStatusChange?.(details);\n\n if (details.status === 'started') {\n hasClosedRef.current = false;\n\n return;\n }\n\n if (hasClosedRef.current) {\n return;\n }\n\n hasClosedRef.current = true;\n onCloseRef.current?.();\n },\n // Same reason as the cast on `Tour.Root` below: our step type isn't a supertype of Ark's, so a\n // callback taking ours doesn't match the signature Ark declares. The payload is Ark's.\n onStepsChange: tourOptions.onStepsChange as UseTourProps['onStepsChange'],\n // Zag's default is `close tour`, and this one is visible rather than screen-reader only: the\n // close button shows its own aria-label in a tooltip. The rest of Zag's translations\n // (`nextStep`, `prevStep`, `skip`) stay as they are — those only ever reach assistive tech.\n translations: { close: 'Close tour', ...tourOptions.translations },\n // `type` last: there is one presentation, so it isn't the caller's to pick.\n steps: tourOptions.steps.map((step) => ({ offset: { mainAxis: 6 }, ...step, type: 'tooltip' as const })),\n });\n\n // Zag's api can start a tour but not stop one. Its close trigger's click handler *is* the\n // machine's dismiss, so this borrows that instead of reaching into the machine. The cast says\n // what the handler is rather than manufacturing an event for it: `actionMap.dismiss` takes none.\n const dismiss = useCallback(() => {\n const closeTriggerClick = tour.getCloseTriggerProps().onClick as (() => void) | undefined;\n\n closeTriggerClick?.();\n }, [tour]);\n\n // A single-step tour points at one control, and operating that control is the same\n // acknowledgement its close button is — there is no next step to go to, so the tour is done. Zag\n // leaves the target clickable on purpose (it excludes it from interact-outside), so without this\n // every caller has to wire the dismissal into the target's own handler, and the ones that forget\n // show the tour again on the user's next visit. Multi-step tours are left alone: there, clicking\n // the highlighted control could as easily mean \"go on\" as \"I'm done\", which isn't ours to guess.\n //\n // The listener is on the document rather than the target, resolving the target when the click\n // happens: the target can re-render while the tour is open, and capture runs even when the\n // caller's own handler stops propagation. `click` rather than `pointerdown` — a right-click or an\n // abandoned drag isn't acting on it — and it still runs before a link's navigation, so a caller\n // that records the dismissal gets its request away.\n // `dismiss` closes over `tour`, which is a new object on every render, so listing it would put the\n // effects below on every render as well — attaching and detaching the listener for nothing.\n const dismissRef = useRef(dismiss);\n dismissRef.current = dismiss;\n\n const isSingleStep = options.steps.length === 1;\n useEffect(() => {\n if (!tour.open || !isSingleStep) {\n return undefined;\n }\n\n const onDocumentClick = (event: MouseEvent) => {\n const target = tour.step?.target?.();\n\n // The event path rather than `target.contains(event.target)`: under an `EnvironmentProvider`\n // the click comes from another realm, where a node is not an instance of this one's `Node`, so\n // narrowing it that way is always false — and the path crosses shadow boundaries as well.\n if (target && event.composedPath().includes(target)) {\n dismissRef.current();\n }\n };\n\n const doc = getDocument();\n\n doc.addEventListener('click', onDocumentClick, true);\n\n return () => doc.removeEventListener('click', onDocumentClick, true);\n // `tour` is a new object on every render; the machine's own state is what matters here.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [getDocument, isSingleStep, tour.open, tour.step?.id]);\n\n // Edge-triggered, on the prop's own `false → true`: comparing it against `tour.open` instead made\n // every close the machine performs for its own reasons look like a caller who wants the tour open,\n // and started it again. Zag reports five statuses and callers reasonably handle `dismissed` only,\n // so `Skip tour` (`skipped`) and a target that goes missing (`not-found`) both closed the tour and\n // had it come straight back at step one. `undefined` as the initial value so an `open` that is\n // already true on mount still starts the tour.\n const { open } = tourOptions;\n const previousOpen = useRef<boolean | undefined>(undefined);\n useEffect(() => {\n if (open === undefined || open === previousOpen.current) {\n return;\n }\n\n previousOpen.current = open;\n\n if (open) {\n tour.start();\n } else {\n dismissRef.current();\n }\n // `tour` is a new object on every render, and reacting to the machine's own `open` is exactly\n // what this effect must not do.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n return { ...tour, dismiss, step: tour.step as BitkitTourStep | null };\n};\n\n// The Ark parts aren't Chakra components, so they're wrapped with the `chakra()` factory to accept\n// `css`. They're deliberately NOT wrapped with `asChild`: each Ark part falls back to the current\n// step's content (title, description, progress text, action label) only when it renders its own\n// children, and `asChild` would replace those children with the wrapper element.\nconst TourBackdrop = chakra(ArkTour.Backdrop);\nconst TourSpotlight = chakra(ArkTour.Spotlight);\nconst TourPositioner = chakra(ArkTour.Positioner);\nconst TourContent = chakra(ArkTour.Content);\nconst TourArrow = chakra(ArkTour.Arrow);\nconst TourArrowTip = chakra(ArkTour.ArrowTip);\nconst TourTitle = chakra(ArkTour.Title);\nconst TourDescription = chakra(ArkTour.Description);\nconst TourControl = chakra(ArkTour.Control);\nconst TourProgressText = chakra(ArkTour.ProgressText);\n\nconst SecondaryAction = ({ secondaryAction }: { secondaryAction: BitkitTourSecondaryAction }) => {\n const { action, href, label } = secondaryAction;\n\n if (href !== undefined) {\n return (\n <BitkitButton href={href} isExternal={secondaryAction.isExternal} size=\"sm\" variant=\"tertiary\">\n {label}\n </BitkitButton>\n );\n }\n\n if (!action) return null;\n\n return (\n <ArkTour.ActionTrigger asChild action={{ action, label }} aria-label={label}>\n <BitkitButton size=\"sm\" variant=\"tertiary\">\n {label}\n </BitkitButton>\n </ArkTour.ActionTrigger>\n );\n};\n\n/**\n * A guided product tour: a step card anchored to an element on the page, with a spotlight cut out\n * of the dimmed backdrop around that element.\n *\n * The steps and the tour behaviour live on the instance — create it with `useBitkitTour` and render\n * `BitkitTour` once, anywhere in the tree. Each step declares its own action buttons; the last one\n * is rendered as the primary button. A single-step tour drops the `1 of n` label, which is what\n * turns the card into the single-feature presentation.\n */\nconst BitkitTour = ({ tour }: BitkitTourProps) => {\n const recipe = useSlotRecipe({ key: 'tour' });\n const styles = recipe();\n const step = tour.step;\n const actions = step?.actions ?? [];\n\n // Zag traps focus with fixed options, so it lands on the card's first tabbable — the close button,\n // which means a screen reader opens the tour on `Close tour` instead of on what the step says. The\n // card is what should take it: Zag gives the content `role=\"alertdialog\"`, `tabIndex: -1` and the\n // `aria-labelledby`/`aria-describedby` pair exactly so it can be focused and read out. Measured\n // alternatives that don't work: making the card tabbable (the trap picks a descendant either way)\n // and a ref (it doesn't survive the `chakra()`-wrapped Ark part). This corrects the trap's choice\n // instead of trying to beat it — the trap activates asynchronously too, and racing it with a frame\n // loses whenever the first paint is slow. The listener takes the first focus that lands anywhere\n // in the card, moves it to the card itself and then steps out of the way, so everything after it\n // (Tab, the buttons, a click) behaves normally. It arms on open and not per step, so stepping on\n // keeps the focus on the button that did it, rather than pulling the next keystroke back to the\n // card. The lookup goes through the machine's own content id, so two mounted tours can't reach\n // each other's card.\n //\n // The tour machine is the one Zag component that can't be told where to put focus: `dialog` takes\n // an `initialFocusEl` and hands it to the same trap, and even defaults an `alertdialog` to its\n // close trigger deliberately. Once the tour has that prop too, this effect goes away.\n const { open } = tour;\n // Everything below reaches for the document through the environment rather than the global one:\n // under an `EnvironmentProvider` — a consumer rendering into an iframe — that is the document the\n // portal, and so the tour, actually lives in. Zag's own machines take the same route.\n const { getDocument } = useEnvironmentContext();\n\n // The tour machine has no scroll handling of its own — no `preventScroll` prop, no listener —\n // while Zag's dialog machine locks the body through this same helper. The tour needs it just as\n // much: everything it paints is positioned in document coordinates from a target it resolves once,\n // when the step opens, and never looks up again. Let the page scroll and any layout that swaps its\n // elements around — a sticky header that moves its buttons into a portal is the common one —\n // leaves the machine anchored to a node that is no longer in the document. A detached node\n // measures zero, so the card, the spotlight and the backdrop's cut-out all collapse into the\n // top-left corner of the page. Locking is the honest reading of the presentation anyway: the tour\n // dims the page and takes focus, exactly like a modal dialog.\n // Not on iOS, where the helper pins `<body>` with `position: fixed` instead of hiding the\n // overflow: from there the machine's `scrollIntoView` on step entry can't move the viewport, so a\n // target below the fold is unreachable and the tour locks the user onto a step they can't see —\n // worse than the misalignment the lock is here to prevent. (`overflow: hidden` leaves programmatic\n // scrolling alone, which is what makes the lock safe everywhere else.)\n useEffect(() => (open && !isIos() ? preventBodyScroll(getDocument()) : undefined), [getDocument, open]);\n\n const contentId = tour.getContentProps().id;\n useEffect(() => {\n if (!open || contentId === undefined) {\n return undefined;\n }\n\n const contentEl = getDocument().getElementById(contentId);\n if (!contentEl) {\n return undefined;\n }\n\n const takeFocus = () => {\n contentEl.removeEventListener('focusin', takeFocus);\n // `preventScroll`, because this runs before Popper has placed the card, and until then the\n // card is parked off-screen at `-100vh` — a plain `focus()` scrolls the page up to it and\n // leaves the user looking at the top of the page with a tour they can't see. Zag passes the\n // same option everywhere it moves focus itself.\n contentEl.focus({ preventScroll: true });\n };\n\n contentEl.addEventListener('focusin', takeFocus);\n\n return () => contentEl.removeEventListener('focusin', takeFocus);\n }, [contentId, getDocument, open]);\n\n return (\n // `unmountOnExit` isn't only tidiness: Ark's backdrop applies `hidden: !tour.step?.backdrop`\n // after merging the presence props, which overrides the presence's own `hidden` — and since the\n // step is kept after the tour closes, the dimming would stay on screen for good. Unmounting on\n // exit takes the node out before that can happen.\n // The cast is the price of refining `step` and the step-mutating methods on the instance: our\n // step type isn't a supertype of Ark's, so the methods no longer match its api signature. The\n // object is Ark's api — only the types are ours.\n <ArkTour.Root unmountOnExit tour={tour as UseTourReturn}>\n <Portal>\n <TourBackdrop css={styles.backdrop} />\n <TourSpotlight css={styles.spotlight} />\n <TourPositioner css={styles.positioner}>\n <TourContent css={styles.content}>\n {/* `hidden={false}` overrides Zag, which hides the arrow until the card has been\n positioned — while Popper computes that position from the arrow's `clientHeight`, so\n a hidden arrow measures 0 and the card lands 6 short of the design's 12, then drops\n by that 6 as soon as anything recomputes. Nothing shows in the meantime: until it is\n positioned the card itself sits off-screen at `-100vh`. */}\n <TourArrow css={styles.arrow} hidden={false}>\n <TourArrowTip css={styles.arrowTip} />\n </TourArrow>\n <Box css={styles.header}>\n <Box css={styles.titleGroup}>\n {tour.totalSteps > 1 ? <TourProgressText css={styles.progressText} /> : null}\n <Box css={styles.titleRow}>\n <TourTitle css={styles.title} />\n {step?.badge ? <Box css={styles.badge}>{step.badge}</Box> : null}\n </Box>\n </Box>\n <ArkTour.CloseTrigger asChild>\n <BitkitCloseButton css={styles.closeTrigger} size=\"xs\" />\n </ArkTour.CloseTrigger>\n </Box>\n <TourDescription css={styles.description} />\n {step?.content ? <Box css={styles.media}>{step.content}</Box> : null}\n <TourControl css={styles.control}>\n {step?.secondaryAction ? <SecondaryAction secondaryAction={step.secondaryAction} /> : null}\n <Box css={styles.actions}>\n {actions.map((action, index) => (\n // Zag labels the trigger from its own translations (`close tour`, `next step`),\n // which would override the visible text as the accessible name — and put two\n // `Close tour` buttons on a card whose dismiss action reads `Got it`.\n <ArkTour.ActionTrigger key={action.label} asChild action={action} aria-label={action.label}>\n <BitkitButton size=\"sm\" variant={index === actions.length - 1 ? 'primary' : 'secondary'}>\n {action.label}\n </BitkitButton>\n </ArkTour.ActionTrigger>\n ))}\n </Box>\n </TourControl>\n </TourContent>\n </TourPositioner>\n </Portal>\n </ArkTour.Root>\n );\n};\n\nBitkitTour.displayName = 'BitkitTour';\n\nexport default BitkitTour;\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitrise/bitkit-v2",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.383",
|
|
5
5
|
"description": "Bitrise Design System Components built with Chakra UI V3",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"react",
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"@ark-ui/react": "5.39.1",
|
|
99
99
|
"@fontsource-variable/figtree": "^5.3.0",
|
|
100
100
|
"@fontsource-variable/source-code-pro": "^5.3.0",
|
|
101
|
+
"@zag-js/dom-query": "1.43.3",
|
|
101
102
|
"@zag-js/remove-scroll": "1.43.3",
|
|
102
103
|
"es-toolkit": "^1.52.0",
|
|
103
104
|
"react-markdown": "^10.1.0"
|