Skip to content

Commit cb4e2b3

Browse files
authored
feat: Support mobile config (#216)
* chore: feature merge master * feat: Support mobile interactive * tests: Add test case * tests: Add coverage * docs: Update example * refactor: More comments * chore: Warning info
1 parent f8db52e commit cb4e2b3

File tree

11 files changed

+360
-19
lines changed

11 files changed

+360
-19
lines changed

assets/index.less

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,5 @@
6464
}
6565
}
6666

67-
@import "./index/Mask";
67+
@import "./index/Mask";
68+
@import "./index/Mobile";

assets/index/Mobile.less

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
.@{triggerPrefixCls} {
2+
&-mobile {
3+
transition: all 0.3s;
4+
position: fixed;
5+
left: 0;
6+
right: 0;
7+
bottom: 0;
8+
top: auto;
9+
10+
&-fade {
11+
&-appear,
12+
&-enter {
13+
&-start {
14+
transform: translateY(100%);
15+
}
16+
}
17+
18+
&-leave {
19+
&-active {
20+
transform: translateY(100%);
21+
}
22+
}
23+
}
24+
}
25+
}

examples/simple.js renamed to examples/simple.tsx

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,33 @@ const RefTarget = React.forwardRef((props, ref) => {
5858
return <InnerTarget {...props} />;
5959
});
6060

