@numidev/react-joyride 1.0.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.
Files changed (51) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +93 -0
  3. package/dist/index.cjs +2677 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +753 -0
  6. package/dist/index.d.mts +753 -0
  7. package/dist/index.mjs +2638 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +158 -0
  10. package/src/components/Arrow.tsx +138 -0
  11. package/src/components/Beacon.tsx +141 -0
  12. package/src/components/Floater.tsx +381 -0
  13. package/src/components/Loader.tsx +80 -0
  14. package/src/components/Overlay.tsx +167 -0
  15. package/src/components/Portal.tsx +17 -0
  16. package/src/components/Step.tsx +72 -0
  17. package/src/components/Tooltip/CloseButton.tsx +29 -0
  18. package/src/components/Tooltip/DefaultTooltip.tsx +82 -0
  19. package/src/components/Tooltip/index.tsx +163 -0
  20. package/src/components/TourRenderer.tsx +157 -0
  21. package/src/defaults.ts +64 -0
  22. package/src/global.d.ts +8 -0
  23. package/src/hooks/useControls.ts +219 -0
  24. package/src/hooks/useDebugLogger.ts +58 -0
  25. package/src/hooks/useEventEmitter.ts +55 -0
  26. package/src/hooks/useFocusTrap.ts +72 -0
  27. package/src/hooks/useJoyride.tsx +32 -0
  28. package/src/hooks/useLifecycleEffect.ts +512 -0
  29. package/src/hooks/usePortalElement.ts +49 -0
  30. package/src/hooks/usePropSync.ts +84 -0
  31. package/src/hooks/useScrollEffect.ts +217 -0
  32. package/src/hooks/useTargetPosition.ts +154 -0
  33. package/src/hooks/useTourEngine.ts +106 -0
  34. package/src/index.tsx +23 -0
  35. package/src/literals/index.ts +61 -0
  36. package/src/modules/changes.ts +20 -0
  37. package/src/modules/dom.ts +359 -0
  38. package/src/modules/helpers.tsx +230 -0
  39. package/src/modules/step.ts +156 -0
  40. package/src/modules/store.ts +215 -0
  41. package/src/modules/svg.ts +40 -0
  42. package/src/styles.ts +183 -0
  43. package/src/types/common.ts +315 -0
  44. package/src/types/components.ts +84 -0
  45. package/src/types/events.ts +89 -0
  46. package/src/types/floating.ts +60 -0
  47. package/src/types/index.ts +8 -0
  48. package/src/types/props.ts +124 -0
  49. package/src/types/state.ts +49 -0
  50. package/src/types/step.ts +108 -0
  51. package/src/types/utilities.ts +26 -0
