@cloud-ru/ds-uikit-product-welcome-tour 1.1.0 → 1.2.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/CHANGELOG.md +12 -0
- package/README.md +5 -0
- package/dist/cjs/components/WelcomeTour/WelcomeTour.d.ts +1 -1
- package/dist/cjs/components/WelcomeTour/WelcomeTour.js +18 -2
- package/dist/cjs/components/WelcomeTour/constants.d.ts +4 -0
- package/dist/cjs/components/WelcomeTour/constants.js +5 -1
- package/dist/cjs/components/WelcomeTour/utils.d.ts +4 -1
- package/dist/cjs/components/WelcomeTour/utils.js +7 -1
- package/dist/cjs/helperComponents/TourHint/TourHint.js +2 -2
- package/dist/cjs/helperComponents/TourHint/types.d.ts +2 -0
- package/dist/cjs/types.d.ts +8 -0
- package/dist/esm/components/WelcomeTour/WelcomeTour.d.ts +1 -1
- package/dist/esm/components/WelcomeTour/WelcomeTour.js +20 -4
- package/dist/esm/components/WelcomeTour/constants.d.ts +4 -0
- package/dist/esm/components/WelcomeTour/constants.js +4 -0
- package/dist/esm/components/WelcomeTour/utils.d.ts +4 -1
- package/dist/esm/components/WelcomeTour/utils.js +7 -2
- package/dist/esm/helperComponents/TourHint/TourHint.js +2 -2
- package/dist/esm/helperComponents/TourHint/types.d.ts +2 -0
- package/dist/esm/types.d.ts +8 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/components/WelcomeTour/WelcomeTour.tsx +23 -3
- package/src/components/WelcomeTour/constants.ts +6 -0
- package/src/components/WelcomeTour/utils.ts +15 -2
- package/src/helperComponents/TourHint/TourHint.tsx +2 -2
- package/src/helperComponents/TourHint/types.ts +2 -0
- package/src/types.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## 1.2.1 (2026-09-06)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **FF-8762:** swallow overlay presses so page layers stay open during the tour ([3812a8f](https://github.com/cloud-ru-tech/snack-v2/commit/3812a8f7a207a0f61e9d85d890e58a5ba5762192))
|
|
11
|
+
|
|
12
|
+
# 1.2.0 (2026-09-03)
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- **FF-8762:** add showStepIndicator prop to welcome tour ([f4beeb2](https://github.com/cloud-ru-tech/snack-v2/commit/f4beeb2d72919d58312cf2a7a00822812517ae27))
|
|
17
|
+
|
|
6
18
|
# 1.1.0 (2026-09-02)
|
|
7
19
|
|
|
8
20
|
### Features
|
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
|
|
34
34
|
Все слоты необязательные, кроме `target`. Индикатор прогресса появляется автоматически, когда шагов больше одного, и остаётся неинтерактивным — он показывает позицию в туре, навигация идёт кнопками.
|
|
35
35
|
|
|
36
|
+
`showStepIndicator={false}` убирает индикатор вместе с озвучкой позиции для скринридера. Это нужно туру, у которого длина неизвестна заранее: шаг с ненайденной целью тур пропускает на лету (адаптив скрыл блок, раздел недоступен по правам), а пересчитать общее число он не может — «2 из 5» тогда врёт. Когда все шаги гарантированно доступны, индикатор оставляют: он показывает, сколько ещё идти.
|
|
37
|
+
|
|
36
38
|
### Placement (default `bottom`)
|
|
37
39
|
|
|
38
40
|
`steps[i].placement` задаёт положение подсказки относительно целевого элемента: `top`, `bottom`, `left`, `right` и их варианты `-start` / `-end`, а также `auto` (сторона подбирается автоматически) и `center` (подсказка по центру экрана — для шага без привязки к элементу). Если выбранной стороне не хватает места, подсказка разворачивается на противоположную.
|
|
@@ -236,6 +238,7 @@ export function WithoutBackButton() {
|
|
|
236
238
|
| `open` | `boolean` | — | Запущен ли тур. Управляемый режим. |
|
|
237
239
|
| `portalContainer` | `HTMLElement \| null` | — | Контейнер для портала. По умолчанию — контейнер из `PortalContextProvider` либо `document.body`. |
|
|
238
240
|
| `scrollOffset` | `number` | `20` | Отступ при скролле к целевому элементу, px. |
|
|
241
|
+
| `showStepIndicator` | `boolean` | `true` | Показывать ли индикатор прогресса. Он и так появляется только у тура длиннее одного <br/> шага; `false` убирает его вместе с озвучкой позиции для скринридера — это нужно там, <br/> где часть шагов может отвалиться на лету (цели нет на странице, шаг пропускается), <br/> и «2 из 5» окажется неправдой. |
|
|
239
242
|
| `spotlightPadding` | `TourSpotlightPadding` | `10` | Отступ выреза от границ целевого элемента для всех шагов. Шаг переопределяет своим <br/> `spotlightPadding`. |
|
|
240
243
|
| `stepIndex` | `number` | — | Индекс текущего шага. Управляемый режим. |
|
|
241
244
|
| `steps` | `TourStep` | — | Шаги тура. |
|
|
@@ -290,6 +293,8 @@ export function WithoutBackButton() {
|
|
|
290
293
|
- ❌ Указывать `target` на элемент за пределами вьюпорта или появляющийся асинхронно — шаг не найдёт цель.
|
|
291
294
|
- ✅ Формулировать шаг одним действием: что это и зачем нажимать.
|
|
292
295
|
- ❌ Складывать в `content` абзац справки — для этого есть документация раздела.
|
|
296
|
+
- ✅ Скрывать индикатор (`showStepIndicator={false}`), если часть шагов может оказаться недоступной.
|
|
297
|
+
- ❌ Показывать «2 из 5» туру, который на узком экране проходит за три шага.
|
|
293
298
|
- ✅ Оставлять `skip`: пользователь должен уметь выйти в любой момент.
|
|
294
299
|
- ❌ Делать тур безвыходным (`buttons` только с `primary`) в обязательном онбординге.
|
|
295
300
|
|
|
@@ -12,4 +12,4 @@ import { WelcomeTourProps } from '../../types';
|
|
|
12
12
|
* `data-test-id` компонент не принимает: слоты подсказки адресуются через публичную
|
|
13
13
|
* константу `TEST_IDS`.
|
|
14
14
|
*/
|
|
15
|
-
export declare function WelcomeTour({ steps, open, defaultOpen, onOpenChange, stepIndex, defaultStepIndex, onStepChange, labels: componentLabels, buttons, scrollOffset, spotlightPadding, portalContainer, }: WelcomeTourProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export declare function WelcomeTour({ steps, open, defaultOpen, onOpenChange, stepIndex, defaultStepIndex, onStepChange, labels: componentLabels, buttons, showStepIndicator, scrollOffset, spotlightPadding, portalContainer, }: WelcomeTourProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -28,7 +28,7 @@ const FLOATING_OPTIONS = { shiftOptions: { crossAxis: true, padding: 16 } };
|
|
|
28
28
|
* `data-test-id` компонент не принимает: слоты подсказки адресуются через публичную
|
|
29
29
|
* константу `TEST_IDS`.
|
|
30
30
|
*/
|
|
31
|
-
function WelcomeTour({ steps, open, defaultOpen = false, onOpenChange, stepIndex, defaultStepIndex = 0, onStepChange, labels: componentLabels, buttons = DEFAULT_BUTTONS, scrollOffset, spotlightPadding, portalContainer, }) {
|
|
31
|
+
function WelcomeTour({ steps, open, defaultOpen = false, onOpenChange, stepIndex, defaultStepIndex = 0, onStepChange, labels: componentLabels, buttons = DEFAULT_BUTTONS, showStepIndicator = true, scrollOffset, spotlightPadding, portalContainer, }) {
|
|
32
32
|
const { t } = locale_1.welcomeTourLocale.useTranslations();
|
|
33
33
|
const portalContextRef = (0, portal_context_1.usePortalContext)();
|
|
34
34
|
const [isOpen, setOpen] = (0, utils_1.useUncontrolledProp)(open, defaultOpen, onOpenChange);
|
|
@@ -62,7 +62,8 @@ function WelcomeTour({ steps, open, defaultOpen = false, onOpenChange, stepIndex
|
|
|
62
62
|
labels: { next: t('next'), back: t('back'), finish: t('finish'), close: t('close') },
|
|
63
63
|
componentLabels,
|
|
64
64
|
buttons,
|
|
65
|
-
|
|
65
|
+
showStepIndicator,
|
|
66
|
+
}), [steps, t, componentLabels, buttons, showStepIndicator]);
|
|
66
67
|
const handleEvent = (0, react_1.useCallback)(({ action, controlled, type, index, status }) => {
|
|
67
68
|
switch (type) {
|
|
68
69
|
// Неуправляемый режим: индекс уже переключил движок, компонент сообщает о факте.
|
|
@@ -109,6 +110,21 @@ function WelcomeTour({ steps, open, defaultOpen = false, onOpenChange, stepIndex
|
|
|
109
110
|
break;
|
|
110
111
|
}
|
|
111
112
|
}, [steps, onStepChange, setOpen]);
|
|
113
|
+
// `overlayClickAction: false` отменяет только реакцию самого тура: нажатие всё равно
|
|
114
|
+
// доходит до документа, и раскрытый список, вокруг которого построен шаг, закрывается
|
|
115
|
+
// как от клика мимо себя. Гасим на подхвате в портале, вместе с уводом фокуса.
|
|
116
|
+
(0, react_1.useEffect)(() => {
|
|
117
|
+
if (!isOpen || !portalElement)
|
|
118
|
+
return;
|
|
119
|
+
const swallowPress = (event) => {
|
|
120
|
+
if (!(0, utils_2.isOverlayTarget)(event.target))
|
|
121
|
+
return;
|
|
122
|
+
event.stopPropagation();
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
};
|
|
125
|
+
constants_2.OVERLAY_PRESS_EVENTS.forEach(type => portalElement.addEventListener(type, swallowPress, true));
|
|
126
|
+
return () => constants_2.OVERLAY_PRESS_EVENTS.forEach(type => portalElement.removeEventListener(type, swallowPress, true));
|
|
127
|
+
}, [isOpen, portalElement]);
|
|
112
128
|
// Esc завершает тур целиком, как у остальных оверлеев ДС; `dismissKeyAction` движка
|
|
113
129
|
// закрывает только текущий шаг, поэтому он выключен. Уступаем чужому слою: шаг тура
|
|
114
130
|
// может открыть диалог, и Esc тогда адресован ему.
|
|
@@ -27,3 +27,7 @@ export declare const TOUR_COLORS: {
|
|
|
27
27
|
* расчётной уезжает за край экрана.
|
|
28
28
|
*/
|
|
29
29
|
export declare const DEFAULT_HINT_WIDTH = 480;
|
|
30
|
+
/** Класс затемнения у react-joyride: отличить его от подсказки в общем портале больше нечем. */
|
|
31
|
+
export declare const OVERLAY_CLASS_NAME = "react-joyride__overlay";
|
|
32
|
+
/** События, по которым закрываются слои страницы: floating-ui слушает `pointerdown`, прочие — `mousedown` и `click`. */
|
|
33
|
+
export declare const OVERLAY_PRESS_EVENTS: readonly ["pointerdown", "mousedown", "click"];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_HINT_WIDTH = exports.TOUR_COLORS = exports.TOUR_GEOMETRY = void 0;
|
|
3
|
+
exports.OVERLAY_PRESS_EVENTS = exports.OVERLAY_CLASS_NAME = exports.DEFAULT_HINT_WIDTH = exports.TOUR_COLORS = exports.TOUR_GEOMETRY = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Геометрия для react-joyride. Движок считает позиции и рисует стрелку инлайн-стилями,
|
|
6
6
|
* поэтому нужны числа, а не CSS-переменные. Рядом с каждым значением — переменная
|
|
@@ -36,3 +36,7 @@ exports.TOUR_COLORS = {
|
|
|
36
36
|
* расчётной уезжает за край экрана.
|
|
37
37
|
*/
|
|
38
38
|
exports.DEFAULT_HINT_WIDTH = 480;
|
|
39
|
+
/** Класс затемнения у react-joyride: отличить его от подсказки в общем портале больше нечем. */
|
|
40
|
+
exports.OVERLAY_CLASS_NAME = 'react-joyride__overlay';
|
|
41
|
+
/** События, по которым закрываются слои страницы: floating-ui слушает `pointerdown`, прочие — `mousedown` и `click`. */
|
|
42
|
+
exports.OVERLAY_PRESS_EVENTS = ['pointerdown', 'mousedown', 'click'];
|
|
@@ -5,7 +5,10 @@ type ToJoyrideStepsParams = {
|
|
|
5
5
|
labels: TourLabels;
|
|
6
6
|
componentLabels?: Partial<TourLabels>;
|
|
7
7
|
buttons: TourButton[];
|
|
8
|
+
showStepIndicator: boolean;
|
|
8
9
|
};
|
|
10
|
+
/** Пришло ли нажатие в затемнение, а не в подсказку: в портале лежат они оба. */
|
|
11
|
+
export declare function isOverlayTarget(target: EventTarget | null): boolean;
|
|
9
12
|
/**
|
|
10
13
|
* Приводит статус движка к публичному `TourStatus`: наружу сообщаются только терминальные
|
|
11
14
|
* `finished` и `skipped`, а каст пропустил бы любую строку.
|
|
@@ -18,5 +21,5 @@ export declare function resolveLabels(base: TourLabels, componentLabels?: Partia
|
|
|
18
21
|
* (`subtitle`, разрешённые подписи, набор кнопок), уезжают в `step.data` — оттуда
|
|
19
22
|
* их читает `TourHint`.
|
|
20
23
|
*/
|
|
21
|
-
export declare function toJoyrideSteps({ steps, labels, componentLabels, buttons }: ToJoyrideStepsParams): Step[];
|
|
24
|
+
export declare function toJoyrideSteps({ steps, labels, componentLabels, buttons, showStepIndicator, }: ToJoyrideStepsParams): Step[];
|
|
22
25
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isOverlayTarget = isOverlayTarget;
|
|
3
4
|
exports.toTourStatus = toTourStatus;
|
|
4
5
|
exports.resolveLabels = resolveLabels;
|
|
5
6
|
exports.toJoyrideSteps = toJoyrideSteps;
|
|
@@ -7,6 +8,10 @@ const constants_1 = require("../../constants");
|
|
|
7
8
|
const constants_2 = require("./constants");
|
|
8
9
|
const spotlight_1 = require("./spotlight");
|
|
9
10
|
const targets_1 = require("./targets");
|
|
11
|
+
/** Пришло ли нажатие в затемнение, а не в подсказку: в портале лежат они оба. */
|
|
12
|
+
function isOverlayTarget(target) {
|
|
13
|
+
return target instanceof Element && Boolean(target.closest(`.${constants_2.OVERLAY_CLASS_NAME}`));
|
|
14
|
+
}
|
|
10
15
|
/**
|
|
11
16
|
* Приводит статус движка к публичному `TourStatus`: наружу сообщаются только терминальные
|
|
12
17
|
* `finished` и `skipped`, а каст пропустил бы любую строку.
|
|
@@ -24,7 +29,7 @@ function resolveLabels(base, componentLabels, stepLabels) {
|
|
|
24
29
|
* (`subtitle`, разрешённые подписи, набор кнопок), уезжают в `step.data` — оттуда
|
|
25
30
|
* их читает `TourHint`.
|
|
26
31
|
*/
|
|
27
|
-
function toJoyrideSteps({ steps, labels, componentLabels, buttons }) {
|
|
32
|
+
function toJoyrideSteps({ steps, labels, componentLabels, buttons, showStepIndicator, }) {
|
|
28
33
|
return steps.map(({ target, title, subtitle, content, placement, width, spotlightTarget, spotlightPadding, labels: stepLabels, onBeforeShow, }) => ({
|
|
29
34
|
// Со списком целей подсказка цепляется за «зеркальный» узел: раскрытый список
|
|
30
35
|
// закрывается по клику мимо себя, то есть уже на `mousedown` по кнопке подсказки —
|
|
@@ -50,6 +55,7 @@ function toJoyrideSteps({ steps, labels, componentLabels, buttons }) {
|
|
|
50
55
|
subtitle,
|
|
51
56
|
labels: resolveLabels(labels, componentLabels, stepLabels),
|
|
52
57
|
buttons,
|
|
58
|
+
showStepIndicator,
|
|
53
59
|
},
|
|
54
60
|
}));
|
|
55
61
|
}
|
|
@@ -30,7 +30,7 @@ function TourHint({ backProps, closeProps, primaryProps, skipProps, tooltipProps
|
|
|
30
30
|
const contentId = (0, react_1.useId)();
|
|
31
31
|
// `Partial`, а не `TourStepData`: каст к полному типу прятал бы падение на
|
|
32
32
|
// `buttons.includes(...)` у шага, пришедшего мимо `toJoyrideSteps`.
|
|
33
|
-
const { subtitle, labels, buttons = [] } = (step.data ?? {});
|
|
33
|
+
const { subtitle, labels, buttons = [], showStepIndicator = true } = (step.data ?? {});
|
|
34
34
|
const backButtonProps = withoutNativeTitle(backProps);
|
|
35
35
|
const primaryButtonProps = withoutNativeTitle(primaryProps);
|
|
36
36
|
const closeButtonProps = withoutNativeTitle(closeProps);
|
|
@@ -45,6 +45,6 @@ function TourHint({ backProps, closeProps, primaryProps, skipProps, tooltipProps
|
|
|
45
45
|
(0, jsx_runtime_1.jsxs)("div", { ...tooltipProps, "aria-describedby": step.content ? contentId : undefined, "aria-labelledby": step.title ? titleId : undefined, className: styles_module_scss_1.default.hint, "data-test-id": constants_1.TEST_IDS.hint,
|
|
46
46
|
// Ширину шага движок отдаёт только в свои стили тултипа — у подсказки они свои.
|
|
47
47
|
// `max-width` не трогаем: он держит фолбэк по ширине экрана.
|
|
48
|
-
style: step.width !== undefined ? { width: step.width } : undefined, children: [(step.title || showSkip) && ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.header, children: [step.title && ((0, jsx_runtime_1.jsx)(typography_1.Typography, { as: 'div', id: titleId, variant: 'title', size: 'm', "data-test-id": constants_1.TEST_IDS.title, children: step.title })), showSkip && ((0, jsx_runtime_1.jsx)(button_1.Button, { ...skipButtonProps, className: styles_module_scss_1.default.close, appearance: button_1.APPEARANCE.Neutral, "aria-label": labels.close, "data-test-id": constants_1.TEST_IDS.closeIcon, icon: (0, jsx_runtime_1.jsx)(system_1.CrossSVG, {}), view: button_1.VIEW.Simple }))] })), subtitle && ((0, jsx_runtime_1.jsx)(typography_1.Typography, { as: 'p', className: styles_module_scss_1.default.subtitle, variant: 'title', size: 's', weight: 'thin', "data-test-id": constants_1.TEST_IDS.subtitle, children: subtitle })), step.content && ((0, jsx_runtime_1.jsx)(typography_1.Typography, { as: 'div', id: contentId, className: styles_module_scss_1.default.content, variant: 'body', size: 's', "data-test-id": constants_1.TEST_IDS.content, children: step.content })), (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.footer, children: [size > 1 && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_1.default.visuallyHidden, children: t('progress', { current: index + 1, total: size }) }), (0, jsx_runtime_1.jsx)(TourSteps_1.TourSteps, { current: index, total: size })] })), (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.actions, children: [showBack && ((0, jsx_runtime_1.jsx)(button_1.Button, { ...backButtonProps, appearance: button_1.APPEARANCE.Neutral, "data-test-id": constants_1.TEST_IDS.backButton, label: labels.back, view: button_1.VIEW.Outline })), showPrimary &&
|
|
48
|
+
style: step.width !== undefined ? { width: step.width } : undefined, children: [(step.title || showSkip) && ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.header, children: [step.title && ((0, jsx_runtime_1.jsx)(typography_1.Typography, { as: 'div', id: titleId, variant: 'title', size: 'm', "data-test-id": constants_1.TEST_IDS.title, children: step.title })), showSkip && ((0, jsx_runtime_1.jsx)(button_1.Button, { ...skipButtonProps, className: styles_module_scss_1.default.close, appearance: button_1.APPEARANCE.Neutral, "aria-label": labels.close, "data-test-id": constants_1.TEST_IDS.closeIcon, icon: (0, jsx_runtime_1.jsx)(system_1.CrossSVG, {}), view: button_1.VIEW.Simple }))] })), subtitle && ((0, jsx_runtime_1.jsx)(typography_1.Typography, { as: 'p', className: styles_module_scss_1.default.subtitle, variant: 'title', size: 's', weight: 'thin', "data-test-id": constants_1.TEST_IDS.subtitle, children: subtitle })), step.content && ((0, jsx_runtime_1.jsx)(typography_1.Typography, { as: 'div', id: contentId, className: styles_module_scss_1.default.content, variant: 'body', size: 's', "data-test-id": constants_1.TEST_IDS.content, children: step.content })), (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.footer, children: [showStepIndicator && size > 1 && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_1.default.visuallyHidden, children: t('progress', { current: index + 1, total: size }) }), (0, jsx_runtime_1.jsx)(TourSteps_1.TourSteps, { current: index, total: size })] })), (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.actions, children: [showBack && ((0, jsx_runtime_1.jsx)(button_1.Button, { ...backButtonProps, appearance: button_1.APPEARANCE.Neutral, "data-test-id": constants_1.TEST_IDS.backButton, label: labels.back, view: button_1.VIEW.Outline })), showPrimary &&
|
|
49
49
|
(isLastStep ? ((0, jsx_runtime_1.jsx)(button_1.Button, { ...closeButtonProps, "data-test-id": constants_1.TEST_IDS.finishButton, label: labels.finish, view: button_1.VIEW.Filled })) : ((0, jsx_runtime_1.jsx)(button_1.Button, { ...primaryButtonProps, "data-test-id": constants_1.TEST_IDS.nextButton, label: labels.next, view: button_1.VIEW.Filled })))] })] })] }));
|
|
50
50
|
}
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -113,6 +113,14 @@ export type WelcomeTourProps = {
|
|
|
113
113
|
* @default ['back', 'primary', 'skip']
|
|
114
114
|
*/
|
|
115
115
|
buttons?: TourButton[];
|
|
116
|
+
/**
|
|
117
|
+
* Показывать ли индикатор прогресса. Он и так появляется только у тура длиннее одного
|
|
118
|
+
* шага; `false` убирает его вместе с озвучкой позиции для скринридера — это нужно там,
|
|
119
|
+
* где часть шагов может отвалиться на лету (цели нет на странице, шаг пропускается),
|
|
120
|
+
* и «2 из 5» окажется неправдой.
|
|
121
|
+
* @default true
|
|
122
|
+
*/
|
|
123
|
+
showStepIndicator?: boolean;
|
|
116
124
|
/**
|
|
117
125
|
* Отступ при скролле к целевому элементу, px.
|
|
118
126
|
* @default 20
|
|
@@ -12,4 +12,4 @@ import { WelcomeTourProps } from '../../types.js';
|
|
|
12
12
|
* `data-test-id` компонент не принимает: слоты подсказки адресуются через публичную
|
|
13
13
|
* константу `TEST_IDS`.
|
|
14
14
|
*/
|
|
15
|
-
export declare function WelcomeTour({ steps, open, defaultOpen, onOpenChange, stepIndex, defaultStepIndex, onStepChange, labels: componentLabels, buttons, scrollOffset, spotlightPadding, portalContainer, }: WelcomeTourProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export declare function WelcomeTour({ steps, open, defaultOpen, onOpenChange, stepIndex, defaultStepIndex, onStepChange, labels: componentLabels, buttons, showStepIndicator, scrollOffset, spotlightPadding, portalContainer, }: WelcomeTourProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -7,9 +7,9 @@ import { ACTIONS, EVENTS, Joyride } from 'react-joyride';
|
|
|
7
7
|
import { TOUR_BUTTON, TOUR_STATUS } from '../../constants.js';
|
|
8
8
|
import { TourHint } from '../../helperComponents/index.js';
|
|
9
9
|
import { welcomeTourLocale } from '../../locale/index.js';
|
|
10
|
-
import { TOUR_COLORS, TOUR_GEOMETRY } from './constants.js';
|
|
10
|
+
import { OVERLAY_PRESS_EVENTS, TOUR_COLORS, TOUR_GEOMETRY } from './constants.js';
|
|
11
11
|
import { removeUnionElement } from './spotlight.js';
|
|
12
|
-
import { toJoyrideSteps, toTourStatus } from './utils.js';
|
|
12
|
+
import { isOverlayTarget, toJoyrideSteps, toTourStatus } from './utils.js';
|
|
13
13
|
const DEFAULT_BUTTONS = [TOUR_BUTTON.Back, TOUR_BUTTON.Primary, TOUR_BUTTON.Skip];
|
|
14
14
|
const FLOATING_OPTIONS = { shiftOptions: { crossAxis: true, padding: 16 } };
|
|
15
15
|
/**
|
|
@@ -25,7 +25,7 @@ const FLOATING_OPTIONS = { shiftOptions: { crossAxis: true, padding: 16 } };
|
|
|
25
25
|
* `data-test-id` компонент не принимает: слоты подсказки адресуются через публичную
|
|
26
26
|
* константу `TEST_IDS`.
|
|
27
27
|
*/
|
|
28
|
-
export function WelcomeTour({ steps, open, defaultOpen = false, onOpenChange, stepIndex, defaultStepIndex = 0, onStepChange, labels: componentLabels, buttons = DEFAULT_BUTTONS, scrollOffset, spotlightPadding, portalContainer, }) {
|
|
28
|
+
export function WelcomeTour({ steps, open, defaultOpen = false, onOpenChange, stepIndex, defaultStepIndex = 0, onStepChange, labels: componentLabels, buttons = DEFAULT_BUTTONS, showStepIndicator = true, scrollOffset, spotlightPadding, portalContainer, }) {
|
|
29
29
|
const { t } = welcomeTourLocale.useTranslations();
|
|
30
30
|
const portalContextRef = usePortalContext();
|
|
31
31
|
const [isOpen, setOpen] = useUncontrolledProp(open, defaultOpen, onOpenChange);
|
|
@@ -59,7 +59,8 @@ export function WelcomeTour({ steps, open, defaultOpen = false, onOpenChange, st
|
|
|
59
59
|
labels: { next: t('next'), back: t('back'), finish: t('finish'), close: t('close') },
|
|
60
60
|
componentLabels,
|
|
61
61
|
buttons,
|
|
62
|
-
|
|
62
|
+
showStepIndicator,
|
|
63
|
+
}), [steps, t, componentLabels, buttons, showStepIndicator]);
|
|
63
64
|
const handleEvent = useCallback(({ action, controlled, type, index, status }) => {
|
|
64
65
|
switch (type) {
|
|
65
66
|
// Неуправляемый режим: индекс уже переключил движок, компонент сообщает о факте.
|
|
@@ -106,6 +107,21 @@ export function WelcomeTour({ steps, open, defaultOpen = false, onOpenChange, st
|
|
|
106
107
|
break;
|
|
107
108
|
}
|
|
108
109
|
}, [steps, onStepChange, setOpen]);
|
|
110
|
+
// `overlayClickAction: false` отменяет только реакцию самого тура: нажатие всё равно
|
|
111
|
+
// доходит до документа, и раскрытый список, вокруг которого построен шаг, закрывается
|
|
112
|
+
// как от клика мимо себя. Гасим на подхвате в портале, вместе с уводом фокуса.
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!isOpen || !portalElement)
|
|
115
|
+
return;
|
|
116
|
+
const swallowPress = (event) => {
|
|
117
|
+
if (!isOverlayTarget(event.target))
|
|
118
|
+
return;
|
|
119
|
+
event.stopPropagation();
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
};
|
|
122
|
+
OVERLAY_PRESS_EVENTS.forEach(type => portalElement.addEventListener(type, swallowPress, true));
|
|
123
|
+
return () => OVERLAY_PRESS_EVENTS.forEach(type => portalElement.removeEventListener(type, swallowPress, true));
|
|
124
|
+
}, [isOpen, portalElement]);
|
|
109
125
|
// Esc завершает тур целиком, как у остальных оверлеев ДС; `dismissKeyAction` движка
|
|
110
126
|
// закрывает только текущий шаг, поэтому он выключен. Уступаем чужому слою: шаг тура
|
|
111
127
|
// может открыть диалог, и Esc тогда адресован ему.
|
|
@@ -27,3 +27,7 @@ export declare const TOUR_COLORS: {
|
|
|
27
27
|
* расчётной уезжает за край экрана.
|
|
28
28
|
*/
|
|
29
29
|
export declare const DEFAULT_HINT_WIDTH = 480;
|
|
30
|
+
/** Класс затемнения у react-joyride: отличить его от подсказки в общем портале больше нечем. */
|
|
31
|
+
export declare const OVERLAY_CLASS_NAME = "react-joyride__overlay";
|
|
32
|
+
/** События, по которым закрываются слои страницы: floating-ui слушает `pointerdown`, прочие — `mousedown` и `click`. */
|
|
33
|
+
export declare const OVERLAY_PRESS_EVENTS: readonly ["pointerdown", "mousedown", "click"];
|
|
@@ -33,3 +33,7 @@ export const TOUR_COLORS = {
|
|
|
33
33
|
* расчётной уезжает за край экрана.
|
|
34
34
|
*/
|
|
35
35
|
export const DEFAULT_HINT_WIDTH = 480;
|
|
36
|
+
/** Класс затемнения у react-joyride: отличить его от подсказки в общем портале больше нечем. */
|
|
37
|
+
export const OVERLAY_CLASS_NAME = 'react-joyride__overlay';
|
|
38
|
+
/** События, по которым закрываются слои страницы: floating-ui слушает `pointerdown`, прочие — `mousedown` и `click`. */
|
|
39
|
+
export const OVERLAY_PRESS_EVENTS = ['pointerdown', 'mousedown', 'click'];
|
|
@@ -5,7 +5,10 @@ type ToJoyrideStepsParams = {
|
|
|
5
5
|
labels: TourLabels;
|
|
6
6
|
componentLabels?: Partial<TourLabels>;
|
|
7
7
|
buttons: TourButton[];
|
|
8
|
+
showStepIndicator: boolean;
|
|
8
9
|
};
|
|
10
|
+
/** Пришло ли нажатие в затемнение, а не в подсказку: в портале лежат они оба. */
|
|
11
|
+
export declare function isOverlayTarget(target: EventTarget | null): boolean;
|
|
9
12
|
/**
|
|
10
13
|
* Приводит статус движка к публичному `TourStatus`: наружу сообщаются только терминальные
|
|
11
14
|
* `finished` и `skipped`, а каст пропустил бы любую строку.
|
|
@@ -18,5 +21,5 @@ export declare function resolveLabels(base: TourLabels, componentLabels?: Partia
|
|
|
18
21
|
* (`subtitle`, разрешённые подписи, набор кнопок), уезжают в `step.data` — оттуда
|
|
19
22
|
* их читает `TourHint`.
|
|
20
23
|
*/
|
|
21
|
-
export declare function toJoyrideSteps({ steps, labels, componentLabels, buttons }: ToJoyrideStepsParams): Step[];
|
|
24
|
+
export declare function toJoyrideSteps({ steps, labels, componentLabels, buttons, showStepIndicator, }: ToJoyrideStepsParams): Step[];
|
|
22
25
|
export {};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { TOUR_STATUS } from '../../constants.js';
|
|
2
|
-
import { DEFAULT_HINT_WIDTH } from './constants.js';
|
|
2
|
+
import { DEFAULT_HINT_WIDTH, OVERLAY_CLASS_NAME } from './constants.js';
|
|
3
3
|
import { getUnionElement } from './spotlight.js';
|
|
4
4
|
import { prepareStep, resolveTargets } from './targets.js';
|
|
5
|
+
/** Пришло ли нажатие в затемнение, а не в подсказку: в портале лежат они оба. */
|
|
6
|
+
export function isOverlayTarget(target) {
|
|
7
|
+
return target instanceof Element && Boolean(target.closest(`.${OVERLAY_CLASS_NAME}`));
|
|
8
|
+
}
|
|
5
9
|
/**
|
|
6
10
|
* Приводит статус движка к публичному `TourStatus`: наружу сообщаются только терминальные
|
|
7
11
|
* `finished` и `skipped`, а каст пропустил бы любую строку.
|
|
@@ -19,7 +23,7 @@ export function resolveLabels(base, componentLabels, stepLabels) {
|
|
|
19
23
|
* (`subtitle`, разрешённые подписи, набор кнопок), уезжают в `step.data` — оттуда
|
|
20
24
|
* их читает `TourHint`.
|
|
21
25
|
*/
|
|
22
|
-
export function toJoyrideSteps({ steps, labels, componentLabels, buttons }) {
|
|
26
|
+
export function toJoyrideSteps({ steps, labels, componentLabels, buttons, showStepIndicator, }) {
|
|
23
27
|
return steps.map(({ target, title, subtitle, content, placement, width, spotlightTarget, spotlightPadding, labels: stepLabels, onBeforeShow, }) => ({
|
|
24
28
|
// Со списком целей подсказка цепляется за «зеркальный» узел: раскрытый список
|
|
25
29
|
// закрывается по клику мимо себя, то есть уже на `mousedown` по кнопке подсказки —
|
|
@@ -45,6 +49,7 @@ export function toJoyrideSteps({ steps, labels, componentLabels, buttons }) {
|
|
|
45
49
|
subtitle,
|
|
46
50
|
labels: resolveLabels(labels, componentLabels, stepLabels),
|
|
47
51
|
buttons,
|
|
52
|
+
showStepIndicator,
|
|
48
53
|
},
|
|
49
54
|
}));
|
|
50
55
|
}
|
|
@@ -24,7 +24,7 @@ export function TourHint({ backProps, closeProps, primaryProps, skipProps, toolt
|
|
|
24
24
|
const contentId = useId();
|
|
25
25
|
// `Partial`, а не `TourStepData`: каст к полному типу прятал бы падение на
|
|
26
26
|
// `buttons.includes(...)` у шага, пришедшего мимо `toJoyrideSteps`.
|
|
27
|
-
const { subtitle, labels, buttons = [] } = (step.data ?? {});
|
|
27
|
+
const { subtitle, labels, buttons = [], showStepIndicator = true } = (step.data ?? {});
|
|
28
28
|
const backButtonProps = withoutNativeTitle(backProps);
|
|
29
29
|
const primaryButtonProps = withoutNativeTitle(primaryProps);
|
|
30
30
|
const closeButtonProps = withoutNativeTitle(closeProps);
|
|
@@ -39,6 +39,6 @@ export function TourHint({ backProps, closeProps, primaryProps, skipProps, toolt
|
|
|
39
39
|
_jsxs("div", { ...tooltipProps, "aria-describedby": step.content ? contentId : undefined, "aria-labelledby": step.title ? titleId : undefined, className: styles.hint, "data-test-id": TEST_IDS.hint,
|
|
40
40
|
// Ширину шага движок отдаёт только в свои стили тултипа — у подсказки они свои.
|
|
41
41
|
// `max-width` не трогаем: он держит фолбэк по ширине экрана.
|
|
42
|
-
style: step.width !== undefined ? { width: step.width } : undefined, children: [(step.title || showSkip) && (_jsxs("div", { className: styles.header, children: [step.title && (_jsx(Typography, { as: 'div', id: titleId, variant: 'title', size: 'm', "data-test-id": TEST_IDS.title, children: step.title })), showSkip && (_jsx(Button, { ...skipButtonProps, className: styles.close, appearance: APPEARANCE.Neutral, "aria-label": labels.close, "data-test-id": TEST_IDS.closeIcon, icon: _jsx(CrossSVG, {}), view: VIEW.Simple }))] })), subtitle && (_jsx(Typography, { as: 'p', className: styles.subtitle, variant: 'title', size: 's', weight: 'thin', "data-test-id": TEST_IDS.subtitle, children: subtitle })), step.content && (_jsx(Typography, { as: 'div', id: contentId, className: styles.content, variant: 'body', size: 's', "data-test-id": TEST_IDS.content, children: step.content })), _jsxs("div", { className: styles.footer, children: [size > 1 && (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.visuallyHidden, children: t('progress', { current: index + 1, total: size }) }), _jsx(TourSteps, { current: index, total: size })] })), _jsxs("div", { className: styles.actions, children: [showBack && (_jsx(Button, { ...backButtonProps, appearance: APPEARANCE.Neutral, "data-test-id": TEST_IDS.backButton, label: labels.back, view: VIEW.Outline })), showPrimary &&
|
|
42
|
+
style: step.width !== undefined ? { width: step.width } : undefined, children: [(step.title || showSkip) && (_jsxs("div", { className: styles.header, children: [step.title && (_jsx(Typography, { as: 'div', id: titleId, variant: 'title', size: 'm', "data-test-id": TEST_IDS.title, children: step.title })), showSkip && (_jsx(Button, { ...skipButtonProps, className: styles.close, appearance: APPEARANCE.Neutral, "aria-label": labels.close, "data-test-id": TEST_IDS.closeIcon, icon: _jsx(CrossSVG, {}), view: VIEW.Simple }))] })), subtitle && (_jsx(Typography, { as: 'p', className: styles.subtitle, variant: 'title', size: 's', weight: 'thin', "data-test-id": TEST_IDS.subtitle, children: subtitle })), step.content && (_jsx(Typography, { as: 'div', id: contentId, className: styles.content, variant: 'body', size: 's', "data-test-id": TEST_IDS.content, children: step.content })), _jsxs("div", { className: styles.footer, children: [showStepIndicator && size > 1 && (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.visuallyHidden, children: t('progress', { current: index + 1, total: size }) }), _jsx(TourSteps, { current: index, total: size })] })), _jsxs("div", { className: styles.actions, children: [showBack && (_jsx(Button, { ...backButtonProps, appearance: APPEARANCE.Neutral, "data-test-id": TEST_IDS.backButton, label: labels.back, view: VIEW.Outline })), showPrimary &&
|
|
43
43
|
(isLastStep ? (_jsx(Button, { ...closeButtonProps, "data-test-id": TEST_IDS.finishButton, label: labels.finish, view: VIEW.Filled })) : (_jsx(Button, { ...primaryButtonProps, "data-test-id": TEST_IDS.nextButton, label: labels.next, view: VIEW.Filled })))] })] })] }));
|
|
44
44
|
}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -113,6 +113,14 @@ export type WelcomeTourProps = {
|
|
|
113
113
|
* @default ['back', 'primary', 'skip']
|
|
114
114
|
*/
|
|
115
115
|
buttons?: TourButton[];
|
|
116
|
+
/**
|
|
117
|
+
* Показывать ли индикатор прогресса. Он и так появляется только у тура длиннее одного
|
|
118
|
+
* шага; `false` убирает его вместе с озвучкой позиции для скринридера — это нужно там,
|
|
119
|
+
* где часть шагов может отвалиться на лету (цели нет на странице, шаг пропускается),
|
|
120
|
+
* и «2 из 5» окажется неправдой.
|
|
121
|
+
* @default true
|
|
122
|
+
*/
|
|
123
|
+
showStepIndicator?: boolean;
|
|
116
124
|
/**
|
|
117
125
|
* Отступ при скролле к целевому элементу, px.
|
|
118
126
|
* @default 20
|