Skip to content

Commit 7c13575

Browse files
authored
feat: Support mobile config (#528)
* chore: init docs * chore: mobile support * test: add test case * chore: fix lint * docs: update demo * chore: fix lint
1 parent 4dd80c8 commit 7c13575

File tree

14 files changed

+349
-152
lines changed

14 files changed

+349
-152
lines changed

.eslintrc.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,10 @@ module.exports = {
1010
'react/sort-comp': 0,
1111
'jsx-a11y/label-has-for': 0,
1212
'jsx-a11y/label-has-associated-control': 0,
13+
'@typescript-eslint/consistent-indexed-object-style': 0,
14+
'@typescript-eslint/no-parameter-properties': 0,
15+
'@typescript-eslint/ban-types': 0,
16+
'@typescript-eslint/type-annotation-spacing': 0,
17+
'@typescript-eslint/no-throw-literal': 0,
1318
},
1419
};

assets/index/Mobile.less

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,55 @@
11
.@{triggerPrefixCls} {
22
&-mobile {
3-
transition: all 0.3s;
43
position: fixed;
54
left: 0;
65
right: 0;
76
bottom: 0;
87
top: auto;
98

10-
&-fade {
11-
&-appear,
12-
&-enter {
13-
&-start {
14-
transform: translateY(100%);
9+
// Motion
10+
&.raise {
11+
&-enter,
12+
&-appear {
13+
transform: translateY(100%);
14+
15+
&-active {
16+
transition: all 0.3s;
17+
transform: translateY(0);
1518
}
1619
}
1720

1821
&-leave {
22+
transform: translateY(0);
23+
1924
&-active {
25+
transition: all 0.3s;
2026
transform: translateY(100%);
2127
}
2228
}
2329
}
30+
31+
// Mask
32+
&-mask {
33+
&.fade {
34+
&-enter,
35+
&-appear {
36+
opacity: 0;
37+
38+
&-active {
39+
transition: all 0.3s;
40+
opacity: 1;
41+
}
42+
}
43+
44+
&-leave {
45+
opacity: 1;
46+
47+
&-active {
48+
transition: all 0.3s;
49+
opacity: 0;
50+
}
51+
}
52+
}
53+
}
2454
}
2555
}

docs/demos/mobile.md

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

docs/examples/mobile.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import Trigger from '@rc-component/trigger';
2+
import React from 'react';
3+
import '../../assets/index.less';
4+
5+
const builtinPlacements = {
6+
left: {
7+
points: ['cr', 'cl'],
8+
},
9+
right: {
10+
points: ['cl', 'cr'],
11+
},
12+
top: {
13+
points: ['bc', 'tc'],
14+
},
15+
bottom: {
16+
points: ['tc', 'bc'],
17+
},
18+
topLeft: {
19+
points: ['bl', 'tl'],
20+
},
21+
topRight: {
22+
points: ['br', 'tr'],
23+
},
24+
bottomRight: {
25+
points: ['tr', 'br'],
26+
},
27+
bottomLeft: {
28+
points: ['tl', 'bl'],
29+
},
30+
};
31+
32+
const Test = () => {
33+
const [open1, setOpen1] = React.useState(false);
34+
35+
return (
36+
<div style={{ margin: 200 }}>
37+
<div>
38+
<Trigger
39+
popupPlacement="top"
40+
action={['hover']}
41+
builtinPlacements={builtinPlacements}
42+
popupVisible={open1}
43+
onOpenChange={setOpen1}
44+
popup={
45+
<div
46+
style={{
47+
background: '#FFF',
48+
boxShadow: '0 0 3px red',
49+
padding: 12,
50+
}}
51+
>
52+
<h2>Hello World</h2>
53+
</div>
54+
}
55+
mobile={{
56+
mask: true,
57+
motion: { motionName: 'raise' },
58+
maskMotion: { motionName: 'fade' },
59+
}}
60+
>
61+
<span>Click Me</span>
62+
</Trigger>
63+
</div>
64+
</div>
65+
);
66+
};
67+
68+
export default Test;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
"react": "^19.1.0",
6969
"react-dom": "^19.1.0",
7070
"regenerator-runtime": "^0.14.0",
71-
"typescript": "^5.1.6"
71+
"typescript": "~5.1.6"
7272
},
7373
"peerDependencies": {
7474
"react": ">=18.0.0",

src/Popup/Mask.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export interface MaskProps {
1111

1212
// Motion
1313
motion?: CSSMotionProps;
14+
15+
mobile?: boolean;
1416
}
1517

1618
export default function Mask(props: MaskProps) {
@@ -21,6 +23,8 @@ export default function Mask(props: MaskProps) {
2123

2224
mask,
2325
motion,
26+
27+
mobile,
2428
} = props;
2529

2630
if (!mask) {
@@ -32,7 +36,11 @@ export default function Mask(props: MaskProps) {
3236
{({ className }) => (
3337
<div
3438
style={{ zIndex }}
35-
className={classNames(`${prefixCls}-mask`, className)}
39+
className={classNames(
40+
`${prefixCls}-mask`,
41+
mobile && `${prefixCls}-mobile-mask`,
42+
className,
43+
)}
3644
/>
3745
)}
3846
</CSSMotion>

src/Popup/index.tsx

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ import Arrow from './Arrow';
1111
import Mask from './Mask';
1212
import PopupContent from './PopupContent';
1313

14+
export interface MobileConfig {
15+
mask?: boolean;
16+
/** Set popup motion. You can ref `rc-motion` for more info. */
17+
motion?: CSSMotionProps;
18+
/** Set mask motion. You can ref `rc-motion` for more info. */
19+
maskMotion?: CSSMotionProps;
20+
}
21+
1422
export interface PopupProps {
1523
prefixCls: string;
1624
className?: string;
@@ -63,6 +71,9 @@ export interface PopupProps {
6371
stretch?: string;
6472
targetWidth?: number;
6573
targetHeight?: number;
74+
75+
// Mobile
76+
mobile?: MobileConfig;
6677
}
6778

6879
const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => {
@@ -95,6 +106,9 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => {
95106
motion,
96107
maskMotion,
97108

109+
// Mobile
110+
mobile,
111+
98112
// Portal
99113
forceRender,
100114
getPopupContainer,
@@ -126,6 +140,24 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => {
126140
// We can not remove holder only when motion finished.
127141
const isNodeVisible = open || keepDom;
128142

143+
// ========================= Mobile =========================
144+
const isMobile = !!mobile;
145+
146+
// ========================== Mask ==========================
147+
const [mergedMask, mergedMaskMotion, mergedPopupMotion] = React.useMemo<
148+
[
149+
mask: boolean,
150+
maskMotion: CSSMotionProps | undefined,
151+
popupMotion: CSSMotionProps | undefined,
152+
]
153+
>(() => {
154+
if (mobile) {
155+
return [mobile.mask, mobile.maskMotion, mobile.motion];
156+
}
157+
158+
return [mask, maskMotion, motion];
159+
}, [mobile]);
160+
129161
// ======================= Container ========================
130162
const getPopupContainerNeedParams = getPopupContainer?.length > 0;
131163

@@ -148,15 +180,17 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => {
148180
// >>>>> Offset
149181
const AUTO = 'auto' as const;
150182

151-
const offsetStyle: React.CSSProperties = {
152-
left: '-1000vw',
153-
top: '-1000vh',
154-
right: AUTO,
155-
bottom: AUTO,
156-
};
183+
const offsetStyle: React.CSSProperties = isMobile
184+
? {}
185+
: {
186+
left: '-1000vw',
187+
top: '-1000vh',
188+
right: AUTO,
189+
bottom: AUTO,
190+
};
157191

158192
// Set align style
159-
if (ready || !open) {
193+
if (!isMobile && (ready || !open)) {
160194
const { points } = align;
161195
const dynamicInset =
162196
align.dynamicInset || (align as any)._experimental?.dynamicInset;
@@ -209,8 +243,9 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => {
209243
prefixCls={prefixCls}
210244
open={open}
211245
zIndex={zIndex}
212-
mask={mask}
213-
motion={maskMotion}
246+
mask={mergedMask}
247+
motion={mergedMaskMotion}
248+
mobile={isMobile}
214249
/>
215250
<ResizeObserver onResize={onAlign} disabled={!open}>
216251
{(resizeObserverRef) => {
@@ -222,7 +257,7 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => {
222257
removeOnLeave={false}
223258
forceRender={forceRender}
224259
leavedClassName={`${prefixCls}-hidden`}
225-
{...motion}
260+
{...mergedPopupMotion}
226261
onAppearPrepare={onPrepare}
227262
onEnterPrepare={onPrepare}
228263
visible={open}
@@ -235,7 +270,9 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => {
235270
{ className: motionClassName, style: motionStyle },
236271
motionRef,
237272
) => {
238-
const cls = classNames(prefixCls, motionClassName, className);
273+
const cls = classNames(prefixCls, motionClassName, className, {
274+
[`${prefixCls}-mobile`]: isMobile,
275+
});
239276

240277
return (
241278
<div

src/hooks/useAction.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,34 @@
11
import * as React from 'react';
22
import type { ActionType } from '../interface';
33

4-
type ActionTypes = ActionType | ActionType[];
4+
type InternalActionType = ActionType | 'touch';
5+
6+
type ActionTypes = InternalActionType | InternalActionType[];
57

68
function toArray<T>(val?: T | T[]) {
79
return val ? (Array.isArray(val) ? val : [val]) : [];
810
}
911

1012
export default function useAction(
11-
mobile: boolean,
1213
action: ActionTypes,
1314
showAction?: ActionTypes,
1415
hideAction?: ActionTypes,
15-
): [showAction: Set<ActionType>, hideAction: Set<ActionType>] {
16+
): [showAction: Set<InternalActionType>, hideAction: Set<InternalActionType>] {
1617
return React.useMemo(() => {
1718
const mergedShowAction = toArray(showAction ?? action);
1819
const mergedHideAction = toArray(hideAction ?? action);
1920

2021
const showActionSet = new Set(mergedShowAction);
2122
const hideActionSet = new Set(mergedHideAction);
2223

23-
if (mobile) {
24-
if (showActionSet.has('hover')) {
25-
showActionSet.delete('hover');
26-
showActionSet.add('click');
27-
}
24+
if (showActionSet.has('hover') && !showActionSet.has('click')) {
25+
showActionSet.add('touch');
26+
}
2827

29-
if (hideActionSet.has('hover')) {
30-
hideActionSet.delete('hover');
31-
hideActionSet.add('click');
32-
}
28+
if (hideActionSet.has('hover') && !hideActionSet.has('click')) {
29+
hideActionSet.add('touch');
3330
}
3431

3532
return [showActionSet, hideActionSet];
36-
}, [mobile, action, showAction, hideAction]);
33+
}, [action, showAction, hideAction]);
3734
}

0 commit comments

Comments
 (0)