@@ -0,0 +1,138 @@
1
+ import type { CSSProperties, ElementType, Ref } from 'react';
2
+
3
+ import type { ArrowRenderProps, Placement } from '~/types';
4
+
5
+ interface ArrowProps {
6
+ arrowComponent?: ElementType<ArrowRenderProps>;
7
+ arrowRef: Ref<HTMLElement>;
8
+ base: number;
9
+ placement: Placement;
10
+ position: { x?: number; y?: number } | undefined;
11
+ size: number;
12
+ styles: CSSProperties;
13
+ }
14
+
15
+ function getDimensions(placement: Placement, base: number, size: number) {
16
+ const [side] = placement.split('-');
17
+
18
+ switch (side) {
19
+ case 'top':
20
+ case 'bottom':
21
+ return { width: base, height: size };
22
+ case 'left':
23
+ case 'right':
24
+ return { width: size, height: base };
25
+ default:
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function getPoints(placement: Placement, base: number, size: number) {
31
+ const [side] = placement.split('-');
32
+
33
+ switch (side) {
34
+ case 'top':
35
+ return {
36
+ points: `0,0 ${base / 2},${size} ${base},0`,
37
+ ...getDimensions(placement, base, size),
38
+ };
39
+ case 'bottom':
40
+ return {
41
+ points: `${base},${size} ${base / 2},0 0,${size}`,
42
+ ...getDimensions(placement, base, size),
43
+ };
44
+ case 'left':
45
+ return {
46
+ points: `0,0 ${size},${base / 2} 0,${base}`,
47
+ ...getDimensions(placement, base, size),
48
+ };
49
+ case 'right':
50
+ return {
51
+ points: `${size},${base} ${size},0 0,${base / 2}`,
52
+ ...getDimensions(placement, base, size),
53
+ };
54
+ default:
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function getPositionStyle(
60
+ placement: Placement,
61
+ position: { x?: number; y?: number } | undefined,
62
+ size: number,
63
+ base: number,
64
+ ): CSSProperties {
65
+ if (!position) {
66
+ return {};
67
+ }
68
+
69
+ const [side] = placement.split('-');
70
+
71
+ switch (side) {
72
+ case 'top':
73
+ return { bottom: -size, left: position.x ?? 0, ...getDimensions(placement, base, size) };
74
+ case 'bottom':
75
+ return { top: -size, left: position.x ?? 0, ...getDimensions(placement, base, size) };
76
+ case 'left':
77
+ return { right: -size, top: position.y ?? 0, ...getDimensions(placement, base, size) };
78
+ case 'right':
79
+ return { left: -size, top: position.y ?? 0, ...getDimensions(placement, base, size) };
80
+ default:
81
+ return {};
82
+ }
83
+ }
84
+
85
+ export default function Arrow({
86
+ arrowComponent,
87
+ arrowRef,
88
+ base,
89
+ placement,
90
+ position,
91
+ size,
92
+ styles,
93
+ }: ArrowProps) {
94
+ const ArrowComponent = arrowComponent;
95
+
96
+ let content = null;
97
+
98
+ if (ArrowComponent) {
99
+ const dimensions = getDimensions(placement, base, size);
100
+
101
+ if (!dimensions) {
102
+ return null;
103
+ }
104
+
105
+ content = (
106
+ <span style={{ flexShrink: 0 }}>
107
+ <ArrowComponent base={base} placement={placement} size={size} />
108
+ </span>
109
+ );
110
+ } else {
111
+ const svg = getPoints(placement, base, size);
112
+
113
+ if (!svg) {
114
+ return null;
115
+ }
116
+
117
+ content = (
118
+ <svg height={svg.height} width={svg.width} xmlns="http://www.w3.org/2000/svg">
119
+ <polygon fill="currentColor" points={svg.points} />
120
+ </svg>
121
+ );
122
+ }
123
+
124
+ return (
125
+ <span
126
+ ref={arrowRef}
127
+ className="react-joyride__arrow"
128
+ data-testid="arrow"
129
+ style={{
130
+ ...styles,
131
+ ...getPositionStyle(placement, position, size, base),
132
+ ...(position ? {} : { visibility: 'hidden' }),
133
+ }}
134
+ >
135
+ {content}
136
+ </span>
137
+ );
138
+ }
@@ -0,0 +1,141 @@
1
+ import { type MouseEventHandler, useEffect, useRef } from 'react';
2
+ import is from 'is-lite';
3
+
4
+ import { getReactNodeText, noop } from '~/modules/helpers';
5
+
6
+ import type { BeaconRenderProps, Locale, Props, Simplify, Styles } from '~/types';
7
+
8
+ type BeaconProps = Simplify<
9
+ Pick<Props, 'beaconComponent' | 'nonce'> &
10
+ BeaconRenderProps & {
11
+ locale: Locale;
12
+ onInteract: MouseEventHandler<HTMLElement>;
13
+ shouldFocus: boolean;
14
+ styles: Styles;
15
+ }
16
+ >;
17
+
18
+ export default function JoyrideBeacon(props: BeaconProps) {
19
+ const {
20
+ beaconComponent,
21
+ continuous,
22
+ index,
23
+ isLastStep,
24
+ locale,
25
+ nonce,
26
+ onInteract,
27
+ shouldFocus,
28
+ size,
29
+ step,
30
+ styles,
31
+ } = props;
32
+ const beaconRef = useRef<HTMLButtonElement | null>(null);
33
+
34
+ const hasBeaconComponent = Boolean(beaconComponent);
35
+
36
+ useEffect(() => {
37
+ if (hasBeaconComponent) {
38
+ return noop;
39
+ }
40
+
41
+ if (document.getElementById('joyride-beacon-animation')) {
42
+ return noop;
43
+ }
44
+
45
+ const style = document.createElement('style');
46
+
47
+ style.id = 'joyride-beacon-animation';
48
+
49
+ if (nonce) {
50
+ style.setAttribute('nonce', nonce);
51
+ }
52
+
53
+ const css = `
54
+ @keyframes joyride-beacon-inner {
55
+ 20% {
56
+ opacity: 0.9;
57
+ }
58
+
59
+ 90% {
60
+ opacity: 0.7;
61
+ }
62
+ }
63
+
64
+ @keyframes joyride-beacon-outer {
65
+ 0% {
66
+ transform: scale(1);
67
+ }
68
+
69
+ 45% {
70
+ opacity: 0.7;
71
+ transform: scale(0.75);
72
+ }
73
+
74
+ 100% {
75
+ opacity: 0.9;
76
+ transform: scale(1);
77
+ }
78
+ }
79
+ `;
80
+
81
+ style.appendChild(document.createTextNode(css));
82
+
83
+ document.head.appendChild(style);
84
+
85
+ const focusTimer = setTimeout(() => {
86
+ if (is.domElement(beaconRef.current) && shouldFocus) {
87
+ beaconRef.current.focus();
88
+ }
89
+ }, 0);
90
+
91
+ return () => {
92
+ clearTimeout(focusTimer);
93
+
94
+ const insertedStyle = document.getElementById('joyride-beacon-animation');
95
+
96
+ if (insertedStyle?.parentNode) {
97
+ insertedStyle.parentNode.removeChild(insertedStyle);
98
+ }
99
+ };
100
+ }, [hasBeaconComponent, nonce, shouldFocus]);
101
+
102
+ const title = getReactNodeText(locale.open);
103
+ let content;
104
+
105
+ if (beaconComponent) {
106
+ const BeaconComponent = beaconComponent;
107
+
108
+ content = (
109
+ <BeaconComponent
110
+ continuous={continuous}
111
+ index={index}
112
+ isLastStep={isLastStep}
113
+ size={size}
114
+ step={step}
115
+ />
116
+ );
117
+ } else {
118
+ content = (
119
+ <span style={styles.beacon}>
120
+ <span style={styles.beaconOuter} />
121
+ <span style={styles.beaconInner} />
122
+ </span>
123
+ );
124
+ }
125
+
126
+ return (
127
+ <button
128
+ ref={beaconRef}
129
+ aria-label={title}
130
+ className="react-joyride__beacon"
131
+ data-testid="button-beacon"
132
+ onClick={onInteract}
133
+ onMouseEnter={onInteract}
134
+ style={styles.beaconWrapper}
135
+ title={title}
136
+ type="button"
137
+ >
138
+ {content}
139
+ </button>
140
+ );
141
+ }
@@ -0,0 +1,381 @@
1
+ import {
2
+ type CSSProperties,
3
+ type MouseEvent,
4
+ type ReactNode,
5
+ type RefCallback,
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ } from 'react';
11
+ import {
12
+ arrow,
13
+ autoPlacement,
14
+ autoUpdate,
15
+ flip,
16
+ type Placement as FloatingPlacement,
17
+ type MiddlewareData,
18
+ offset,
19
+ shift,
20
+ useFloating,
21
+ } from '@floating-ui/react-dom';
22
+
23
+ import { LIFECYCLE } from '~/literals';
24
+ import { getScrollParent, hasCustomScrollParent, hasPosition } from '~/modules/dom';
25
+ import { sortObjectKeys } from '~/modules/helpers';
26
+ import type { StoreState } from '~/modules/store';
27
+
28
+ import type { Controls, Lifecycle, PositionData, StepMerged } from '~/types';
29
+
30
+ import Arrow from './Arrow';
31
+ import Beacon from './Beacon';
32
+ import Portal from './Portal';
33
+ import Tooltip from './Tooltip';
34
+
35
+ interface FloaterProps {
36
+ continuous: boolean;
37
+ controls: Controls;
38
+ index: number;
39
+ lifecycle: Lifecycle;
40
+ nonce?: string;
41
+ open: boolean;
42
+ portalElement: HTMLElement | null;
43
+ setPositionData: (name: 'beacon' | 'tooltip', data: PositionData) => void;
44
+ setTooltipRef: RefCallback<HTMLElement>;
45
+ shouldScroll: boolean;
46
+ size: number;
47
+ step: StepMerged;
48
+ target: Element;
49
+ updateState: (state: Partial<StoreState>) => void;
50
+ }
51
+
52
+ function getFallbackPlacements(placement: FloatingPlacement): FloatingPlacement[] | undefined {
53
+ if (placement.startsWith('left')) return ['top', 'bottom'];
54
+ if (placement.startsWith('right')) return ['bottom', 'top'];
55
+
56
+ return undefined;
57
+ }
58
+
59
+ function getFlipMiddleware(isAuto: boolean, step: StepMerged, tooltipPlacement: FloatingPlacement) {
60
+ if (isAuto) {
61
+ return [autoPlacement()];
62
+ }
63
+
64
+ if (step.floatingOptions?.flipOptions === false) {
65
+ return [];
66
+ }
67
+
68
+ return [
69
+ flip({
70
+ crossAxis: false,
71
+ fallbackPlacements: getFallbackPlacements(tooltipPlacement),
72
+ padding: 20,
73
+ ...step.floatingOptions?.flipOptions,
74
+ }),
75
+ ];
76
+ }
77
+
78
+ export default function JoyrideFloater(props: FloaterProps) {
79
+ const {
80
+ continuous,
81
+ controls,
82
+ index,
83
+ lifecycle,
84
+ nonce,
85
+ open,
86
+ portalElement,
87
+ setPositionData,
88
+ setTooltipRef,
89
+ shouldScroll,
90
+ size,
91
+ step,
92
+ target,
93
+ updateState,
94
+ } = props;
95
+ const arrowRef = useRef<HTMLElement>(null);
96
+ const beaconMiddlewareRef = useRef<MiddlewareData>({});
97
+ const tooltipMiddlewareRef = useRef<MiddlewareData>({});
98
+
99
+ const isCenter = step.placement === 'center';
100
+ const isAuto = step.placement === 'auto';
101
+
102
+ const centerReference = useMemo(
103
+ () => ({
104
+ getBoundingClientRect: () => ({
105
+ x: window.innerWidth / 2,
106
+ y: window.innerHeight / 2,
107
+ top: window.innerHeight / 2,
108
+ left: window.innerWidth / 2,
109
+ bottom: window.innerHeight / 2,
110
+ right: window.innerWidth / 2,
111
+ width: 0,
112
+ height: 0,
113
+ }),
114
+ }),
115
+ [],
116
+ );
117
+
118
+ const scrollParent = useMemo(
119
+ () =>
120
+ hasCustomScrollParent(target as HTMLElement)
121
+ ? getScrollParent(target as HTMLElement)
122
+ : undefined,
123
+ [target],
124
+ );
125
+
126
+ const isFixedTarget = useMemo(() => hasPosition(target), [target]);
127
+
128
+ const boundaryOptions = useMemo(
129
+ () =>
130
+ scrollParent ? { boundary: scrollParent as Element, rootBoundary: 'viewport' as const } : {},
131
+ [scrollParent],
132
+ );
133
+
134
+ const tooltipPlacement = isCenter || isAuto ? 'bottom' : (step.placement as FloatingPlacement);
135
+
136
+ const strategy = isCenter
137
+ ? 'fixed'
138
+ : (step.floatingOptions?.strategy ?? (step.isFixed || isFixedTarget ? 'fixed' : 'absolute'));
139
+
140
+ const tooltipMiddleware = useMemo(
141
+ () =>
142
+ isCenter
143
+ ? [
144
+ {
145
+ name: 'center',
146
+ fn: ({ rects }: { rects: { floating: { height: number; width: number } } }) => ({
147
+ x: (window.innerWidth - rects.floating.width) / 2,
148
+ y: (window.innerHeight - rects.floating.height) / 2,
149
+ }),
150
+ },
151
+ ]
152
+ : [
153
+ offset(
154
+ ({ placement: currentPlacement }) => {
155
+ let side: 'top' | 'bottom' | 'left' | 'right' = 'right';
156
+
157
+ if (currentPlacement.startsWith('top')) side = 'top';
158
+ else if (currentPlacement.startsWith('bottom')) side = 'bottom';
159
+ else if (currentPlacement.startsWith('left')) side = 'left';
160
+
161
+ const padding = step.spotlightTarget ? 0 : step.spotlightPadding[side];
162
+
163
+ return (
164
+ step.offset + padding + (step.floatingOptions?.hideArrow ? 0 : step.arrowSize)
165
+ );
166
+ },
167
+ [
168
+ step.offset,
169
+ step.spotlightPadding,
170
+ step.spotlightTarget,
171
+ step.arrowSize,
172
+ step.floatingOptions?.hideArrow,
173
+ ],
174
+ ),
175
+ ...getFlipMiddleware(isAuto, step, tooltipPlacement),
176
+ shift({
177
+ padding: 10,
178
+ ...boundaryOptions,
179
+ ...step.floatingOptions?.shiftOptions,
180
+ }),
181
+ ...(step.floatingOptions?.hideArrow
182
+ ? []
183
+ : [
184
+ arrow({ element: arrowRef, padding: step.arrowSpacing }, [
185
+ step.arrowSpacing,
186
+ step.arrowBase,
187
+ ]),
188
+ ]),
189
+ ...(step.floatingOptions?.middleware ?? []),
190
+ ],
191
+ [isCenter, step, isAuto, tooltipPlacement, boundaryOptions],
192
+ );
193
+
194
+ const tooltipFloating = useFloating({
195
+ ...(isCenter ? { elements: { reference: centerReference } } : {}),
196
+ placement: tooltipPlacement,
197
+ strategy,
198
+ middleware: tooltipMiddleware,
199
+ });
200
+
201
+ const beaconPlacement =
202
+ step.beaconPlacement ?? (isAuto || isCenter ? 'bottom' : (step.placement as FloatingPlacement));
203
+
204
+ const beaconMiddleware = useMemo(
205
+ () => [offset(step.floatingOptions?.beaconOptions?.offset ?? -18)],
206
+ [step.floatingOptions?.beaconOptions?.offset],
207
+ );
208
+
209
+ const beaconFloating = useFloating({
210
+ strategy,
211
+ placement: beaconPlacement,
212
+ middleware: beaconMiddleware,
213
+ whileElementsMounted: autoUpdate,
214
+ });
215
+
216
+ tooltipMiddlewareRef.current = tooltipFloating.middlewareData;
217
+ beaconMiddlewareRef.current = beaconFloating.middlewareData;
218
+
219
+ useEffect(() => {
220
+ const { floating, reference } = tooltipFloating.elements;
221
+
222
+ if (!reference || !floating || lifecycle !== LIFECYCLE.TOOLTIP) {
223
+ return undefined;
224
+ }
225
+
226
+ return autoUpdate(
227
+ reference,
228
+ floating,
229
+ tooltipFloating.update,
230
+ step.floatingOptions?.autoUpdate,
231
+ );
232
+ }, [
233
+ lifecycle,
234
+ tooltipFloating.update,
235
+ step.floatingOptions?.autoUpdate,
236
+ step.target,
237
+ tooltipFloating.elements,
238
+ ]);
239
+
240
+ // Wire reference element to both floating instances
241
+ useEffect(() => {
242
+ if (!isCenter && target) {
243
+ tooltipFloating.refs.setReference(target);
244
+ }
245
+
246
+ if (target) {
247
+ beaconFloating.refs.setReference(target);
248
+ }
249
+ }, [beaconFloating.refs, isCenter, target, tooltipFloating.refs]);
250
+
251
+ // Sync tooltip position data to store
252
+ useEffect(() => {
253
+ if (tooltipFloating.isPositioned) {
254
+ setPositionData('tooltip', {
255
+ placement: tooltipFloating.placement,
256
+ x: tooltipFloating.x ?? 0,
257
+ y: tooltipFloating.y ?? 0,
258
+ middlewareData: tooltipMiddlewareRef.current,
259
+ });
260
+ }
261
+ }, [
262
+ setPositionData,
263
+ tooltipFloating.isPositioned,
264
+ tooltipFloating.placement,
265
+ tooltipFloating.x,
266
+ tooltipFloating.y,
267
+ ]);
268
+
269
+ // Sync beacon position data to store
270
+ useEffect(() => {
271
+ if (beaconFloating.isPositioned) {
272
+ setPositionData('beacon', {
273
+ placement: beaconFloating.placement,
274
+ x: beaconFloating.x ?? 0,
275
+ y: beaconFloating.y ?? 0,
276
+ middlewareData: beaconMiddlewareRef.current,
277
+ });
278
+ }
279
+ }, [
280
+ setPositionData,
281
+ beaconFloating.isPositioned,
282
+ beaconFloating.placement,
283
+ beaconFloating.x,
284
+ beaconFloating.y,
285
+ ]);
286
+
287
+ const zIndex = step.zIndex + 1;
288
+
289
+ const handleBeaconInteraction = useCallback(
290
+ (event: MouseEvent<HTMLElement>) => {
291
+ if (event.type === 'mouseenter' && step.beaconTrigger !== 'hover') {
292
+ return;
293
+ }
294
+
295
+ updateState({ lifecycle: LIFECYCLE.TOOLTIP_BEFORE, positioned: false });
296
+ },
297
+ [step.beaconTrigger, updateState],
298
+ );
299
+
300
+ const floaterRef = useCallback(
301
+ (node: HTMLElement | null) => {
302
+ if (node) {
303
+ tooltipFloating.refs.setFloating(node);
304
+ setTooltipRef(node);
305
+ }
306
+ },
307
+ [tooltipFloating.refs, setTooltipRef],
308
+ );
309
+
310
+ const { arrow: arrowStyles, floater: floaterStyles } = step.styles;
311
+ let content: ReactNode = null;
312
+
313
+ if (lifecycle === LIFECYCLE.TOOLTIP || lifecycle === LIFECYCLE.TOOLTIP_BEFORE) {
314
+ const styles: CSSProperties = sortObjectKeys({
315
+ ...floaterStyles,
316
+ ...tooltipFloating.floatingStyles,
317
+ zIndex,
318
+ opacity: open && tooltipFloating.isPositioned ? 1 : 0,
319
+ ...(!open && { transition: 'none' }),
320
+ });
321
+
322
+ content = (
323
+ <div
324
+ ref={floaterRef}
325
+ className="react-joyride__floater"
326
+ data-testid="floater"
327
+ id={`react-joyride-step-${index}`}
328
+ style={styles}
329
+ >
330
+ <Tooltip
331
+ continuous={continuous}
332
+ controls={controls}
333
+ index={index}
334
+ isLastStep={index + 1 === size}
335
+ size={size}
336
+ step={step}
337
+ />
338
+ {!isCenter && !step.floatingOptions?.hideArrow && (
339
+ <Arrow
340
+ arrowComponent={step.arrowComponent}
341
+ arrowRef={arrowRef}
342
+ base={step.arrowBase}
343
+ placement={tooltipFloating.placement}
344
+ position={tooltipFloating.middlewareData.arrow}
345
+ size={step.arrowSize}
346
+ styles={arrowStyles}
347
+ />
348
+ )}
349
+ </div>
350
+ );
351
+ } else if (lifecycle === LIFECYCLE.BEACON || lifecycle === LIFECYCLE.BEACON_BEFORE) {
352
+ content = (
353
+ <div
354
+ ref={beaconFloating.refs.setFloating}
355
+ className="react-joyride__floater"
356
+ data-testid="floater-beacon"
357
+ id={`react-joyride-step-${index}-beacon`}
358
+ style={sortObjectKeys({
359
+ ...beaconFloating.floatingStyles,
360
+ zIndex,
361
+ })}
362
+ >
363
+ <Beacon
364
+ beaconComponent={step.beaconComponent}
365
+ continuous={continuous}
366
+ index={index}
367
+ isLastStep={index + 1 === size}
368
+ locale={step.locale}
369
+ nonce={nonce}
370
+ onInteract={handleBeaconInteraction}
371
+ shouldFocus={shouldScroll}
372
+ size={size}
373
+ step={step}
374
+ styles={step.styles}
375
+ />
376
+ </div>
377
+ );
378
+ }
379
+
380
+ return <Portal element={portalElement}>{content}</Portal>;
381
+ }