Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export default {
{
title: 'Fab 悬浮按钮',
name: 'fab',
component: () => import('tdesign-mobile-react/fab/_example/index.jsx'),
component: () => import('tdesign-mobile-react/fab/_example/index.tsx'),
},
{
title: 'NoticeBar 公告栏',
Expand Down
204 changes: 177 additions & 27 deletions src/fab/Fab.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,196 @@
import React, { useMemo, forwardRef, Ref } from 'react';
import classNames from 'classnames';
import Button, { ButtonProps } from '../button';
import React, { useRef, useState, useEffect } from 'react';
import { reconvertUnit } from '../_util/convertUnit';
import Button from '../button';
import { TdFabProps } from './type';
import { fabDefaultProps } from './defaultProps';
import { StyledProps } from '../common';
import useConfig from '../hooks/useConfig';
import { usePrefixClass } from '../hooks/useClass';
import useDefaultProps from '../hooks/useDefaultProps';

export interface FabProps extends TdFabProps, StyledProps {}

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

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

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

const onClickHandle = (e) => {
useEffect(() => {
if (fabButtonRef.current) {
const info = window.getComputedStyle(fabButtonRef.current);

setFabButtonSize({
width: +reconvertUnit(info.width),
height: +reconvertUnit(info.height),
});
}
}, []);

const [btnSwitchPos, setBtnSwitchPos] = useState({
x: 16,
y: 32,
});
let switchPos = {
hasMoved: false, // exclude click event
x: btnSwitchPos.x, // right
y: btnSwitchPos.y, // bottom
startX: 0,
startY: 0,
endX: 0,
endY: 0,
};

const fabClass = usePrefixClass('fab');

const onClickHandle = (e: React.MouseEvent<HTMLDivElement>) => {
onClick({ e });
};

const fabStyle = {
...props.style,
right: `${btnSwitchPos.x}px`,
bottom: `${btnSwitchPos.y}px`,
};

const onTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
props.onDragStart?.({ e });

switchPos = {
...switchPos,
startX: e.touches[0].clientX,
startY: e.touches[0].clientY,
};
};

const getSwitchButtonSafeAreaXY = (x: number, y: number) => {
const bottomThreshold = reconvertUnit(props.yBounds?.[1] ?? 0);
const topThreshold = reconvertUnit(props.yBounds?.[0] ?? 0);

const docWidth = Math.min(window.innerWidth, document.documentElement.clientWidth, screen.width);
const docHeight = Math.min(window.innerHeight, document.documentElement.clientHeight, screen.height);

const maxY = docHeight - fabButtonSize.height - topThreshold;
const maxX = docWidth - fabButtonSize.width;

const resultX = Math.max(0, Math.min(maxX, x));
const resultY = Math.max(bottomThreshold, Math.min(maxY, y));

return [resultX, resultY];
};

const onTouchMove = (e: TouchEvent) => {
if (!switchPos.hasMoved) {
switchPos = {
...switchPos,
startX: e.touches[0].clientX,
startY: e.touches[0].clientY,
};
}

e.stopPropagation();
e.preventDefault();

if (!props.draggable) {
return;
}

if (e.touches.length <= 0) {
return;
}

const offsetX = e.touches[0].clientX - switchPos.startX;
const offsetY = e.touches[0].clientY - switchPos.startY;
const x = Math.floor(switchPos.x - offsetX);
const y = Math.floor(switchPos.y - offsetY);

const [newX, newY] = getSwitchButtonSafeAreaXY(x, y);

const toChangeData = { ...btnSwitchPos };
const toChangePos = { ...switchPos, hasMoved: true };

if (props.draggable !== 'vertical') {
toChangeData.x = newX;
toChangePos.endX = newX;
}
if (props.draggable !== 'horizontal') {
toChangeData.y = newY;
toChangePos.endY = newY;
}
switchPos = toChangePos;
setBtnSwitchPos(toChangeData);
};

useEffect(() => {
const fab = fabRef.current;
fab && fab.addEventListener('touchmove', onTouchMove, { passive: false });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const setSwitchPosition = (switchX: number, switchY: number) => {
const [newSwitchX, newSwitchY] = getSwitchButtonSafeAreaXY(switchX, switchY);
switchPos = {
...switchPos,
x: newSwitchX,
y: newSwitchY,
};

if (props.draggable !== 'vertical') {
setBtnSwitchPos({
...btnSwitchPos,
x: switchX,
});
}
if (props.draggable !== 'horizontal') {
setBtnSwitchPos({
...btnSwitchPos,
y: switchY,
});
}
};

const onTouchEnd = (e: React.TouchEvent<HTMLDivElement>) => {
if (!switchPos.hasMoved) {
return;
}
props.onDragEnd?.({ e });
switchPos = {
...switchPos,
startX: 0,
startY: 0,
hasMoved: false,
};
setSwitchPosition(switchPos.endX, switchPos.endY);
};

return (
<Button
ref={ref}
style={style}
className={FabClasses}
{...baseButtonProps}
{...buttonProps}
<div
ref={fabRef}
className={fabClass}
style={props.draggable && fabButtonSize.width ? { ...fabStyle } : props.style}
onClick={onClickHandle}
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
>
{icon}
{text && <span className={classNames(`${name}__text`)}>{text}</span>}
</Button>
<Button
ref={fabButtonRef}
size="large"
theme="primary"
shape={props.text ? 'round' : 'circle'}
className={`${fabClass}__button`}
{...(buttonProps as TdFabProps['buttonProps'])}
icon={icon}
>
{text}
</Button>
</div>
);
});
};

Fab.displayName = 'Fab';
export default Fab;
4 changes: 2 additions & 2 deletions src/fab/_example/text.jsx → src/fab/_example/advance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ export default function () {
return (
<>
<Fab
icon={<Icon name="add" />}
icon={<Icon name="add" size={24} />}
text="按钮文字"
draggable="vertical"
style={{ right: '16px', bottom: '32px' }}
buttonProps={{ variant: 'outline' }}
onClick={onClick}
/>
</>
Expand Down
10 changes: 9 additions & 1 deletion src/fab/_example/display.jsx → src/fab/_example/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ export default function () {
const onClick = (e) => {
console.log('click Fab', e);
};
const yBounds = [30, 20];

return (
<>
<Fab icon={<Icon name="add" />} style={{ right: '16px', bottom: '32px' }} onClick={onClick} />
<Fab
icon={<Icon name="add" size={24} />}
draggable="all"
style={{ right: '16px', bottom: '32px' }}
onClick={onClick}
yBounds={yBounds}
/>
</>
);
}
53 changes: 26 additions & 27 deletions src/fab/_example/index.jsx → src/fab/_example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Button } from 'tdesign-mobile-react';
import TDemoBlock from '../../../site/mobile/components/DemoBlock';
import TDemoHeader from '../../../site/mobile/components/DemoHeader';
import './style/index.less';
import BaseDemo from './display';
import TextDemo from './text';
import BaseDemo from './base';
import TextDemo from './advance';

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

<TDemoBlock title="01 类型" summary="纯图标悬浮按钮">
<ul className="fab-container">
<li>
<Button
className="fab-btn"
theme="primary"
variant="outline"
size="large"
onClick={() => changeType('base')}
>
基础使用
</Button>
</li>
<li>
<Button
className="fab-btn"
theme="primary"
variant="outline"
size="large"
onClick={() => changeType('advance')}
>
进阶使用
</Button>
</li>
</ul>
<TDemoBlock title="01 类型" summary="纯图标悬浮按钮" padding>
<Button
className="fab-btn"
theme="primary"
variant="outline"
size="large"
block
onClick={() => changeType('base')}
>
纯图标悬浮按钮
</Button>
</TDemoBlock>
<TDemoBlock summary="图标加文字悬浮按钮" padding>
<Button
className="fab-btn"
theme="primary"
variant="outline"
size="large"
block
onClick={() => changeType('advance')}
>
图标加文字悬浮按钮
</Button>
</TDemoBlock>

{type === 'base' ? <BaseDemo /> : <TextDemo />}
</div>
);
Expand Down
16 changes: 3 additions & 13 deletions src/fab/_example/style/index.less
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
.fab-container {
li {
display: flex;
justify-content: center;
margin-bottom: 12px;
.tdesign-mobile-demo {
background-color: #f6f6f6;
}

.fab-btn {
width: 350px;
border: 1px solid #dcdcdc;
color: #000;
font-size: 16px;
}
}
}
7 changes: 7 additions & 0 deletions src/fab/defaultProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */

import { TdFabProps } from './type';

export const fabDefaultProps: TdFabProps = { draggable: false };
19 changes: 19 additions & 0 deletions src/fab/fab.en-US.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
:: BASE_DOC ::

## API


### Fab Props

name | type | default | description | required
-- | -- | -- | -- | --
className | String | - | className of component | N
style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N
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
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
icon | TElement | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
text | String | - | \- | N
yBounds | Array | - | Typescript:`Array<string \| number>` | N
onClick | Function | | Typescript:`(context: {e: MouseEvent}) => void`<br/> | N
onDragEnd | Function | | Typescript:`(context: { e: TouchEvent }) => void`<br/> | N
onDragStart | Function | | Typescript:`(context: { e: TouchEvent }) => void`<br/> | N
Loading