61-
class Test extends React.Component {
62-
state = {
61+
interface TestState {
62+
mask: boolean;
63+
maskClosable: boolean;
64+
placement: 'right';
65+
trigger: {
66+
click?: boolean;
67+
focus?: boolean;
68+
hover?: boolean;
69+
contextMenu?: boolean;
70+
};
71+
offsetX: number;
72+
offsetY: number;
73+
stretch: string;
74+
transitionName: string;
75+
destroyed?: boolean;
76+
destroyPopupOnHide?: boolean;
77+
autoDestroy?: boolean;
78+
mobile?: boolean;
79+
}
80+
81+
class Test extends React.Component<{}, TestState> {
82+
state: TestState = {
6383
mask: true,
6484
maskClosable: true,
6585
placement: 'right',
6686
trigger: {
67-
click: 1,
87+
click: true,
6888
},
6989
offsetX: undefined,
7090
offsetY: undefined,
@@ -281,6 +301,19 @@ class Test extends React.Component {
281301
/>
282302
maskClosable
283303
</label>
304+
&nbsp;&nbsp;&nbsp;&nbsp;
305+
<label>
306+
<input
307+
checked={!!this.state.mobile}
308+
type="checkbox"
309+
onChange={() => {
310+
this.setState(({ mobile }) => ({
311+
mobile: !mobile,
312+
}));
313+
}}
314+
/>
315+
mobile
316+
</label>
284317
<br />
285318
<label>
286319
offsetX:
@@ -328,6 +361,33 @@ class Test extends React.Component {
328361
}}
329362
popup={<div>i am a popup</div>}
330363
popupTransitionName={state.transitionName}
364+
mobile={
365+
state.mobile
366+
? {
367+
popupMotion: {
368+
motionName: 'rc-trigger-popup-mobile-fade',
369+
},
370+
popupClassName: 'rc-trigger-popup-mobile',
371+
popupStyle: {
372+
padding: 16,
373+
borderTop: '1px solid red',
374+
background: '#FFF',
375+
textAlign: 'center',
376+
},
377+
popupRender: node => (
378+
<>
379+
<div>
380+
<input
381+
style={{ width: '100%' }}
382+
placeholder="additional content"
383+
/>
384+
</div>
385+
{node}
386+
</>
387+
),
388+
}
389+
: null
390+
}
331391
>
332392
<RefTarget />
333393
</Trigger>

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
},
4242
"devDependencies": {
4343
"@types/classnames": "^2.2.10",
44-
"@types/jest": "^26.0.13",
44+
"@types/enzyme": "^3.10.8",
45+
"@types/jest": "^26.0.15",
4546
"@types/react": "^16.8.19",
4647
"@types/react-dom": "^16.8.4",
4748
"cross-env": "^7.0.1",
@@ -61,6 +62,6 @@
6162
"classnames": "^2.2.6",
6263
"rc-align": "^4.0.0",
6364
"rc-motion": "^2.0.0",
64-
"rc-util": "^5.3.4"
65+
"rc-util": "^5.5.0"
6566
}
6667
}

src/Popup/MobilePopupInner.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import * as React from 'react';
2+
import CSSMotion from 'rc-motion';
3+
import classNames from 'classnames';
4+
import { PopupInnerProps, PopupInnerRef } from './PopupInner';
5+
import { MobileConfig } from '../interface';
6+
7+
interface MobilePopupInnerProps extends PopupInnerProps {
8+
mobile?: MobileConfig;
9+
}
10+
11+
const MobilePopupInner = React.forwardRef<PopupInnerRef, MobilePopupInnerProps>(
12+
(props, ref) => {
13+
const {
14+
prefixCls,
15+
visible,
16+
zIndex,
17+
children,
18+
mobile: {
19+
popupClassName,
20+
popupStyle,
21+
popupMotion = {},
22+
popupRender,
23+
} = {},
24+
} = props;
25+
const elementRef = React.useRef<HTMLDivElement>();
26+
27+
// ========================= Refs =========================
28+
React.useImperativeHandle(ref, () => ({
29+
forceAlign: () => {},
30+
getElement: () => elementRef.current,
31+
}));
32+
33+
// ======================== Render ========================
34+
const mergedStyle: React.CSSProperties = {
35+
zIndex,
36+
37+
...popupStyle,
38+
};
39+
40+
let childNode = children;
41+
42+
// Wrapper when multiple children
43+
if (React.Children.count(children) > 1) {
44+
childNode = <div className={`${prefixCls}-content`}>{children}</div>;
45+
}
46+
47+
// Mobile support additional render
48+
if (popupRender) {
49+
childNode = popupRender(childNode);
50+
}
51+
52+
return (
53+
<CSSMotion
54+
visible={visible}
55+
ref={elementRef}
56+
removeOnLeave
57+
{...popupMotion}
58+
>
59+
{({ className: motionClassName, style: motionStyle }, motionRef) => {
60+
const mergedClassName = classNames(
61+
prefixCls,
62+
popupClassName,
63+
motionClassName,
64+
);
65+
66+
return (
67+
<div
68+
ref={motionRef}
69+
className={mergedClassName}
70+
style={{
71+
...motionStyle,
72+
...mergedStyle,
73+
}}
74+
>
75+
{childNode}
76+
</div>
77+
);
78+
}}
79+
</CSSMotion>
80+
);
81+
},
82+
);
83+
84+
MobilePopupInner.displayName = 'MobilePopupInner';
85+
86+
export default MobilePopupInner;

src/Popup/PopupInner.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ const PopupInner = React.forwardRef<PopupInnerRef, PopupInnerProps>(
134134

135135
// ======================== Motion ========================
136136
const motion = { ...getMotion(props) };
137-
['onAppearEnd', 'onEnterEnd', 'onLeaveEnd'].forEach(eventName => {
137+
['onAppearEnd', 'onEnterEnd', 'onLeaveEnd'].forEach((eventName) => {
138138
const originHandler: MotionEndEventHandler = motion[eventName];
139139
motion[eventName] = (element, event) => {
140140
goNextStatus();
@@ -143,7 +143,7 @@ const PopupInner = React.forwardRef<PopupInnerRef, PopupInnerProps>(
143143
});
144144

145145
function onShowPrepare() {
146-
return new Promise(resolve => {
146+
return new Promise((resolve) => {
147147
prepareResolveRef.current = resolve;
148148
});
149149
}
@@ -166,7 +166,8 @@ const PopupInner = React.forwardRef<PopupInnerRef, PopupInnerProps>(
166166
...stretchStyle,
167167
zIndex,
168168
...style,
169-
opacity: status === 'motion' || status === 'stable' || !visible ? undefined : 0,
169+
opacity:
170+
status === 'motion' || status === 'stable' || !visible ? undefined : 0,
170171
pointerEvents: status === 'stable' ? undefined : 'none',
171172
};
172173

src/Popup/index.tsx

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import * as React from 'react';
2+
import { useState, useEffect } from 'react';
23
import { CSSMotionProps } from 'rc-motion';
4+
import isMobile from 'rc-util/lib/isMobile';
35
import {
46
StretchType,
57
AlignType,
68
TransitionNameType,
79
AnimationType,
810
Point,
11+
MobileConfig,
912
} from '../interface';
1013
import Mask from './Mask';
1114
import PopupInner, { PopupInnerRef } from './PopupInner';
15+
import MobilePopupInner from './MobilePopupInner';
1216

1317
export interface PopupProps {
1418
visible?: boolean;
@@ -39,19 +43,41 @@ export interface PopupProps {
3943
transitionName: TransitionNameType;
4044
maskAnimation: AnimationType;
4145
maskTransitionName: TransitionNameType;
46+
47+
// Mobile
48+
mobile?: MobileConfig;
4249
}
4350

44-
const Popup = React.forwardRef<PopupInnerRef, PopupProps>((props, ref) => {
45-
const { ...cloneProps } = props;
51+
const Popup = React.forwardRef<PopupInnerRef, PopupProps>(
52+
({ visible, mobile, ...props }, ref) => {
53+
const [innerVisible, serInnerVisible] = useState(visible);
54+
const [inMobile, setInMobile] = useState(false);
55+
const cloneProps = { ...props, visible: innerVisible };
56+
57+
// We check mobile in visible changed here.
58+
// And this also delay set `innerVisible` to avoid popup component render flash
59+
useEffect(() => {
60+
serInnerVisible(visible);
61+
if (visible && mobile) {
62+
setInMobile(isMobile());
63+
}
64+
}, [visible, !!mobile]);
4665

47-
// We can use fragment directly but this may failed some selector usage. Keep as origin logic
48-
return (
49-
<div>
50-
<Mask {...cloneProps} />
66+
const popupNode: React.ReactNode = inMobile ? (
67+
<MobilePopupInner {...cloneProps} mobile={mobile} ref={ref} />
68+
) : (
5169
<PopupInner {...cloneProps} ref={ref} />
52-
</div>
53-
);
54-
});
70+
);
71+
72+
// We can use fragment directly but this may failed some selector usage. Keep as origin logic
73+
return (
74+
<div>
75+
<Mask {...cloneProps} />
76+
{popupNode}
77+
</div>
78+
);
79+
},
80+
);
5581

5682
Popup.displayName = 'Popup';
5783

0 commit comments

Comments
 (0)