Skip to content

Commit 9301716

Browse files
authored
add float btn drag (#4)
* add float btn drag * 중복 changeset 제거
1 parent e97f7cb commit 9301716

4 files changed

Lines changed: 157 additions & 9 deletions

File tree

.changeset/early-cities-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"lynx-console": patch
3+
---
4+
5+
드래그 해서 플로팅 위치 변경

package/src/components/FloatingButton.css.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ import { vars } from "../styles/vars";
44

55
export const wrapper = style({
66
position: "fixed",
7-
right: "16px",
8-
bottom: "84px",
9-
zIndex: 9998,
7+
zIndex: 9999,
108
display: "flex",
119
flexDirection: "row",
1210
alignItems: "center",
1311
gap: "8px",
12+
overflow: "visible",
13+
transition: `transform ${vars.$duration.d4} cubic-bezier(0.4, 0, 0.2, 1)`,
1414
});
1515

16-
export const container = style({});
17-
1816
export const button = style({
17+
position: "relative",
18+
overflow: "hidden",
1919
paddingLeft: "8px",
2020
paddingRight: "8px",
2121
paddingTop: "4px",
@@ -30,6 +30,16 @@ export const button = style({
3030
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
3131
});
3232

33+
export const shineOverlay = style({
34+
position: "absolute",
35+
top: "-50%",
36+
left: "-25%",
37+
width: "150%",
38+
height: "200%",
39+
backgroundColor: "rgba(255, 255, 255, 0.2)",
40+
borderRadius: "9999px",
41+
});
42+
3343
export const title = style({
3444
...typography("t4", "regular"),
3545
color: vars.$color.palette.staticWhite,
@@ -43,6 +53,7 @@ export const subtitle = style({
4353
});
4454

4555
export const reloadButton = style({
56+
overflow: "visible",
4657
width: "32px",
4758
height: "32px",
4859
borderRadius: "16px",

package/src/components/FloatingButton.tsx

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,37 @@
11
import type { ReactNode } from "@lynx-js/react";
2+
import { useLongPressDrag } from "../hooks/useLongPressDrag";
23
import * as css from "./FloatingButton.css";
34

45
interface FloatingButtonProps {
56
bindtap: () => void;
67
children: ReactNode;
78
}
89

10+
const SHINE_STYLES = {
11+
idle: {
12+
transform: "scale(0)",
13+
opacity: 0,
14+
},
15+
dragging: {
16+
transform: "scale(1)",
17+
opacity: 1,
18+
transition: "transform 300ms cubic-bezier(0.4, 0, 0.2, 1)",
19+
},
20+
releasing: {
21+
transform: "scale(1)",
22+
opacity: 0,
23+
transition: "opacity 300ms cubic-bezier(0.4, 0, 0.2, 1)",
24+
},
25+
} as const;
26+
927
export const FloatingButton = ({
1028
bindtap,
1129
children,
1230
}: FloatingButtonProps) => {
31+
const { phase, right, bottom, clearTimer, handlers } =
32+
useLongPressDrag(bindtap);
33+
34+
1335
const handleReload = () => {
1436
try {
1537
lynx.reload({}, () => {
@@ -20,12 +42,27 @@ export const FloatingButton = ({
2042
}
2143
};
2244

45+
const isDragging = phase === "dragging";
46+
2347
return (
24-
<view className={css.wrapper}>
25-
<view className={css.container} bindtap={bindtap}>
26-
<view className={css.button}>{children}</view>
48+
<view
49+
className={css.wrapper}
50+
style={{
51+
right: `${right}px`,
52+
bottom: `${bottom}px`,
53+
transform: isDragging ? "scale(1.05)" : "scale(1)",
54+
}}
55+
{...handlers}
56+
>
57+
<view className={css.button}>
58+
{children}
59+
<view className={css.shineOverlay} style={SHINE_STYLES[phase]} />
2760
</view>
28-
<view className={css.reloadButton} bindtap={handleReload}>
61+
<view
62+
className={css.reloadButton}
63+
catchtouchstart={() => clearTimer()}
64+
bindtap={handleReload}
65+
>
2966
<text className={css.reloadIcon}>{"\u21BB"}</text>
3067
</view>
3168
</view>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { useRef, useState } from "@lynx-js/react";
2+
import type { BaseTouchEvent, Target } from "@lynx-js/types";
3+
4+
const LONG_PRESS_DURATION = 400;
5+
const MOVE_THRESHOLD = 5;
6+
7+
const DEFAULT_RIGHT = 16;
8+
const DEFAULT_BOTTOM = 84;
9+
10+
let savedRight = DEFAULT_RIGHT;
11+
let savedBottom = DEFAULT_BOTTOM;
12+
13+
export function useLongPressDrag(onTap: () => void) {
14+
const [right, setRight] = useState(savedRight);
15+
const [bottom, setBottom] = useState(savedBottom);
16+
const [phase, setPhase] = useState<"idle" | "dragging" | "releasing">("idle");
17+
const [tempRight, setTempRight] = useState(savedRight);
18+
const [tempBottom, setTempBottom] = useState(savedBottom);
19+
20+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
21+
const draggingRef = useRef(false);
22+
const startRef = useRef({ x: 0, y: 0, r: 0, b: 0 });
23+
24+
const clearTimer = () => {
25+
if (timerRef.current) {
26+
clearTimeout(timerRef.current);
27+
timerRef.current = null;
28+
}
29+
};
30+
31+
const handleTouchStart = (e: BaseTouchEvent<Target>) => {
32+
startRef.current = {
33+
x: e.detail.x,
34+
y: e.detail.y,
35+
r: right,
36+
b: bottom,
37+
};
38+
draggingRef.current = false;
39+
40+
timerRef.current = setTimeout(() => {
41+
draggingRef.current = true;
42+
setPhase("dragging");
43+
setTempRight(right);
44+
setTempBottom(bottom);
45+
}, LONG_PRESS_DURATION);
46+
};
47+
48+
const handleTouchMove = (e: BaseTouchEvent<Target>) => {
49+
const dx = e.detail.x - startRef.current.x;
50+
const dy = e.detail.y - startRef.current.y;
51+
52+
if (
53+
!draggingRef.current &&
54+
(Math.abs(dx) > MOVE_THRESHOLD || Math.abs(dy) > MOVE_THRESHOLD)
55+
) {
56+
clearTimer();
57+
}
58+
59+
if (!draggingRef.current) return;
60+
61+
// right/bottom 기준이므로 방향 반전
62+
setTempRight(startRef.current.r - dx);
63+
setTempBottom(startRef.current.b - dy);
64+
};
65+
66+
const handleTouchEnd = () => {
67+
clearTimer();
68+
69+
if (draggingRef.current) {
70+
setRight(tempRight);
71+
setBottom(tempBottom);
72+
savedRight = tempRight;
73+
savedBottom = tempBottom;
74+
setPhase("releasing");
75+
draggingRef.current = false;
76+
setTimeout(() => setPhase("idle"), 300);
77+
} else {
78+
onTap();
79+
}
80+
};
81+
82+
const isDragging = phase === "dragging";
83+
84+
return {
85+
phase,
86+
right: isDragging ? tempRight : right,
87+
bottom: isDragging ? tempBottom : bottom,
88+
clearTimer,
89+
handlers: {
90+
bindtouchstart: handleTouchStart,
91+
bindtouchmove: handleTouchMove,
92+
bindtouchend: handleTouchEnd,
93+
},
94+
};
95+
}

0 commit comments

Comments
 (0)