@matthesketh/react-guidetour 1.0.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.
@@ -0,0 +1,238 @@
1
+ import * as React from 'react';
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import treeChanges from 'tree-changes';
4
+
5
+ import {
6
+ getClientRect,
7
+ getDocumentHeight,
8
+ getElement,
9
+ getElementPosition,
10
+ getScrollParent,
11
+ hasCustomScrollParent,
12
+ hasPosition,
13
+ } from '~/modules/dom';
14
+ import { isSafari, log } from '~/modules/helpers';
15
+
16
+ import { LIFECYCLE } from '~/literals';
17
+
18
+ import { Lifecycle, OverlayProps } from '~/types';
19
+
20
+ import Spotlight from './Spotlight';
21
+
22
+ interface SpotlightStyles extends React.CSSProperties {
23
+ height: number;
24
+ left: number;
25
+ top: number;
26
+ width: number;
27
+ }
28
+
29
+ export default function JoyrideOverlay(props: OverlayProps) {
30
+ const {
31
+ continuous,
32
+ debug,
33
+ disableOverlay,
34
+ disableOverlayClose,
35
+ disableScrolling,
36
+ disableScrollParentFix = false,
37
+ lifecycle,
38
+ onClickOverlay,
39
+ placement,
40
+ spotlightClicks,
41
+ spotlightPadding = 0,
42
+ styles,
43
+ target,
44
+ } = props;
45
+
46
+ const [isScrolling, setIsScrolling] = useState(false);
47
+ const [mouseOverSpotlight, setMouseOverSpotlight] = useState(false);
48
+ const [showSpotlight, setShowSpotlight] = useState(true);
49
+ const [, setForceRender] = useState(0);
50
+
51
+ const isActiveRef = useRef(false);
52
+ const resizeTimeoutRef = useRef<number | undefined>(undefined);
53
+ const scrollTimeoutRef = useRef<number | undefined>(undefined);
54
+ const scrollParentRef = useRef<Document | Element | undefined>(undefined);
55
+ const previousPropsRef = useRef(props);
56
+
57
+ const getSpotlightStyles = useCallback((): SpotlightStyles => {
58
+ const element = getElement(target);
59
+ const elementRect = getClientRect(element);
60
+ const isFixedTarget = hasPosition(element);
61
+ const top = getElementPosition(element, spotlightPadding, disableScrollParentFix);
62
+
63
+ return {
64
+ ...styles.spotlight,
65
+ height: Math.round((elementRect?.height ?? 0) + spotlightPadding * 2),
66
+ left: Math.round((elementRect?.left ?? 0) - spotlightPadding),
67
+ opacity: showSpotlight ? 1 : 0,
68
+ pointerEvents: spotlightClicks ? 'none' : 'auto',
69
+ position: isFixedTarget ? 'fixed' : 'absolute',
70
+ top,
71
+ transition: 'opacity 0.2s',
72
+ width: Math.round((elementRect?.width ?? 0) + spotlightPadding * 2),
73
+ } satisfies React.CSSProperties;
74
+ }, [target, spotlightPadding, disableScrollParentFix, styles.spotlight, showSpotlight, spotlightClicks]);
75
+
76
+ const handleMouseMove = useCallback((event: MouseEvent) => {
77
+ const spotStyles = getSpotlightStyles();
78
+ const { height, left, position, top, width } = spotStyles;
79
+
80
+ const offsetY = position === 'fixed' ? event.clientY : event.pageY;
81
+ const offsetX = position === 'fixed' ? event.clientX : event.pageX;
82
+ const inSpotlightHeight = offsetY >= top && offsetY <= top + height;
83
+ const inSpotlightWidth = offsetX >= left && offsetX <= left + width;
84
+ const inSpotlight = inSpotlightWidth && inSpotlightHeight;
85
+
86
+ setMouseOverSpotlight(prev => {
87
+ if (inSpotlight !== prev) {
88
+ return inSpotlight;
89
+ }
90
+ return prev;
91
+ });
92
+ }, [getSpotlightStyles]);
93
+
94
+ const handleScroll = useCallback(() => {
95
+ const element = getElement(target);
96
+
97
+ if (scrollParentRef.current !== document) {
98
+ setIsScrolling(prev => {
99
+ if (!prev) {
100
+ setShowSpotlight(false);
101
+ return true;
102
+ }
103
+ return prev;
104
+ });
105
+
106
+ clearTimeout(scrollTimeoutRef.current);
107
+
108
+ scrollTimeoutRef.current = window.setTimeout(() => {
109
+ if (isActiveRef.current) {
110
+ setIsScrolling(false);
111
+ setShowSpotlight(true);
112
+ }
113
+ }, 50);
114
+ } else if (hasPosition(element, 'sticky')) {
115
+ setForceRender(c => c + 1);
116
+ }
117
+ }, [target]);
118
+
119
+ const handleResize = useCallback(() => {
120
+ clearTimeout(resizeTimeoutRef.current);
121
+
122
+ resizeTimeoutRef.current = window.setTimeout(() => {
123
+ if (isActiveRef.current) {
124
+ setForceRender(c => c + 1);
125
+ }
126
+ }, 100);
127
+ }, []);
128
+
129
+ // Mount/unmount
130
+ useEffect(() => {
131
+ const element = getElement(target);
132
+ scrollParentRef.current = getScrollParent(element ?? document.body, disableScrollParentFix, true);
133
+ isActiveRef.current = true;
134
+
135
+ if (process.env.NODE_ENV !== 'production') {
136
+ if (!disableScrolling && hasCustomScrollParent(element, true)) {
137
+ log({
138
+ title: 'step has a custom scroll parent and can cause trouble with scrolling',
139
+ data: [{ key: 'parent', value: scrollParentRef.current }],
140
+ debug,
141
+ });
142
+ }
143
+ }
144
+
145
+ window.addEventListener('resize', handleResize);
146
+
147
+ return () => {
148
+ isActiveRef.current = false;
149
+ window.removeEventListener('mousemove', handleMouseMove);
150
+ window.removeEventListener('resize', handleResize);
151
+ clearTimeout(resizeTimeoutRef.current);
152
+ clearTimeout(scrollTimeoutRef.current);
153
+ scrollParentRef.current?.removeEventListener('scroll', handleScroll);
154
+ };
155
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
156
+
157
+ // componentDidUpdate equivalent
158
+ useEffect(() => {
159
+ const previousProps = previousPropsRef.current;
160
+ const { changed } = treeChanges(previousProps, props);
161
+
162
+ if (changed('target') || changed('disableScrollParentFix')) {
163
+ const element = getElement(target);
164
+ scrollParentRef.current = getScrollParent(element ?? document.body, disableScrollParentFix, true);
165
+ }
166
+
167
+ if (changed('lifecycle', LIFECYCLE.TOOLTIP)) {
168
+ scrollParentRef.current?.addEventListener('scroll', handleScroll, { passive: true });
169
+
170
+ setTimeout(() => {
171
+ if (!isScrolling) {
172
+ setShowSpotlight(true);
173
+ }
174
+ }, 100);
175
+ }
176
+
177
+ if (changed('spotlightClicks') || changed('disableOverlay') || changed('lifecycle')) {
178
+ if (spotlightClicks && lifecycle === LIFECYCLE.TOOLTIP) {
179
+ window.addEventListener('mousemove', handleMouseMove, false);
180
+ } else if (lifecycle !== LIFECYCLE.TOOLTIP) {
181
+ window.removeEventListener('mousemove', handleMouseMove);
182
+ }
183
+ }
184
+
185
+ previousPropsRef.current = props;
186
+ });
187
+
188
+ const hideSpotlight = () => {
189
+ const hiddenLifecycles = [
190
+ LIFECYCLE.INIT,
191
+ LIFECYCLE.BEACON,
192
+ LIFECYCLE.COMPLETE,
193
+ LIFECYCLE.ERROR,
194
+ ] as Lifecycle[];
195
+
196
+ return (
197
+ disableOverlay ||
198
+ (continuous ? hiddenLifecycles.includes(lifecycle) : lifecycle !== LIFECYCLE.TOOLTIP)
199
+ );
200
+ };
201
+
202
+ if (hideSpotlight()) {
203
+ return null;
204
+ }
205
+
206
+ const spotlightStyles = getSpotlightStyles();
207
+
208
+ const overlayStyles: React.CSSProperties = {
209
+ cursor: disableOverlayClose ? 'default' : 'pointer',
210
+ height: getDocumentHeight(),
211
+ pointerEvents: mouseOverSpotlight ? 'none' : 'auto',
212
+ ...styles.overlay,
213
+ };
214
+
215
+ let spotlight = placement !== 'center' && showSpotlight && (
216
+ <Spotlight styles={spotlightStyles} />
217
+ );
218
+
219
+ // Hack for Safari bug with mix-blend-mode with z-index
220
+ if (isSafari()) {
221
+ const { mixBlendMode, zIndex, ...safariOverlay } = overlayStyles;
222
+
223
+ spotlight = <div style={{ ...safariOverlay }}>{spotlight}</div>;
224
+ delete overlayStyles.backgroundColor;
225
+ }
226
+
227
+ return (
228
+ <div
229
+ className="react-joyride__overlay"
230
+ data-test-id="overlay"
231
+ onClick={onClickOverlay}
232
+ role="presentation"
233
+ style={overlayStyles}
234
+ >
235
+ {spotlight}
236
+ </div>
237
+ );
238
+ }
@@ -0,0 +1,37 @@
1
+ import { ReactElement, useEffect, useRef } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ import { canUseDOM } from '~/modules/dom';
5
+
6
+ interface Props {
7
+ children: ReactElement;
8
+ id: string;
9
+ }
10
+
11
+ export default function JoyridePortal({ children, id }: Props) {
12
+ const nodeRef = useRef<HTMLElement | null>(null);
13
+
14
+ useEffect(() => {
15
+ if (!canUseDOM()) {
16
+ return;
17
+ }
18
+
19
+ const node = document.createElement('div');
20
+ node.id = id;
21
+ document.body.appendChild(node);
22
+ nodeRef.current = node;
23
+
24
+ return () => {
25
+ if (node.parentNode === document.body) {
26
+ document.body.removeChild(node);
27
+ }
28
+ nodeRef.current = null;
29
+ };
30
+ }, [id]);
31
+
32
+ if (!canUseDOM() || !nodeRef.current) {
33
+ return null;
34
+ }
35
+
36
+ return createPortal(children, nodeRef.current);
37
+ }
@@ -0,0 +1,18 @@
1
+ import * as React from 'react';
2
+
3
+ interface Props {
4
+ styles: React.CSSProperties;
5
+ }
6
+
7
+ function JoyrideSpotlight({ styles }: Props) {
8
+ return (
9
+ <div
10
+ key="JoyrideSpotlight"
11
+ className="react-joyride__spotlight"
12
+ data-test-id="spotlight"
13
+ style={styles}
14
+ />
15
+ );
16
+ }
17
+
18
+ export default JoyrideSpotlight;
@@ -0,0 +1,255 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ import Floater, { Props as FloaterProps } from 'react-floater';
3
+ import is from 'is-lite';
4
+ import treeChanges from 'tree-changes';
5
+
6
+ import { getElement, isElementVisible } from '~/modules/dom';
7
+ import { hideBeacon, log } from '~/modules/helpers';
8
+ import Scope from '~/modules/scope';
9
+ import { validateStep } from '~/modules/step';
10
+
11
+ import { ACTIONS, EVENTS, LIFECYCLE, STATUS } from '~/literals';
12
+
13
+ import { StepProps } from '~/types';
14
+
15
+ import Beacon from './Beacon';
16
+ import Tooltip from './Tooltip/index';
17
+
18
+ export default function JoyrideStep(props: StepProps) {
19
+ const {
20
+ action,
21
+ callback,
22
+ continuous,
23
+ controlled,
24
+ debug,
25
+ helpers,
26
+ index,
27
+ lifecycle,
28
+ nonce,
29
+ shouldScroll,
30
+ size,
31
+ status,
32
+ step,
33
+ store,
34
+ } = props;
35
+
36
+ const scopeRef = useRef<Scope | null>(null);
37
+ const tooltipRef = useRef<HTMLElement | null>(null);
38
+ const previousPropsRef = useRef(props);
39
+
40
+ const setTooltipRef = useCallback((element: HTMLElement) => {
41
+ tooltipRef.current = element;
42
+ }, []);
43
+
44
+ const handleClickHoverBeacon = useCallback((event: React.MouseEvent<HTMLElement>) => {
45
+ if (event.type === 'mouseenter' && step.event !== 'hover') {
46
+ return;
47
+ }
48
+
49
+ store.update({ lifecycle: LIFECYCLE.TOOLTIP });
50
+ }, [step.event, store]);
51
+
52
+ const setPopper: FloaterProps['getPopper'] = useCallback((popper: any, type: any) => {
53
+ if (type === 'wrapper') {
54
+ store.setPopper('beacon', popper);
55
+ } else {
56
+ store.setPopper('tooltip', popper);
57
+ }
58
+
59
+ if (
60
+ store.getPopper('beacon') &&
61
+ (store.getPopper('tooltip') || step.placement === 'center') &&
62
+ lifecycle === LIFECYCLE.INIT
63
+ ) {
64
+ store.update({
65
+ action,
66
+ lifecycle: LIFECYCLE.READY,
67
+ });
68
+ }
69
+
70
+ if (step.floaterProps?.getPopper) {
71
+ step.floaterProps.getPopper(popper, type);
72
+ }
73
+ }, [action, lifecycle, step.placement, step.floaterProps, store]);
74
+
75
+ // Log on mount
76
+ useEffect(() => {
77
+ log({
78
+ title: `step:${index}`,
79
+ data: [{ key: 'props', value: props }],
80
+ debug,
81
+ });
82
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
83
+
84
+ // Cleanup scope on unmount
85
+ useEffect(() => {
86
+ return () => {
87
+ scopeRef.current?.removeScope();
88
+ };
89
+ }, []);
90
+
91
+ // componentDidUpdate equivalent
92
+ useEffect(() => {
93
+ const previousProps = previousPropsRef.current;
94
+ const { changed, changedFrom } = treeChanges(previousProps, props);
95
+ const state = helpers.info();
96
+
97
+ const skipBeacon =
98
+ continuous && action !== ACTIONS.CLOSE && (index > 0 || action === ACTIONS.PREV);
99
+ const hasStoreChanged =
100
+ changed('action') || changed('index') || changed('lifecycle') || changed('status');
101
+ const isInitial = changedFrom('lifecycle', [LIFECYCLE.TOOLTIP, LIFECYCLE.INIT], LIFECYCLE.INIT);
102
+ const isAfterAction = changed('action', [
103
+ ACTIONS.NEXT,
104
+ ACTIONS.PREV,
105
+ ACTIONS.SKIP,
106
+ ACTIONS.CLOSE,
107
+ ]);
108
+ const isControlled = controlled && index === previousProps.index;
109
+
110
+ if (isAfterAction && (isInitial || isControlled)) {
111
+ callback({
112
+ ...state,
113
+ index: previousProps.index,
114
+ lifecycle: LIFECYCLE.COMPLETE,
115
+ step: previousProps.step,
116
+ type: EVENTS.STEP_AFTER,
117
+ });
118
+ }
119
+
120
+ if (
121
+ step.placement === 'center' &&
122
+ status === STATUS.RUNNING &&
123
+ changed('index') &&
124
+ action !== ACTIONS.START &&
125
+ lifecycle === LIFECYCLE.INIT
126
+ ) {
127
+ store.update({ lifecycle: LIFECYCLE.READY });
128
+ }
129
+
130
+ if (hasStoreChanged) {
131
+ const element = getElement(step.target);
132
+ const elementExists = !!element;
133
+ const hasRenderedTarget = elementExists && isElementVisible(element);
134
+
135
+ if (hasRenderedTarget) {
136
+ if (
137
+ changedFrom('status', STATUS.READY, STATUS.RUNNING) ||
138
+ changedFrom('lifecycle', LIFECYCLE.INIT, LIFECYCLE.READY)
139
+ ) {
140
+ callback({
141
+ ...state,
142
+ step,
143
+ type: EVENTS.STEP_BEFORE,
144
+ });
145
+ }
146
+ } else {
147
+ // eslint-disable-next-line no-console
148
+ console.warn(elementExists ? 'Target not visible' : 'Target not mounted', step);
149
+ callback({
150
+ ...state,
151
+ type: EVENTS.TARGET_NOT_FOUND,
152
+ step,
153
+ });
154
+
155
+ if (!controlled) {
156
+ store.update({ index: index + (action === ACTIONS.PREV ? -1 : 1) });
157
+ }
158
+ }
159
+ }
160
+
161
+ if (changedFrom('lifecycle', LIFECYCLE.INIT, LIFECYCLE.READY)) {
162
+ store.update({
163
+ lifecycle: hideBeacon(step) || skipBeacon ? LIFECYCLE.TOOLTIP : LIFECYCLE.BEACON,
164
+ });
165
+ }
166
+
167
+ if (changed('index')) {
168
+ log({
169
+ title: `step:${lifecycle}`,
170
+ data: [{ key: 'props', value: props }],
171
+ debug,
172
+ });
173
+ }
174
+
175
+ if (changed('lifecycle', LIFECYCLE.BEACON)) {
176
+ callback({
177
+ ...state,
178
+ step,
179
+ type: EVENTS.BEACON,
180
+ });
181
+ }
182
+
183
+ if (changed('lifecycle', LIFECYCLE.TOOLTIP)) {
184
+ callback({
185
+ ...state,
186
+ step,
187
+ type: EVENTS.TOOLTIP,
188
+ });
189
+
190
+ if (shouldScroll && tooltipRef.current) {
191
+ scopeRef.current = new Scope(tooltipRef.current, { selector: '[data-action=primary]' });
192
+ scopeRef.current.setFocus();
193
+ }
194
+ }
195
+
196
+ if (changedFrom('lifecycle', [LIFECYCLE.TOOLTIP, LIFECYCLE.INIT], LIFECYCLE.INIT)) {
197
+ scopeRef.current?.removeScope();
198
+ store.cleanupPoppers();
199
+ }
200
+
201
+ previousPropsRef.current = props;
202
+ });
203
+
204
+ const target = getElement(step.target);
205
+
206
+ if (!validateStep(step) || !is.domElement(target)) {
207
+ return null;
208
+ }
209
+
210
+ const isOpen = hideBeacon(step) || lifecycle === LIFECYCLE.TOOLTIP;
211
+
212
+ const TooltipComponent = () => (
213
+ <Tooltip
214
+ continuous={continuous}
215
+ helpers={helpers}
216
+ index={index}
217
+ isLastStep={index + 1 === size}
218
+ setTooltipRef={setTooltipRef}
219
+ size={size}
220
+ step={step}
221
+ />
222
+ );
223
+
224
+ // Omit content/component from step.floaterProps to avoid conflict with RequireExactlyOne
225
+ const { content: _c, component: _comp, ...safeFloaterProps } = (step.floaterProps ?? {}) as any;
226
+
227
+ return (
228
+ <div key={`JoyrideStep-${index}`} className="react-joyride__step">
229
+ <Floater
230
+ {...safeFloaterProps}
231
+ component={TooltipComponent}
232
+ debug={debug}
233
+ getPopper={setPopper}
234
+ id={`react-joyride-step-${index}`}
235
+ open={isOpen}
236
+ placement={step.placement}
237
+ target={step.target}
238
+ >
239
+ <Beacon
240
+ beaconComponent={step.beaconComponent}
241
+ continuous={continuous}
242
+ index={index}
243
+ isLastStep={index + 1 === size}
244
+ locale={step.locale}
245
+ nonce={nonce}
246
+ onClickOrHover={handleClickHoverBeacon}
247
+ shouldFocus={shouldScroll}
248
+ size={size}
249
+ step={step}
250
+ styles={step.styles}
251
+ />
252
+ </Floater>
253
+ </div>
254
+ );
255
+ }
@@ -0,0 +1,31 @@
1
+ import React, { CSSProperties } from 'react';
2
+
3
+ interface Props {
4
+ styles: CSSProperties;
5
+ }
6
+
7
+ function JoyrideTooltipCloseButton({ styles, ...props }: Props) {
8
+ const { color, height, width, ...style } = styles;
9
+
10
+ return (
11
+ <button style={style} type="button" {...props}>
12
+ <svg
13
+ height={typeof height === 'number' ? `${height}px` : height}
14
+ preserveAspectRatio="xMidYMid"
15
+ version="1.1"
16
+ viewBox="0 0 18 18"
17
+ width={typeof width === 'number' ? `${width}px` : width}
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ >
20
+ <g>
21
+ <path
22
+ d="M8.13911129,9.00268191 L0.171521827,17.0258467 C-0.0498027049,17.248715 -0.0498027049,17.6098394 0.171521827,17.8327545 C0.28204354,17.9443526 0.427188206,17.9998706 0.572051765,17.9998706 C0.71714958,17.9998706 0.862013139,17.9443526 0.972581703,17.8327545 L9.0000937,9.74924618 L17.0276057,17.8327545 C17.1384085,17.9443526 17.2832721,17.9998706 17.4281356,17.9998706 C17.5729992,17.9998706 17.718097,17.9443526 17.8286656,17.8327545 C18.0499901,17.6098862 18.0499901,17.2487618 17.8286656,17.0258467 L9.86135722,9.00268191 L17.8340066,0.973848225 C18.0553311,0.750979934 18.0553311,0.389855532 17.8340066,0.16694039 C17.6126821,-0.0556467968 17.254037,-0.0556467968 17.0329467,0.16694039 L9.00042166,8.25611765 L0.967006424,0.167268345 C0.745681892,-0.0553188426 0.387317931,-0.0553188426 0.165993399,0.167268345 C-0.0553311331,0.390136635 -0.0553311331,0.751261038 0.165993399,0.974176179 L8.13920499,9.00268191 L8.13911129,9.00268191 Z"
23
+ fill={color}
24
+ />
25
+ </g>
26
+ </svg>
27
+ </button>
28
+ );
29
+ }
30
+
31
+ export default JoyrideTooltipCloseButton;
@@ -0,0 +1,75 @@
1
+ import * as React from 'react';
2
+
3
+ import { getReactNodeText } from '~/modules/helpers';
4
+
5
+ import { TooltipRenderProps } from '~/types';
6
+
7
+ import CloseButton from './CloseButton';
8
+
9
+ function JoyrideTooltipContainer(props: TooltipRenderProps) {
10
+ const { backProps, closeProps, index, isLastStep, primaryProps, skipProps, step, tooltipProps } =
11
+ props;
12
+ const { content, hideBackButton, hideCloseButton, hideFooter, showSkipButton, styles, title } =
13
+ step;
14
+ const output: Record<string, React.ReactNode> = {};
15
+
16
+ output.primary = (
17
+ <button
18
+ data-test-id="button-primary"
19
+ style={styles.buttonNext}
20
+ type="button"
21
+ {...primaryProps}
22
+ />
23
+ );
24
+
25
+ if (showSkipButton && !isLastStep) {
26
+ output.skip = (
27
+ <button
28
+ aria-live="off"
29
+ data-test-id="button-skip"
30
+ style={styles.buttonSkip}
31
+ type="button"
32
+ {...skipProps}
33
+ />
34
+ );
35
+ }
36
+
37
+ if (!hideBackButton && index > 0) {
38
+ output.back = (
39
+ <button data-test-id="button-back" style={styles.buttonBack} type="button" {...backProps} />
40
+ );
41
+ }
42
+
43
+ output.close = !hideCloseButton && (
44
+ <CloseButton data-test-id="button-close" styles={styles.buttonClose} {...closeProps} />
45
+ );
46
+
47
+ return (
48
+ <div
49
+ key="JoyrideTooltip"
50
+ aria-label={getReactNodeText(title ?? content)}
51
+ className="react-joyride__tooltip"
52
+ style={styles.tooltip}
53
+ {...tooltipProps}
54
+ >
55
+ <div style={styles.tooltipContainer}>
56
+ {title && (
57
+ <h1 aria-label={getReactNodeText(title)} style={styles.tooltipTitle}>
58
+ {title}
59
+ </h1>
60
+ )}
61
+ <div style={styles.tooltipContent}>{content}</div>
62
+ </div>
63
+ {!hideFooter && (
64
+ <div style={styles.tooltipFooter}>
65
+ <div style={styles.tooltipFooterSpacer}>{output.skip}</div>
66
+ {output.back}
67
+ {output.primary}
68
+ </div>
69
+ )}
70
+ {output.close}
71
+ </div>
72
+ );
73
+ }
74
+
75
+ export default JoyrideTooltipContainer;