@robertlinde/react-tour-kit 1.0.2 → 1.1.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/CHANGELOG.md +17 -0
- package/README.md +5 -3
- package/dist/react-native-platform/layer/native-renderer.d.ts.map +1 -1
- package/dist/react-native-platform/layer/native-renderer.js +21 -2
- package/dist/react-native-platform/layer/native-renderer.js.map +1 -1
- package/dist/react-platform/dom/keep-reachable.util.d.ts +8 -0
- package/dist/react-platform/dom/keep-reachable.util.d.ts.map +1 -0
- package/dist/react-platform/dom/keep-reachable.util.js +35 -0
- package/dist/react-platform/dom/keep-reachable.util.js.map +1 -0
- package/dist/react-platform/dom/web-platform.d.ts.map +1 -1
- package/dist/react-platform/dom/web-platform.js +6 -3
- package/dist/react-platform/dom/web-platform.js.map +1 -1
- package/dist/react-platform/layer/styles.d.ts +1 -1
- package/dist/react-platform/layer/styles.d.ts.map +1 -1
- package/dist/react-platform/layer/styles.js +41 -1
- package/dist/react-platform/layer/styles.js.map +1 -1
- package/dist/react-platform/layer/web-renderer.d.ts.map +1 -1
- package/dist/react-platform/layer/web-renderer.js +27 -6
- package/dist/react-platform/layer/web-renderer.js.map +1 -1
- package/dist/shared/renderer/motion.d.ts +5 -2
- package/dist/shared/renderer/motion.d.ts.map +1 -1
- package/dist/shared/renderer/motion.js +5 -2
- package/dist/shared/renderer/motion.js.map +1 -1
- package/dist/shared/renderer/renderer.type.d.ts +7 -0
- package/dist/shared/renderer/renderer.type.d.ts.map +1 -1
- package/dist/shared/renderer/tour-overlay.d.ts.map +1 -1
- package/dist/shared/renderer/tour-overlay.js +49 -20
- package/dist/shared/renderer/tour-overlay.js.map +1 -1
- package/dist/shared/types/tour-i18n.type.d.ts +3 -1
- package/dist/shared/types/tour-i18n.type.d.ts.map +1 -1
- package/dist/shared/utils/i18n.util.d.ts.map +1 -1
- package/dist/shared/utils/i18n.util.js +2 -0
- package/dist/shared/utils/i18n.util.js.map +1 -1
- package/package.json +2 -1
- package/src/react-native-platform/layer/native-renderer.tsx +35 -1
- package/src/react-platform/dom/keep-reachable.util.ts +43 -0
- package/src/react-platform/dom/web-platform.ts +6 -3
- package/src/react-platform/layer/styles.ts +41 -1
- package/src/react-platform/layer/web-renderer.tsx +37 -7
- package/src/shared/renderer/motion.ts +5 -2
- package/src/shared/renderer/renderer.type.ts +8 -0
- package/src/shared/renderer/tour-overlay.tsx +66 -27
- package/src/shared/types/tour-i18n.type.ts +3 -1
- package/src/shared/utils/i18n.util.ts +2 -0
|
@@ -252,9 +252,49 @@ export const tourCss = `
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/* While a step takes a while to prepare: a spinner in the middle, on the tooltip's background. */
|
|
256
|
+
.rtk-waiting {
|
|
257
|
+
position: fixed !important;
|
|
258
|
+
top: 50% !important;
|
|
259
|
+
left: 50% !important;
|
|
260
|
+
translate: -50% -50%;
|
|
261
|
+
box-sizing: border-box;
|
|
262
|
+
display: flex;
|
|
263
|
+
align-items: center;
|
|
264
|
+
justify-content: center;
|
|
265
|
+
width: 56px;
|
|
266
|
+
height: 56px;
|
|
267
|
+
margin: 0 !important;
|
|
268
|
+
border-radius: var(--rtk-radius);
|
|
269
|
+
background: var(--rtk-bg);
|
|
270
|
+
box-shadow: 0 12px 32px -8px rgba(15, 23, 42, 0.35), 0 4px 12px -6px rgba(15, 23, 42, 0.2);
|
|
271
|
+
pointer-events: none !important;
|
|
272
|
+
animation: rtk-fade-in var(--rtk-fade-duration, 200ms) var(--rtk-ease) both;
|
|
273
|
+
}
|
|
274
|
+
.rtk-spinner {
|
|
275
|
+
box-sizing: border-box;
|
|
276
|
+
width: 24px;
|
|
277
|
+
height: 24px;
|
|
278
|
+
border: 3px solid rgba(127, 127, 127, 0.25);
|
|
279
|
+
border: 3px solid color-mix(in srgb, var(--rtk-primary) 20%, transparent);
|
|
280
|
+
border-top-color: var(--rtk-primary);
|
|
281
|
+
border-radius: 50%;
|
|
282
|
+
animation: rtk-spin 0.8s linear infinite;
|
|
283
|
+
}
|
|
284
|
+
@keyframes rtk-fade-in {
|
|
285
|
+
from { opacity: 0; }
|
|
286
|
+
}
|
|
287
|
+
@keyframes rtk-spin {
|
|
288
|
+
to { rotate: 1turn; }
|
|
289
|
+
}
|
|
290
|
+
@media (prefers-reduced-motion: reduce) {
|
|
291
|
+
.rtk-spinner { animation-duration: 2s; }
|
|
292
|
+
}
|
|
293
|
+
|
|
255
294
|
@media (forced-colors: active) {
|
|
256
295
|
.rtk-spotlight { outline: 3px solid Highlight; }
|
|
257
|
-
.rtk-card { border: 1px solid CanvasText; }
|
|
296
|
+
.rtk-card, .rtk-waiting { border: 1px solid CanvasText; }
|
|
297
|
+
.rtk-spinner { border-color: GrayText; border-top-color: Highlight; }
|
|
258
298
|
/* Without backgrounds, a border is what shows a button is one. */
|
|
259
299
|
.rtk-button { border: 1px solid ButtonText !important; }
|
|
260
300
|
.rtk-button:disabled { border-color: GrayText !important; }
|
|
@@ -28,8 +28,10 @@ import type {
|
|
|
28
28
|
TextPartProps,
|
|
29
29
|
TooltipFrameProps,
|
|
30
30
|
TourRenderer,
|
|
31
|
+
WaitingProps,
|
|
31
32
|
} from '../../shared/renderer/renderer.type.js';
|
|
32
33
|
import {handlesKey, pageDirection} from '../dom/elements.util.js';
|
|
34
|
+
import {keepReachable} from '../dom/keep-reachable.util.js';
|
|
33
35
|
import {lockScroll} from '../dom/scroll-lock.util.js';
|
|
34
36
|
import {injectTourStyles} from './styles.js';
|
|
35
37
|
|
|
@@ -154,7 +156,9 @@ function Layer({
|
|
|
154
156
|
const trigger = document.activeElement;
|
|
155
157
|
openModal(dialog);
|
|
156
158
|
const unlock = lockScroll(dialog);
|
|
159
|
+
const stopKeepingReachable = keepReachable(dialog);
|
|
157
160
|
return (): void => {
|
|
161
|
+
stopKeepingReachable();
|
|
158
162
|
unlock();
|
|
159
163
|
reopening.current = true;
|
|
160
164
|
closeModal(dialog);
|
|
@@ -162,6 +166,29 @@ function Layer({
|
|
|
162
166
|
};
|
|
163
167
|
}, []);
|
|
164
168
|
|
|
169
|
+
// Escape belongs to the topmost modal, the tour. Modal libraries underneath listen for it on the
|
|
170
|
+
// document before it reaches the tour (Radix, in the capture phase): take it on the window first.
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!isInteractive) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const onEscape = (event: KeyboardEvent): void => {
|
|
177
|
+
if (event.key !== 'Escape' || event.isComposing || event.altKey || event.ctrlKey || event.metaKey) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
event.stopPropagation();
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
onIntent('escape');
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
globalThis.addEventListener('keydown', onEscape, {capture: true});
|
|
187
|
+
return (): void => {
|
|
188
|
+
globalThis.removeEventListener('keydown', onEscape, {capture: true});
|
|
189
|
+
};
|
|
190
|
+
}, [isInteractive, onIntent]);
|
|
191
|
+
|
|
165
192
|
// If the app opened its own modal (in onBeforeStep), move the tour back on top of it.
|
|
166
193
|
useEffect(() => {
|
|
167
194
|
const dialog = layerRef.current;
|
|
@@ -229,12 +256,6 @@ function Layer({
|
|
|
229
256
|
return;
|
|
230
257
|
}
|
|
231
258
|
|
|
232
|
-
if (key === 'Escape') {
|
|
233
|
-
event.preventDefault();
|
|
234
|
-
onIntent('escape');
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
259
|
if ((key === 'ArrowRight' || key === 'ArrowLeft') && !handlesKey(event.target, key)) {
|
|
239
260
|
event.preventDefault();
|
|
240
261
|
// In right-to-left pages, the arrow pointing to the start goes forward.
|
|
@@ -453,6 +474,15 @@ function TooltipFrame({children, layout, entrance, isBusy, isHidden, onSize}: To
|
|
|
453
474
|
);
|
|
454
475
|
}
|
|
455
476
|
|
|
477
|
+
/** A spinner in the middle of the screen; presses go through to the dimmed area. */
|
|
478
|
+
function Waiting({label}: WaitingProps): ReactNode {
|
|
479
|
+
return (
|
|
480
|
+
<div className="rtk-waiting" role="progressbar" aria-label={label}>
|
|
481
|
+
<span aria-hidden="true" className="rtk-spinner" />
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
456
486
|
function MeasureFrame({children, maxWidth, maxHeight, onSize}: MeasureFrameProps): ReactNode {
|
|
457
487
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
458
488
|
useIsomorphicLayoutEffect(() => {
|
|
@@ -591,4 +621,4 @@ const card: CardParts = {
|
|
|
591
621
|
};
|
|
592
622
|
|
|
593
623
|
/** How the web draws the tour. */
|
|
594
|
-
export const webRenderer: TourRenderer = {Layer, Backdrop, Spotlight, TooltipFrame, MeasureFrame, card};
|
|
624
|
+
export const webRenderer: TourRenderer = {Layer, Backdrop, Spotlight, TooltipFrame, MeasureFrame, Waiting, card};
|
|
@@ -17,8 +17,11 @@ export const motion = {
|
|
|
17
17
|
/** Distance (from the target's side) and scale the tooltip enters from. */
|
|
18
18
|
enterOffset: 6,
|
|
19
19
|
enterScale: 0.96,
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* If a step takes longer than this to prepare (navigation, data, a target that is still
|
|
22
|
+
* rendering), the tour shows that it is waiting: the highlight closes and a spinner appears.
|
|
23
|
+
*/
|
|
24
|
+
waitingDelayMs: 300,
|
|
22
25
|
/** Control points of the standard easing curve (`cubic-bezier`). */
|
|
23
26
|
easing: [0.2, 0, 0, 1],
|
|
24
27
|
} as const;
|
|
@@ -25,6 +25,8 @@ export type TourRenderer = {
|
|
|
25
25
|
readonly TooltipFrame: ComponentType<TooltipFrameProps>;
|
|
26
26
|
/** Renders a tooltip invisibly with given size limits and reports its size. */
|
|
27
27
|
readonly MeasureFrame: ComponentType<MeasureFrameProps>;
|
|
28
|
+
/** A spinner in the middle of the layer, while the tour waits for a step that takes a while to prepare. */
|
|
29
|
+
readonly Waiting: ComponentType<WaitingProps>;
|
|
28
30
|
/** The pieces of the built-in tooltip card. */
|
|
29
31
|
readonly card: CardParts;
|
|
30
32
|
};
|
|
@@ -104,6 +106,12 @@ export type Entrance = {
|
|
|
104
106
|
readonly delayMs: number;
|
|
105
107
|
};
|
|
106
108
|
|
|
109
|
+
export type WaitingProps = {
|
|
110
|
+
/** Accessible name of the spinner. */
|
|
111
|
+
readonly label: string;
|
|
112
|
+
readonly theme: ResolvedTheme;
|
|
113
|
+
};
|
|
114
|
+
|
|
107
115
|
export type MeasureFrameProps = {
|
|
108
116
|
readonly children: ReactNode;
|
|
109
117
|
readonly maxWidth: number;
|
|
@@ -86,14 +86,17 @@ function OverlayContent({
|
|
|
86
86
|
isClosing,
|
|
87
87
|
onClosed,
|
|
88
88
|
}: OverlayContentProps): ReactNode {
|
|
89
|
-
const {Layer, Backdrop, Spotlight} = renderer;
|
|
89
|
+
const {Layer, Backdrop, Spotlight, Waiting} = renderer;
|
|
90
90
|
const id = useId();
|
|
91
91
|
const titleId = `${id}title`;
|
|
92
92
|
const descriptionId = `${id}description`;
|
|
93
93
|
const {shown, phase, measure} = view;
|
|
94
94
|
const totalSteps = view.steps.length;
|
|
95
95
|
// The first step can take a while (navigation in onBeforeStep): dim the screen after a moment.
|
|
96
|
-
const isWaitingDimmed = useDelayedFlag(!shown && !isClosing, motion.
|
|
96
|
+
const isWaitingDimmed = useDelayedFlag(!shown && !isClosing, motion.waitingDelayMs);
|
|
97
|
+
// Preparing a step takes a while: the highlight closes and a spinner shows that the tour is waiting.
|
|
98
|
+
const isPreparing = phase === 'preparing' && !isClosing;
|
|
99
|
+
const isWaiting = useDelayedFlag(isPreparing, motion.waitingDelayMs);
|
|
97
100
|
// Once on screen, the overlay stays until the tour ends (also when another tour takes over).
|
|
98
101
|
const [hasBeenVisible, setHasBeenVisible] = useState(false);
|
|
99
102
|
const isShowingSomething = shown !== undefined || isWaitingDimmed;
|
|
@@ -107,8 +110,8 @@ function OverlayContent({
|
|
|
107
110
|
useEffect(() => {
|
|
108
111
|
visibleSince.current = isVisible ? Date.now() : undefined;
|
|
109
112
|
}, [isVisible]);
|
|
110
|
-
const spotlight = useSpotlight(view, isVisible, isReducedMotion);
|
|
111
|
-
const announcement = useAnnouncement(
|
|
113
|
+
const spotlight = useSpotlight(view, isVisible, isReducedMotion, isWaiting);
|
|
114
|
+
const announcement = useAnnouncement(view, i18n, isClosing, isWaiting);
|
|
112
115
|
|
|
113
116
|
// Fade out, then unmount.
|
|
114
117
|
useEffect(() => {
|
|
@@ -155,10 +158,11 @@ function OverlayContent({
|
|
|
155
158
|
|
|
156
159
|
const onBackdropPress = (point: Point): void => {
|
|
157
160
|
// Not while the overlay is still appearing (a double click on the button that started the
|
|
158
|
-
// tour), nor
|
|
161
|
+
// tour), nor right after a step change began (a second tap on Next falls through the busy
|
|
162
|
+
// tooltip). Once the tour shows that it is waiting, a press leaves it.
|
|
159
163
|
const since = visibleSince.current;
|
|
160
164
|
const hasFadedIn = since !== undefined && Date.now() - since >= motion.fadeInMs;
|
|
161
|
-
const isChangingStep = shown !== undefined && phase !== 'showing';
|
|
165
|
+
const isChangingStep = shown !== undefined && phase !== 'showing' && !isWaiting;
|
|
162
166
|
if (!isOverlayDismissible || isClosing || !hasFadedIn || isChangingStep) {
|
|
163
167
|
return;
|
|
164
168
|
}
|
|
@@ -170,9 +174,6 @@ function OverlayContent({
|
|
|
170
174
|
};
|
|
171
175
|
|
|
172
176
|
const context = useMemo<RendererContextValue>(() => ({renderer, theme, i18n}), [renderer, theme, i18n]);
|
|
173
|
-
const counter = shown
|
|
174
|
-
? formatTemplate(i18n.stepCounterTemplate, {current: shown.stepIndex + 1, total: totalSteps})
|
|
175
|
-
: '';
|
|
176
177
|
|
|
177
178
|
return (
|
|
178
179
|
<RendererContext.Provider value={context}>
|
|
@@ -184,7 +185,7 @@ function OverlayContent({
|
|
|
184
185
|
theme={theme}
|
|
185
186
|
titleId={shown ? titleId : undefined}
|
|
186
187
|
descriptionId={shown ? descriptionId : undefined}
|
|
187
|
-
fallbackLabel={(shown
|
|
188
|
+
fallbackLabel={fallbackLabelOf(shown, totalSteps, i18n)}
|
|
188
189
|
announcement={announcement}
|
|
189
190
|
stepKey={shown?.transitionId}
|
|
190
191
|
onIntent={onIntent}
|
|
@@ -199,8 +200,8 @@ function OverlayContent({
|
|
|
199
200
|
Tooltip={Tooltip}
|
|
200
201
|
shown={shown}
|
|
201
202
|
totalSteps={totalSteps}
|
|
202
|
-
isBusy={
|
|
203
|
-
isHidden={phase === 'revealing'}
|
|
203
|
+
isBusy={isPreparing}
|
|
204
|
+
isHidden={phase === 'revealing' || isWaiting}
|
|
204
205
|
isDelayed={spotlight.isMorphing}
|
|
205
206
|
isReducedMotion={isReducedMotion}
|
|
206
207
|
titleId={titleId}
|
|
@@ -208,6 +209,7 @@ function OverlayContent({
|
|
|
208
209
|
onIntent={onIntent}
|
|
209
210
|
/>
|
|
210
211
|
) : null}
|
|
212
|
+
{isWaiting ? <Waiting label={i18n.loadingAriaLabel} theme={theme} /> : null}
|
|
211
213
|
{measure && !isClosing ? (
|
|
212
214
|
<MeasuredTooltip
|
|
213
215
|
key={measure.id}
|
|
@@ -365,20 +367,46 @@ type SpotlightState = {
|
|
|
365
367
|
readonly isMorphing: boolean;
|
|
366
368
|
};
|
|
367
369
|
|
|
370
|
+
/** What the spotlight marks, and the key under which a move there is animated. */
|
|
371
|
+
function spotlightTarget(
|
|
372
|
+
view: TourViewState,
|
|
373
|
+
isWaiting: boolean,
|
|
374
|
+
): {readonly hole: Box | undefined; readonly key: string} {
|
|
375
|
+
const {shown, phase, revealHighlight} = view;
|
|
376
|
+
const shownKey = shown?.transitionId ?? 'none';
|
|
377
|
+
// While the tour waits for a step, nothing is highlighted: the hole closes.
|
|
378
|
+
if (isWaiting) {
|
|
379
|
+
return {hole: undefined, key: `waiting:${shownKey}`};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (phase === 'revealing') {
|
|
383
|
+
return {
|
|
384
|
+
hole: revealHighlight ? inflate(revealHighlight, tourLayout.highlightPadding) : undefined,
|
|
385
|
+
key: `reveal:${shownKey}`,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const hole = shown?.highlight ? inflate(shown.highlight, tourLayout.highlightPadding) : undefined;
|
|
390
|
+
return {hole, key: `${shownKey}:${hole ? 'open' : 'closed'}`};
|
|
391
|
+
}
|
|
392
|
+
|
|
368
393
|
/**
|
|
369
394
|
* Where the spotlight's hole is and how it gets there. While the next target is scrolled into
|
|
370
395
|
* view, the hole goes with it (frame by frame, or straight to where it will end up); otherwise it
|
|
371
396
|
* marks the step on screen. Moving to another step is animated; following a target that moves on
|
|
372
397
|
* its own (layout changes, resizing) is instant, like the tooltip, also while the spotlight is
|
|
373
|
-
* still on its way. Nothing animates before the
|
|
398
|
+
* still on its way. While the tour waits for a step, the hole closes. Nothing animates before the
|
|
399
|
+
* overlay has appeared.
|
|
374
400
|
*/
|
|
375
|
-
function useSpotlight(
|
|
376
|
-
|
|
401
|
+
function useSpotlight(
|
|
402
|
+
view: TourViewState,
|
|
403
|
+
isVisible: boolean,
|
|
404
|
+
isReducedMotion: boolean,
|
|
405
|
+
isWaiting: boolean,
|
|
406
|
+
): SpotlightState {
|
|
407
|
+
const {phase, followsScroll} = view;
|
|
377
408
|
const isRevealing = phase === 'revealing';
|
|
378
|
-
const
|
|
379
|
-
const hole = source ? inflate(source, tourLayout.highlightPadding) : undefined;
|
|
380
|
-
const shownKey = shown?.transitionId ?? 'none';
|
|
381
|
-
const key = isRevealing ? `reveal:${shownKey}` : `${shownKey}:${hole ? 'open' : 'closed'}`;
|
|
409
|
+
const {hole, key} = spotlightTarget(view, isWaiting);
|
|
382
410
|
|
|
383
411
|
// The key at which the overlay appeared: it appears in place, later changes animate.
|
|
384
412
|
const [appearedAtKey, setAppearedAtKey] = useState<string | undefined>(undefined);
|
|
@@ -409,26 +437,37 @@ function useSpotlight(view: TourViewState, isVisible: boolean, isReducedMotion:
|
|
|
409
437
|
}
|
|
410
438
|
|
|
411
439
|
/** "2 of 5: Title" when the step changes (the first step is announced by the tour appearing). */
|
|
412
|
-
function useAnnouncement(
|
|
413
|
-
shown
|
|
414
|
-
totalSteps: number,
|
|
415
|
-
i18n: ResolvedI18n,
|
|
416
|
-
isClosing: boolean,
|
|
417
|
-
): string {
|
|
440
|
+
function useAnnouncement(view: TourViewState, i18n: ResolvedI18n, isClosing: boolean, isWaiting: boolean): string {
|
|
441
|
+
const {shown, phase} = view;
|
|
418
442
|
const [firstKey, setFirstKey] = useState<number | undefined>(undefined);
|
|
419
443
|
if (shown && firstKey === undefined) {
|
|
420
444
|
setFirstKey(shown.transitionId);
|
|
421
445
|
}
|
|
422
446
|
|
|
423
|
-
|
|
447
|
+
// While the tour waits for a step, that is what is said; a step once it is on screen.
|
|
448
|
+
if (isWaiting) {
|
|
449
|
+
return i18n.loadingAriaLabel;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!shown || isClosing || phase !== 'showing' || shown.transitionId === firstKey) {
|
|
424
453
|
return '';
|
|
425
454
|
}
|
|
426
455
|
|
|
427
|
-
const counter = formatTemplate(i18n.stepCounterTemplate, {current: shown.stepIndex + 1, total:
|
|
456
|
+
const counter = formatTemplate(i18n.stepCounterTemplate, {current: shown.stepIndex + 1, total: view.steps.length});
|
|
428
457
|
const title = textOf(shown.step.title);
|
|
429
458
|
return title ? `${counter}: ${title}` : counter;
|
|
430
459
|
}
|
|
431
460
|
|
|
461
|
+
/** Name of the tour dialog for a custom tooltip without a title element: the step's title, or its counter. */
|
|
462
|
+
function fallbackLabelOf(shown: ShownStep | undefined, totalSteps: number, i18n: ResolvedI18n): string {
|
|
463
|
+
if (!shown) {
|
|
464
|
+
return '';
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const counter = formatTemplate(i18n.stepCounterTemplate, {current: shown.stepIndex + 1, total: totalSteps});
|
|
468
|
+
return textOf(shown.step.title) || counter;
|
|
469
|
+
}
|
|
470
|
+
|
|
432
471
|
/** The plain text of React content (strings, numbers, arrays and the children of elements). */
|
|
433
472
|
export function textOf(node: ReactNode): string {
|
|
434
473
|
if (typeof node === 'string' || typeof node === 'number') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Texts of the built-in tooltip. Every key is optional. */
|
|
1
|
+
/** Texts of the tour (most of them in the built-in tooltip). Every key is optional. */
|
|
2
2
|
export type TourI18n = {
|
|
3
3
|
/** @defaultValue `'Next'` */
|
|
4
4
|
readonly nextLabel?: string;
|
|
@@ -12,4 +12,6 @@ export type TourI18n = {
|
|
|
12
12
|
readonly closeAriaLabel?: string;
|
|
13
13
|
/** Accessible name of each step dot; `{step}` and `{total}` are replaced. @defaultValue `'Go to step {step}'` */
|
|
14
14
|
readonly dotAriaLabelTemplate?: string;
|
|
15
|
+
/** Accessible name of the spinner shown while a step takes a while to prepare. @defaultValue `'Loading…'` */
|
|
16
|
+
readonly loadingAriaLabel?: string;
|
|
15
17
|
};
|
|
@@ -9,6 +9,7 @@ export const defaultI18n: ResolvedI18n = {
|
|
|
9
9
|
stepCounterTemplate: '{current} of {total}',
|
|
10
10
|
closeAriaLabel: 'Close tour',
|
|
11
11
|
dotAriaLabelTemplate: 'Go to step {step}',
|
|
12
|
+
loadingAriaLabel: 'Loading…',
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
const i18nKeys = [
|
|
@@ -18,6 +19,7 @@ const i18nKeys = [
|
|
|
18
19
|
'stepCounterTemplate',
|
|
19
20
|
'closeAriaLabel',
|
|
20
21
|
'dotAriaLabelTemplate',
|
|
22
|
+
'loadingAriaLabel',
|
|
21
23
|
] as const satisfies ReadonlyArray<keyof TourI18n>;
|
|
22
24
|
|
|
23
25
|
export function resolveI18n(i18n: TourI18n | undefined): ResolvedI18n {
|