|
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'; |
4 | 4 | import { TdFabProps } from './type'; |
| 5 | +import { fabDefaultProps } from './defaultProps'; |
5 | 6 | import { StyledProps } from '../common'; |
6 | | -import useConfig from '../hooks/useConfig'; |
| 7 | +import { usePrefixClass } from '../hooks/useClass'; |
| 8 | +import useDefaultProps from '../hooks/useDefaultProps'; |
7 | 9 |
|
8 | 10 | export interface FabProps extends TdFabProps, StyledProps {} |
9 | 11 |
|
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; |
16 | 15 |
|
17 | | - const { classPrefix } = useConfig(); |
18 | | - const name = useMemo(() => `${classPrefix}-fab`, [classPrefix]); |
| 16 | + const fabButtonRef = useRef(null); |
| 17 | + const fabRef = useRef<HTMLDivElement>(null); |
19 | 18 |
|
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, |
24 | 22 | }); |
25 | 23 |
|
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>) => { |
27 | 52 | onClick({ e }); |
28 | 53 | }; |
29 | 54 |
|
| 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 | + |
30 | 171 | 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} |
37 | 176 | onClick={onClickHandle} |
| 177 | + onTouchStart={onTouchStart} |
| 178 | + onTouchEnd={onTouchEnd} |
38 | 179 | > |
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> |
42 | 192 | ); |
43 | | -}); |
| 193 | +}; |
44 | 194 |
|
45 | 195 | Fab.displayName = 'Fab'; |
46 | 196 | export default Fab; |
0 commit comments