|
1 |
| -import React, { useEffect, useRef, useState } from 'react' |
2 |
| -import { useHoverIntent } from 'react-use-hoverintent' |
| 1 | +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' |
| 2 | +import { useSpring, useSprings } from 'react-spring' |
3 | 3 |
|
4 |
| -import { Icon } from '@iconify/react' |
5 | 4 | import Tippy from '@tippyjs/react'
|
6 |
| -import styled, { css } from 'styled-components' |
| 5 | +import { useGesture } from '@use-gesture/react' |
7 | 6 |
|
8 | 7 | import { TitleWithShortcut } from '@workduck-io/mex-components'
|
9 | 8 |
|
10 | 9 | import { useLayoutStore } from '@mexit/core'
|
11 | 10 | import { WDLogo } from '@mexit/shared'
|
12 | 11 |
|
13 |
| -import { useSidebarTransition } from '../../Hooks/useSidebarTransition' |
14 | 12 | import { getElementById } from '../../Utils/cs-utils'
|
15 | 13 |
|
16 |
| -const DragIcon = styled(Icon)<{ $show: boolean }>` |
17 |
| - margin-right: -18px; |
18 |
| - opacity: 0; |
19 |
| - pointer-events: none; |
20 |
| - transition: margin-right 0.2s ease-in-out, opacity 0.2s ease-in-out; |
21 |
| - ${(props) => |
22 |
| - props.$show && |
23 |
| - css` |
24 |
| - margin-right: 0; |
25 |
| - opacity: 1; |
26 |
| - pointer-events: all; |
27 |
| - `} |
28 |
| -` |
29 |
| - |
30 |
| -const ToggleWrapper = styled.div<{ $endColumnWidth?: string; $expanded?: boolean; $top: number }>` |
31 |
| - position: fixed; |
32 |
| - display: flex; |
33 |
| - align-items: center; |
34 |
| - width: max-content; |
35 |
| -
|
36 |
| - ${({ $expanded, $top, $endColumnWidth, theme }) => |
37 |
| - $expanded |
38 |
| - ? css` |
39 |
| - top: ${$top}px; |
40 |
| - right: calc(${($endColumnWidth ?? '400px') + ' + ' + (theme.additional.hasBlocks ? 0 : -15)}px); |
41 |
| - ` |
42 |
| - : css` |
43 |
| - top: ${$top}px; |
44 |
| - right: 0; |
45 |
| - `} |
46 |
| -
|
47 |
| - z-index: 9999999999; |
48 |
| - padding: 8px; |
49 |
| - border-radius: ${({ theme }) => theme.borderRadius.small}; |
50 |
| - background: ${({ theme }) => theme.tokens.surfaces.sidebar}; |
51 |
| - color: ${({ theme }) => theme.tokens.text.fade}; |
52 |
| - transition: right 0.2s ease-in-out, background 0.2s ease-in-out, width 0.2s ease-in-out; |
53 |
| -
|
54 |
| - svg { |
55 |
| - height: 16px; |
56 |
| - width: 16px; |
57 |
| - } |
58 |
| -
|
59 |
| - &:hover { |
60 |
| - cursor: pointer; |
61 |
| - box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.25); |
62 |
| - background: ${({ theme }) => theme.tokens.colors.primary.default}; |
63 |
| - color: ${({ theme }) => theme.tokens.colors.primary.text}; |
64 |
| -
|
65 |
| - svg { |
66 |
| - path { |
67 |
| - fill: ${({ theme }) => theme.tokens.surfaces.sidebar}; |
68 |
| - } |
69 |
| - } |
70 |
| - } |
71 |
| -
|
72 |
| - ${DragIcon} { |
73 |
| - cursor: ns-resize; |
74 |
| - } |
75 |
| -
|
76 |
| - &:active { |
77 |
| - transition: background 0.1s ease; |
78 |
| - background-color: ${({ theme }) => theme.tokens.colors.primary.default}; |
79 |
| - color: ${({ theme }) => theme.tokens.colors.primary.text}; |
80 |
| - } |
81 |
| -` |
82 |
| - |
83 |
| -const Wrapper = styled.div` |
84 |
| - display: flex; |
85 |
| - align-items: center; |
86 |
| - justify-content: center; |
87 |
| - position: relative; |
88 |
| -
|
89 |
| - /* ::before { |
90 |
| - content: ''; |
91 |
| - position: absolute; |
92 |
| - bottom: 20px; |
93 |
| - height: 20px; |
94 |
| - width: 20px; |
95 |
| - border-bottom-right-radius: ${({ theme }) => theme.borderRadius.large}; |
96 |
| - background: ${({ theme }) => theme.tokens.surfaces.sidebar}; |
97 |
| - color: ${({ theme }) => theme.tokens.text.fade}; |
98 |
| - } */ |
99 |
| -` |
| 14 | +import { ShortenerComponent } from './ShortenerComponent' |
| 15 | +import { ButtonWrapper, DragIcon, ToggleWrapper, Wrapper } from './styled' |
| 16 | + |
| 17 | +// Change this if more buttons have to be added |
| 18 | +const FLOATING_BUTTONS = 1 |
100 | 19 |
|
101 | 20 | export const DraggableToggle = () => {
|
102 |
| - const [isHovering, intentRef, setIsHovering] = useHoverIntent({ timeout: 500 }) |
103 |
| - const [tracking, setTracking] = useState(false) |
| 21 | + const [isHovering, setIsHovering] = useState(false) |
| 22 | + const [editable, setEditable] = useState(false) |
104 | 23 | const { rhSidebar, toggleRHSidebar, toggleTop, setToggleTop } = useLayoutStore()
|
105 |
| - const { endColumnWidth } = useSidebarTransition() |
106 |
| - |
107 |
| - const handleRef = useRef<any>(null) |
108 |
| - |
109 |
| - useEffect(() => { |
110 |
| - const handleMouseDown = (event: MouseEvent) => { |
111 |
| - event.preventDefault() |
112 |
| - event.stopPropagation() |
113 |
| - setTracking(true) |
114 |
| - } |
115 | 24 |
|
116 |
| - if (handleRef?.current) { |
117 |
| - handleRef.current.addEventListener('mousedown', handleMouseDown) |
118 |
| - } |
119 |
| - |
120 |
| - return () => { |
121 |
| - handleRef?.current?.removeEventListener('mousedown', handleMouseDown) |
| 25 | + const avatarRefs = useRef<HTMLDivElement[]>([]) |
| 26 | + const avatarRefInitialPositions = useRef<number[]>([]) |
| 27 | + const toggleRef = useRef<HTMLDivElement>(null) |
| 28 | + const avatarTimeoutRef = useRef<ReturnType<typeof setTimeout>>() |
| 29 | + |
| 30 | + const [{ x, y }, api] = useSpring(() => ({ x: `${window.innerWidth - 40}px`, y: toggleTop }), []) |
| 31 | + const [buttonSprings, buttonApi] = useSprings(FLOATING_BUTTONS, (i) => ({ y: 0 }), []) |
| 32 | + |
| 33 | + const bind = useGesture( |
| 34 | + { |
| 35 | + onDrag: ({ offset: [, y] }) => { |
| 36 | + api.start({ y }) |
| 37 | + }, |
| 38 | + onDragEnd: () => { |
| 39 | + setToggleTop(y.get()) |
| 40 | + }, |
| 41 | + onHover: ({ hovering }) => { |
| 42 | + setIsHovering(hovering) |
| 43 | + } |
| 44 | + }, |
| 45 | + { |
| 46 | + drag: { |
| 47 | + from: () => [0, y.get()], |
| 48 | + axis: 'y', |
| 49 | + // filters click events when dragging |
| 50 | + filterTaps: true, |
| 51 | + pointer: { |
| 52 | + keys: false |
| 53 | + }, |
| 54 | + // Hard coding lower bound for now but would have to change if more buttons are added |
| 55 | + bounds: { |
| 56 | + top: -window.innerHeight, |
| 57 | + bottom: -100 |
| 58 | + } |
| 59 | + } |
122 | 60 | }
|
123 |
| - }, [handleRef?.current]) |
| 61 | + ) |
124 | 62 |
|
125 |
| - useEffect(() => { |
126 |
| - const handleMouseMove = (event: MouseEvent) => { |
127 |
| - event.stopPropagation() |
| 63 | + useLayoutEffect(() => { |
| 64 | + if (avatarRefInitialPositions.current.length === 0) { |
| 65 | + const { y: buttonY } = toggleRef.current.getBoundingClientRect() |
128 | 66 |
|
129 |
| - if (tracking) { |
130 |
| - const newHeight = event.clientY |
131 |
| - setToggleTop(newHeight) |
132 |
| - } |
| 67 | + avatarRefInitialPositions.current = avatarRefs.current.map((node) => buttonY - node.getBoundingClientRect().y) |
133 | 68 | }
|
134 | 69 |
|
135 |
| - window.addEventListener('mousemove', handleMouseMove) |
| 70 | + buttonApi.start((i) => ({ |
| 71 | + y: avatarRefInitialPositions.current[i], |
| 72 | + immediate: true |
| 73 | + })) |
| 74 | + }, []) |
136 | 75 |
|
137 |
| - return () => { |
138 |
| - window.removeEventListener('mousemove', handleMouseMove) |
139 |
| - } |
140 |
| - }, [tracking]) |
| 76 | + useEffect(() => { |
| 77 | + api.start({ x: `${window.innerWidth - (rhSidebar.expanded ? 433 : 40)}px` }) |
| 78 | + }, [rhSidebar.expanded]) |
141 | 79 |
|
142 | 80 | useEffect(() => {
|
143 |
| - const handleMouseUp = (event) => { |
144 |
| - if (tracking) setTracking(false) |
145 |
| - } |
146 |
| - window.addEventListener('mouseup', handleMouseUp) |
| 81 | + if (isHovering || editable) { |
| 82 | + if (avatarTimeoutRef.current) { |
| 83 | + clearTimeout(avatarTimeoutRef.current) |
| 84 | + } |
147 | 85 |
|
148 |
| - return () => { |
149 |
| - window.removeEventListener('mouseup', handleMouseUp) |
| 86 | + buttonApi.start({ |
| 87 | + y: 0 |
| 88 | + }) |
| 89 | + } else { |
| 90 | + avatarTimeoutRef.current = setTimeout(() => { |
| 91 | + buttonApi.start((i) => ({ |
| 92 | + y: avatarRefInitialPositions.current[i] |
| 93 | + })) |
| 94 | + }, 1500) |
150 | 95 | }
|
151 |
| - }, [tracking]) |
| 96 | + }, [isHovering, editable]) |
152 | 97 |
|
153 | 98 | return (
|
154 |
| - <Tippy |
155 |
| - theme="mex-bright" |
156 |
| - placement="left" |
157 |
| - appendTo={() => getElementById('ext-side-nav')} |
158 |
| - content={<TitleWithShortcut title={rhSidebar.expanded ? 'Collapse Sidebar' : 'Expand Sidebar'} />} |
159 |
| - > |
160 |
| - <ToggleWrapper |
161 |
| - $endColumnWidth={endColumnWidth} |
162 |
| - ref={intentRef as any} |
163 |
| - $top={toggleTop} |
164 |
| - $expanded={rhSidebar.expanded} |
165 |
| - onClick={toggleRHSidebar} |
| 99 | + <ToggleWrapper ref={toggleRef} {...bind()} style={{ x, y }}> |
| 100 | + <Tippy |
| 101 | + theme="mex-bright" |
| 102 | + placement="left" |
| 103 | + appendTo={() => getElementById('ext-side-nav')} |
| 104 | + content={<TitleWithShortcut title={rhSidebar.expanded ? 'Collapse Sidebar' : 'Expand Sidebar'} />} |
166 | 105 | >
|
167 |
| - <Wrapper> |
| 106 | + <Wrapper onClick={() => toggleRHSidebar()}> |
168 | 107 | <WDLogo />
|
169 |
| - <DragIcon ref={handleRef} $show={isHovering} icon="ic:outline-drag-indicator" /> |
| 108 | + <DragIcon $show={isHovering} icon="ic:outline-drag-indicator" /> |
170 | 109 | </Wrapper>
|
171 |
| - </ToggleWrapper> |
172 |
| - </Tippy> |
| 110 | + </Tippy> |
| 111 | + |
| 112 | + <ButtonWrapper ref={(ref) => (avatarRefs.current[0] = ref!)} style={buttonSprings[0]}> |
| 113 | + <ShortenerComponent editable={editable} setEditable={setEditable} /> |
| 114 | + </ButtonWrapper> |
| 115 | + </ToggleWrapper> |
173 | 116 | )
|
174 | 117 | }
|
0 commit comments