Skip to content

Commit 929d25b

Browse files
authored
feat: support inline mode (#74)
* chore: init demo * chore: tmp of it * feat: rect calc * test: update snapshot * chore: clean up * chore: take advice
1 parent e840ed3 commit 929d25b

File tree

8 files changed

+205
-57
lines changed

8 files changed

+205
-57
lines changed

docs/demo/inline.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: inline
3+
nav:
4+
title: Demo
5+
path: /demo
6+
---
7+
8+
<code src="../examples/inline.tsx"></code>

docs/examples/inline.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React, { useRef } from 'react';
2+
import Tour from '../../src/index';
3+
import './basic.less';
4+
5+
const App = () => {
6+
const [open, setOpen] = React.useState(true);
7+
const createBtnRef = useRef<HTMLButtonElement>(null);
8+
9+
return (
10+
<div style={{ margin: 20 }}>
11+
<div
12+
style={{
13+
width: 800,
14+
height: 500,
15+
boxShadow: '0 0 0 1px red',
16+
display: 'flex',
17+
alignItems: 'flex-start',
18+
justifyContent: 'center',
19+
// justifyContent: 'right',
20+
position: 'relative',
21+
overflow: 'hidden',
22+
}}
23+
id="inlineHolder"
24+
>
25+
<button
26+
className="ant-target"
27+
ref={createBtnRef}
28+
onClick={() => {
29+
setOpen(true);
30+
}}
31+
>
32+
Show
33+
</button>
34+
35+
<Tour
36+
open={open}
37+
defaultCurrent={0}
38+
getPopupContainer={false}
39+
onClose={() => {
40+
setOpen(false);
41+
}}
42+
// style={{ background: 'red' }}
43+
steps={[
44+
{
45+
title: '创建',
46+
description: '创建一条数据',
47+
target: () => createBtnRef.current,
48+
mask: true,
49+
},
50+
]}
51+
/>
52+
</div>
53+
54+
<div style={{ height: '200vh' }} />
55+
</div>
56+
);
57+
};
58+
59+
export default App;

src/Mask.tsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import classNames from 'classnames';
33
import Portal from '@rc-component/portal';
44
import type { PosInfo } from './hooks/useTarget';
55
import useId from 'rc-util/lib/hooks/useId';
6-
import { SemanticName } from './interface';
6+
import type { SemanticName, TourProps } from './interface';
77

88
const COVER_PROPS = {
99
fill: 'transparent',
@@ -24,6 +24,7 @@ export interface MaskProps {
2424
disabledInteraction?: boolean;
2525
classNames?: Partial<Record<SemanticName, string>>;
2626
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
27+
getPopupContainer?: TourProps['getPopupContainer'];
2728
}
2829

2930
const Mask = (props: MaskProps) => {
@@ -33,29 +34,44 @@ const Mask = (props: MaskProps) => {
3334
pos,
3435
showMask,
3536
style = {},
36-
fill = "rgba(0,0,0,0.5)",
37+
fill = 'rgba(0,0,0,0.5)',
3738
open,
3839
animated,
3940
zIndex,
4041
disabledInteraction,
4142
styles,
4243
classNames: tourClassNames,
44+
getPopupContainer,
4345
} = props;
4446

4547
const id = useId();
4648
const maskId = `${prefixCls}-mask-${id}`;
4749
const mergedAnimated =
4850
typeof animated === 'object' ? animated?.placeholder : animated;
4951

50-
const isSafari = typeof navigator !== 'undefined' && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
51-
const maskRectSize = isSafari ? { width: '100%', height: '100%' } : { width: '100vw', height: '100vh'};
52+
const isSafari =
53+
typeof navigator !== 'undefined' &&
54+
/^((?!chrome|android).)*safari/i.test(navigator.userAgent);
55+
const maskRectSize = isSafari
56+
? { width: '100%', height: '100%' }
57+
: { width: '100vw', height: '100vh' };
58+
59+
const inlineMode = getPopupContainer === false;
5260

5361
return (
54-
<Portal open={open} autoLock>
62+
<Portal
63+
open={open}
64+
autoLock={!inlineMode}
65+
getContainer={getPopupContainer as any}
66+
>
5567
<div
56-
className={classNames(`${prefixCls}-mask`, rootClassName, tourClassNames?.mask)}
68+
className={classNames(
69+
`${prefixCls}-mask`,
70+
rootClassName,
71+
tourClassNames?.mask,
72+
)}
5773
style={{
58-
position: 'fixed',
74+
position: inlineMode ? 'absolute' : 'fixed',
5975
left: 0,
6076
right: 0,
6177
top: 0,
@@ -103,32 +119,37 @@ const Mask = (props: MaskProps) => {
103119
{/* Block click region */}
104120
{pos && (
105121
<>
122+
{/* Top */}
123+
106124
<rect
107125
{...COVER_PROPS}
108126
x="0"
109127
y="0"
110128
width="100%"
111-
height={pos.top}
129+
height={Math.max(pos.top, 0)}
112130
/>
131+
{/* Left */}
113132
<rect
114133
{...COVER_PROPS}
115134
x="0"
116135
y="0"
117-
width={pos.left}
136+
width={Math.max(pos.left, 0)}
118137
height="100%"
119138
/>
139+
{/* Bottom */}
120140
<rect
121141
{...COVER_PROPS}
122142
x="0"
123143
y={pos.top + pos.height}
124144
width="100%"
125-
height={`calc(100vh - ${pos.top + pos.height}px)`}
145+
height={`calc(100% - ${pos.top + pos.height}px)`}
126146
/>
147+
{/* Right */}
127148
<rect
128149
{...COVER_PROPS}
129150
x={pos.left + pos.width}
130151
y="0"
131-
width={`calc(100vw - ${pos.left + pos.width}px)`}
152+
width={`calc(100% - ${pos.left + pos.width}px)`}
132153
height="100%"
133154
/>
134155
</>

src/Tour.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const Tour: React.FC<TourProps> = props => {
3838
onClose,
3939
onFinish,
4040
open,
41+
defaultOpen,
4142
mask = true,
4243
arrow = true,
4344
rootClassName,
@@ -55,6 +56,7 @@ const Tour: React.FC<TourProps> = props => {
5556
classNames: tourClassNames,
5657
className,
5758
style,
59+
getPopupContainer,
5860
...restProps
5961
} = props;
6062

@@ -65,7 +67,7 @@ const Tour: React.FC<TourProps> = props => {
6567
defaultValue: defaultCurrent,
6668
});
6769

68-
const [mergedOpen, setMergedOpen] = useMergedState(undefined, {
70+
const [mergedOpen, setMergedOpen] = useMergedState(defaultOpen, {
6971
value: open,
7072
postState: origin =>
7173
mergedCurrent < 0 || mergedCurrent >= steps.length
@@ -112,11 +114,19 @@ const Tour: React.FC<TourProps> = props => {
112114
const mergedMask = mergedOpen && (stepMask ?? mask);
113115
const mergedScrollIntoViewOptions =
114116
stepScrollIntoViewOptions ?? scrollIntoViewOptions;
117+
118+
// ====================== Align Target ======================
119+
const placeholderRef = React.useRef<HTMLDivElement>(null);
120+
121+
const inlineMode = getPopupContainer === false;
122+
115123
const [posInfo, targetElement] = useTarget(
116124
target,
117125
open,
118126
gap,
119127
mergedScrollIntoViewOptions,
128+
inlineMode,
129+
placeholderRef,
120130
);
121131
const mergedPlacement = getPlacement(targetElement, placement, stepPlacement);
122132

@@ -198,6 +208,7 @@ const Tour: React.FC<TourProps> = props => {
198208
return (
199209
<>
200210
<Mask
211+
getPopupContainer={getPopupContainer}
201212
styles={styles}
202213
classNames={tourClassNames}
203214
zIndex={zIndex}
@@ -213,6 +224,8 @@ const Tour: React.FC<TourProps> = props => {
213224
/>
214225
<Trigger
215226
{...restProps}
227+
// `rc-portal` def bug not support `false` but does support and in used.
228+
getPopupContainer={getPopupContainer as any}
216229
builtinPlacements={mergedBuiltinPlacements}
217230
ref={triggerRef}
218231
popupStyle={stepStyle}
@@ -227,16 +240,21 @@ const Tour: React.FC<TourProps> = props => {
227240
getTriggerDOMNode={getTriggerDOMNode}
228241
arrow={!!mergedArrow}
229242
>
230-
<Portal open={mergedOpen} autoLock>
243+
<Portal
244+
open={mergedOpen}
245+
autoLock={!inlineMode}
246+
getContainer={getPopupContainer as any}
247+
>
231248
<div
249+
ref={placeholderRef}
232250
className={classNames(
233251
className,
234252
rootClassName,
235253
`${prefixCls}-target-placeholder`,
236254
)}
237255
style={{
238256
...(posInfo || CENTER_PLACEHOLDER),
239-
position: 'fixed',
257+
position: inlineMode ? 'absolute' : 'fixed',
240258
pointerEvents: 'none',
241259
...style,
242260
}}

src/hooks/useTarget.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export default function useTarget(
2525
open: boolean,
2626
gap?: Gap,
2727
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions,
28+
inlineMode?: boolean,
29+
placeholderRef?: React.RefObject<HTMLDivElement>,
2830
): [PosInfo, HTMLElement] {
2931
// ========================= Target =========================
3032
// We trade `undefined` as not get target by function yet.
@@ -53,6 +55,17 @@ export default function useTarget(
5355
targetElement.getBoundingClientRect();
5456
const nextPosInfo: PosInfo = { left, top, width, height, radius: 0 };
5557

58+
// If `inlineMode` we need cut off parent offset
59+
if (inlineMode) {
60+
const parentRect =
61+
placeholderRef.current?.parentElement?.getBoundingClientRect();
62+
63+
if (parentRect) {
64+
nextPosInfo.left -= parentRect.left;
65+
nextPosInfo.top -= parentRect.top;
66+
}
67+
}
68+
5669
setPosInfo(origin => {
5770
if (JSON.stringify(origin) !== JSON.stringify(nextPosInfo)) {
5871
return nextPosInfo;

src/interface.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export interface TourProps extends Pick<TriggerProps, 'onPopupAlign'> {
5353
style?: React.CSSProperties;
5454
steps?: TourStepInfo[];
5555
open?: boolean;
56+
defaultOpen?: boolean;
5657
defaultCurrent?: number;
5758
current?: number;
5859
onChange?: (current: number) => void;
@@ -76,7 +77,7 @@ export interface TourProps extends Pick<TriggerProps, 'onPopupAlign'> {
7677
animated?: boolean | { placeholder: boolean };
7778
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions;
7879
zIndex?: number;
79-
getPopupContainer?: TriggerProps['getPopupContainer'];
80+
getPopupContainer?: TriggerProps['getPopupContainer'] | false;
8081
builtinPlacements?:
8182
| TriggerProps['builtinPlacements']
8283
| ((config?: {

0 commit comments

Comments
 (0)