Skip to content

Commit fe575de

Browse files
novlan1anlyyao
andauthored
feat(fab): 对齐 mobile-vue (#594)
* feat(fab): 对齐mobile-vue" * feat(fab): prevent default when touchmove * feat(fab): prevent default * chore: update demo file name * chore: update test case * chore: remove forwardRef --------- Co-authored-by: anlyyao <anly_yaw@163.com>
1 parent f23758c commit fe575de

File tree

14 files changed

+441
-77
lines changed

14 files changed

+441
-77
lines changed

site/mobile/mobile.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ export default {
245245
{
246246
title: 'Fab 悬浮按钮',
247247
name: 'fab',
248-
component: () => import('tdesign-mobile-react/fab/_example/index.jsx'),
248+
component: () => import('tdesign-mobile-react/fab/_example/index.tsx'),
249249
},
250250
{
251251
title: 'NoticeBar 公告栏',

src/fab/Fab.tsx

Lines changed: 177 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,196 @@
1-
import React, { useMemo, forwardRef, Ref } from 'react';
2-
import classNames from 'classnames';
3-
import Button, { ButtonProps } from '../button';
1+
import React, { useRef, useState, useEffect } from 'react';
2+
import { reconvertUnit } from '../_util/convertUnit';
3+
import Button from '../button';
44
import { TdFabProps } from './type';
5+
import { fabDefaultProps } from './defaultProps';
56
import { StyledProps } from '../common';
6-
import useConfig from '../hooks/useConfig';
7+
import { usePrefixClass } from '../hooks/useClass';
8+
import useDefaultProps from '../hooks/useDefaultProps';
79

810
export interface FabProps extends TdFabProps, StyledProps {}
911

10-
const Fab: React.FC<FabProps> = forwardRef((props, ref: Ref<HTMLButtonElement>) => {
11-
const { buttonProps, style, icon = null, text, onClick } = props;
12-
const baseButtonProps: ButtonProps = {
13-
shape: 'round',
14-
theme: 'primary',
15-
};
12+
const Fab: React.FC<FabProps> = (originProps) => {
13+
const props = useDefaultProps(originProps, fabDefaultProps);
14+
const { buttonProps, icon = null, text, onClick } = props;
1615

17-
const { classPrefix } = useConfig();
18-
const name = useMemo(() => `${classPrefix}-fab`, [classPrefix]);
16+
const fabButtonRef = useRef(null);
17+
const fabRef = useRef<HTMLDivElement>(null);
1918

20-
// 外层样式类
21-
const FabClasses = classNames({
22-
[`${name}`]: true,
23-
[`${name}--icon-only`]: icon && !text,
19+
const [fabButtonSize, setFabButtonSize] = useState({
20+
width: 0,
21+
height: 0,
2422
});
2523

26-
const onClickHandle = (e) => {
24+
useEffect(() => {
25+
if (fabButtonRef.current) {
26+
const info = window.getComputedStyle(fabButtonRef.current);
27+
28+
setFabButtonSize({
29+
width: +reconvertUnit(info.width),
30+
height: +reconvertUnit(info.height),
31+
});
32+
}
33+
}, []);
34+
35+
const [btnSwitchPos, setBtnSwitchPos] = useState({
36+
x: 16,
37+
y: 32,
38+
});
39+
let switchPos = {
40+
hasMoved: false, // exclude click event
41+
x: btnSwitchPos.x, // right
42+
y: btnSwitchPos.y, // bottom
43+
startX: 0,
44+
startY: 0,
45+
endX: 0,
46+
endY: 0,
47+
};
48+
49+
const fabClass = usePrefixClass('fab');
50+
51+
const onClickHandle = (e: React.MouseEvent<HTMLDivElement>) => {
2752
onClick({ e });
2853
};
2954

55+
const fabStyle = {
56+
...props.style,
57+
right: `${btnSwitchPos.x}px`,
58+
bottom: `${btnSwitchPos.y}px`,
59+
};
60+
61+
const onTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
62+
props.onDragStart?.({ e });
63+
64+
switchPos = {
65+
...switchPos,
66+
startX: e.touches[0].clientX,
67+
startY: e.touches[0].clientY,
68+
};
69+
};
70+
71+
const getSwitchButtonSafeAreaXY = (x: number, y: number) => {
72+
const bottomThreshold = reconvertUnit(props.yBounds?.[1] ?? 0);
73+
const topThreshold = reconvertUnit(props.yBounds?.[0] ?? 0);
74+
75+
const docWidth = Math.min(window.innerWidth, document.documentElement.clientWidth, screen.width);
76+
const docHeight = Math.min(window.innerHeight, document.documentElement.clientHeight, screen.height);
77+
78+
const maxY = docHeight - fabButtonSize.height - topThreshold;
79+
const maxX = docWidth - fabButtonSize.width;
80+
81+
const resultX = Math.max(0, Math.min(maxX, x));
82+
const resultY = Math.max(bottomThreshold, Math.min(maxY, y));
83+
84+
return [resultX, resultY];
85+
};
86+
87+
const onTouchMove = (e: TouchEvent) => {
88+
if (!switchPos.hasMoved) {
89+
switchPos = {
90+
...switchPos,
91+
startX: e.touches[0].clientX,
92+
startY: e.touches[0].clientY,
93+
};
94+
}
95+
96+
e.stopPropagation();
97+
e.preventDefault();
98+
99+
if (!props.draggable) {
100+
return;
101+
}
102+
103+
if (e.touches.length <= 0) {
104+
return;
105+
}
106+
107+
const offsetX = e.touches[0].clientX - switchPos.startX;
108+
const offsetY = e.touches[0].clientY - switchPos.startY;
109+
const x = Math.floor(switchPos.x - offsetX);
110+
const y = Math.floor(switchPos.y - offsetY);
111+
112+
const [newX, newY] = getSwitchButtonSafeAreaXY(x, y);
113+
114+
const toChangeData = { ...btnSwitchPos };
115+
const toChangePos = { ...switchPos, hasMoved: true };
116+
117+
if (props.draggable !== 'vertical') {
118+
toChangeData.x = newX;
119+
toChangePos.endX = newX;
120+
}
121+
if (props.draggable !== 'horizontal') {
122+
toChangeData.y = newY;
123+
toChangePos.endY = newY;
124+
}
125+
switchPos = toChangePos;
126+
setBtnSwitchPos(toChangeData);
127+
};
128+
129+
useEffect(() => {
130+
const fab = fabRef.current;
131+
fab && fab.addEventListener('touchmove', onTouchMove, { passive: false });
132+
// eslint-disable-next-line react-hooks/exhaustive-deps
133+
}, []);
134+
135+
const setSwitchPosition = (switchX: number, switchY: number) => {
136+
const [newSwitchX, newSwitchY] = getSwitchButtonSafeAreaXY(switchX, switchY);
137+
switchPos = {
138+
...switchPos,
139+
x: newSwitchX,
140+
y: newSwitchY,
141+
};
142+
143+
if (props.draggable !== 'vertical') {
144+
setBtnSwitchPos({
145+
...btnSwitchPos,
146+
x: switchX,
147+
});
148+
}
149+
if (props.draggable !== 'horizontal') {
150+
setBtnSwitchPos({
151+
...btnSwitchPos,
152+
y: switchY,
153+
});
154+
}
155+
};
156+
157+
const onTouchEnd = (e: React.TouchEvent<HTMLDivElement>) => {
158+
if (!switchPos.hasMoved) {
159+
return;
160+
}
161+
props.onDragEnd?.({ e });
162+
switchPos = {
163+
...switchPos,
164+
startX: 0,
165+
startY: 0,
166+
hasMoved: false,
167+
};
168+
setSwitchPosition(switchPos.endX, switchPos.endY);
169+
};
170+
30171
return (
31-
<Button
32-
ref={ref}
33-
style={style}
34-
className={FabClasses}
35-
{...baseButtonProps}
36-
{...buttonProps}
172+
<div
173+
ref={fabRef}
174+
className={fabClass}
175+
style={props.draggable && fabButtonSize.width ? { ...fabStyle } : props.style}
37176
onClick={onClickHandle}
177+
onTouchStart={onTouchStart}
178+
onTouchEnd={onTouchEnd}
38179
>
39-
{icon}
40-
{text && <span className={classNames(`${name}__text`)}>{text}</span>}
41-
</Button>
180+
<Button
181+
ref={fabButtonRef}
182+
size="large"
183+
theme="primary"
184+
shape={props.text ? 'round' : 'circle'}
185+
className={`${fabClass}__button`}
186+
{...(buttonProps as TdFabProps['buttonProps'])}
187+
icon={icon}
188+
>
189+
{text}
190+
</Button>
191+
</div>
42192
);
43-
});
193+
};
44194

45195
Fab.displayName = 'Fab';
46196
export default Fab;
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ export default function () {
99
return (
1010
<>
1111
<Fab
12-
icon={<Icon name="add" />}
12+
icon={<Icon name="add" size={24} />}
1313
text="按钮文字"
14+
draggable="vertical"
1415
style={{ right: '16px', bottom: '32px' }}
15-
buttonProps={{ variant: 'outline' }}
1616
onClick={onClick}
1717
/>
1818
</>
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@ export default function () {
66
const onClick = (e) => {
77
console.log('click Fab', e);
88
};
9+
const yBounds = [30, 20];
10+
911
return (
1012
<>
11-
<Fab icon={<Icon name="add" />} style={{ right: '16px', bottom: '32px' }} onClick={onClick} />
13+
<Fab
14+
icon={<Icon name="add" size={24} />}
15+
draggable="all"
16+
style={{ right: '16px', bottom: '32px' }}
17+
onClick={onClick}
18+
yBounds={yBounds}
19+
/>
1220
</>
1321
);
1422
}
Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { Button } from 'tdesign-mobile-react';
33
import TDemoBlock from '../../../site/mobile/components/DemoBlock';
44
import TDemoHeader from '../../../site/mobile/components/DemoHeader';
55
import './style/index.less';
6-
import BaseDemo from './display';
7-
import TextDemo from './text';
6+
import BaseDemo from './base';
7+
import TextDemo from './advance';
88

99
export default function FabDemo() {
1010
const [type, setType] = useState('base');
@@ -17,32 +17,31 @@ export default function FabDemo() {
1717
summary="当功能使用图标即可表示清楚时,可使用纯图标悬浮按钮,例如:添加、发布"
1818
/>
1919

20-
<TDemoBlock title="01 类型" summary="纯图标悬浮按钮">
21-
<ul className="fab-container">
22-
<li>
23-
<Button
24-
className="fab-btn"
25-
theme="primary"
26-
variant="outline"
27-
size="large"
28-
onClick={() => changeType('base')}
29-
>
30-
基础使用
31-
</Button>
32-
</li>
33-
<li>
34-
<Button
35-
className="fab-btn"
36-
theme="primary"
37-
variant="outline"
38-
size="large"
39-
onClick={() => changeType('advance')}
40-
>
41-
进阶使用
42-
</Button>
43-
</li>
44-
</ul>
20+
<TDemoBlock title="01 类型" summary="纯图标悬浮按钮" padding>
21+
<Button
22+
className="fab-btn"
23+
theme="primary"
24+
variant="outline"
25+
size="large"
26+
block
27+
onClick={() => changeType('base')}
28+
>
29+
纯图标悬浮按钮
30+
</Button>
4531
</TDemoBlock>
32+
<TDemoBlock summary="图标加文字悬浮按钮" padding>
33+
<Button
34+
className="fab-btn"
35+
theme="primary"
36+
variant="outline"
37+
size="large"
38+
block
39+
onClick={() => changeType('advance')}
40+
>
41+
图标加文字悬浮按钮
42+
</Button>
43+
</TDemoBlock>
44+
4645
{type === 'base' ? <BaseDemo /> : <TextDemo />}
4746
</div>
4847
);

src/fab/_example/style/index.less

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,4 @@
1-
.fab-container {
2-
li {
3-
display: flex;
4-
justify-content: center;
5-
margin-bottom: 12px;
1+
.tdesign-mobile-demo {
2+
background-color: #f6f6f6;
3+
}
64

7-
.fab-btn {
8-
width: 350px;
9-
border: 1px solid #dcdcdc;
10-
color: #000;
11-
font-size: 16px;
12-
}
13-
}
14-
}

src/fab/defaultProps.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
3+
* */
4+
5+
import { TdFabProps } from './type';
6+
7+
export const fabDefaultProps: TdFabProps = { draggable: false };

src/fab/fab.en-US.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
:: BASE_DOC ::
2+
3+
## API
4+
5+
6+
### Fab Props
7+
8+
name | type | default | description | required
9+
-- | -- | -- | -- | --
10+
className | String | - | className of component | N
11+
style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N
12+
buttonProps | Object | - | Typescript:`ButtonProps`[Button API Documents](./button?tab=api)[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/fab/type.ts) | N
13+
draggable | String / Boolean | false | Typescript:`boolean \| FabDirectionEnum ` `type FabDirectionEnum = 'all' \| 'vertical' \| 'horizontal'`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/fab/type.ts) | N
14+
icon | TElement | - | Typescript:`TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
15+
text | String | - | \- | N
16+
yBounds | Array | - | Typescript:`Array<string \| number>` | N
17+
onClick | Function | | Typescript:`(context: {e: MouseEvent}) => void`<br/> | N
18+
onDragEnd | Function | | Typescript:`(context: { e: TouchEvent }) => void`<br/> | N
19+
onDragStart | Function | | Typescript:`(context: { e: TouchEvent }) => void`<br/> | N

0 commit comments

Comments
 (0)