@rpcbase/client 0.209.0 → 0.211.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apiClient.js +2 -2
- package/package.json +1 -1
- package/ui/ExpandableFloatView/index.tsx +123 -0
- package/ui/Modal/Modal.js +72 -76
- package/ui/Modal/ModalForm/AlertBanner.js +3 -4
- package/ui/Modal/ModalForm/index.js +146 -151
- package/ui/View/index.tsx +17 -0
- package/ui/View/index.web.js +35 -32
- package/ui/helpers/withSuspense/index.js +4 -5
- package/ui/nav/{Header → HeaderContainer}/header.scss +1 -1
- package/ui/nav/HeaderContainer/index.tsx +151 -0
- package/ui/nav/MorphingDropdown/MorphingDropdownContext.tsx +151 -0
- package/ui/nav/MorphingDropdown/MorphingDropdownMenu.tsx +38 -0
- package/ui/nav/MorphingDropdown/MorphingDropdownPortal.tsx +166 -0
- package/ui/nav/MorphingDropdown/MorphingDropdownToggle.tsx +34 -0
- package/ui/nav/MorphingDropdown/index.tsx +16 -0
- package/ui/nav/MorphingDropdown/morphing-dropdown.scss +35 -0
- package/ui/nav/index.ts +6 -1
- package/ui/ExpandableFloatView/index.js +0 -120
- package/ui/View/index.js +0 -12
- package/ui/nav/Header/index.tsx +0 -150
- /package/ui/nav/{Header → HeaderContainer}/variables.scss +0 -0
- /package/ui/{springs.js → springs.ts} +0 -0
package/ui/View/index.web.js
CHANGED
|
@@ -1,41 +1,44 @@
|
|
|
1
|
-
/* @flow */
|
|
2
|
-
import {forwardRef} from "react"
|
|
3
1
|
import {OverlayTrigger, Tooltip} from "react-bootstrap"
|
|
4
2
|
|
|
5
3
|
|
|
6
|
-
export const View =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
export const View = ({
|
|
5
|
+
ref,
|
|
6
|
+
children,
|
|
7
|
+
tooltip,
|
|
8
|
+
tooltipProps = {style: {}},
|
|
9
|
+
className = "",
|
|
10
|
+
...props
|
|
11
|
+
}) => {
|
|
12
|
+
const comp = (
|
|
13
|
+
<div ref={ref} className={cx("d-flex", className)} {...props}>
|
|
14
|
+
{children}
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<Tooltip
|
|
18
|
-
{...overlayTooltipProps}
|
|
19
|
-
{...tooltipProps}
|
|
20
|
-
placement="left-start"
|
|
21
|
-
style={{...style, ...tooltipProps.style}}
|
|
22
|
-
>
|
|
23
|
-
{tooltip}
|
|
24
|
-
</Tooltip>
|
|
25
|
-
)
|
|
26
|
-
}
|
|
18
|
+
if (tooltip) {
|
|
19
|
+
const renderTooltip = ({style, ...overlayTooltipProps}) => {
|
|
27
20
|
return (
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
<Tooltip
|
|
22
|
+
{...overlayTooltipProps}
|
|
23
|
+
{...tooltipProps}
|
|
24
|
+
placement="left-start"
|
|
25
|
+
style={{...style, ...tooltipProps.style}}
|
|
33
26
|
>
|
|
34
|
-
{
|
|
35
|
-
</
|
|
27
|
+
{tooltip}
|
|
28
|
+
</Tooltip>
|
|
36
29
|
)
|
|
37
30
|
}
|
|
31
|
+
return (
|
|
32
|
+
<OverlayTrigger
|
|
33
|
+
placement="top"
|
|
34
|
+
transition={false}
|
|
35
|
+
delay={{show: 0, hide: 100}}
|
|
36
|
+
overlay={renderTooltip}
|
|
37
|
+
>
|
|
38
|
+
{comp}
|
|
39
|
+
</OverlayTrigger>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
43
|
+
return comp
|
|
44
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import React, {Suspense, useEffect, useState} from "react"
|
|
1
|
+
import {Suspense, useEffect, useState, lazy} from "react"
|
|
3
2
|
|
|
4
3
|
import ActivityIndicator from "../../ActivityIndicator"
|
|
5
4
|
|
|
@@ -7,7 +6,7 @@ import ActivityIndicator from "../../ActivityIndicator"
|
|
|
7
6
|
const DELAY_BEFORE_LOADER = 200
|
|
8
7
|
|
|
9
8
|
export const withSuspense = (loadFn, opts = {hideLoader: false}) => {
|
|
10
|
-
const Component =
|
|
9
|
+
const Component = lazy(loadFn)
|
|
11
10
|
|
|
12
11
|
const Loader = () => {
|
|
13
12
|
const [showLoader, setShowLoader] = useState(false)
|
|
@@ -28,11 +27,11 @@ export const withSuspense = (loadFn, opts = {hideLoader: false}) => {
|
|
|
28
27
|
)
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
const Wrapper =
|
|
30
|
+
const Wrapper = ({ref, ...props}) => (
|
|
32
31
|
<Suspense fallback={<Loader />}>
|
|
33
32
|
<Component ref={ref} {...props} />
|
|
34
33
|
</Suspense>
|
|
35
|
-
)
|
|
34
|
+
)
|
|
36
35
|
|
|
37
36
|
return Wrapper
|
|
38
37
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {ReactNode} from "react"
|
|
2
|
+
|
|
3
|
+
// import apiClient from "../../../apiClient"
|
|
4
|
+
|
|
5
|
+
// import SearchAnything from "components/search/SearchAnything"
|
|
6
|
+
|
|
7
|
+
// import LogoNav from "./components/LogoNav"
|
|
8
|
+
|
|
9
|
+
// Env Selector
|
|
10
|
+
// import useFilteredEnvs from "./components/EnvSelector/useFilteredEnvs"
|
|
11
|
+
// import EnvSelectorToggle from "./components/EnvSelector/Toggle"
|
|
12
|
+
// import EnvSelectorDropdown from "./components/EnvSelector/Dropdown"
|
|
13
|
+
// Group Selector
|
|
14
|
+
// import useFilteredGroups from "./components/GroupSelector/useFilteredGroups"
|
|
15
|
+
// import GroupSelectorToggle from "./components/GroupSelector/Toggle"
|
|
16
|
+
// import GroupSelectorDropdown from "./components/GroupSelector/Dropdown"
|
|
17
|
+
|
|
18
|
+
// import MorphingDropdown from "./components/MorphingDropdown"
|
|
19
|
+
|
|
20
|
+
// import AccountsToggle from "./components/AccountsToggle"
|
|
21
|
+
// import NotificationsToggle from "./components/NotificationsToggle"
|
|
22
|
+
// import EnvSettingsToggle from "./components/EnvSettingsToggle"
|
|
23
|
+
// import PublishControl from "./components/PublishControl"
|
|
24
|
+
|
|
25
|
+
// import AccountsDropdown from "./components/AccountsDropdown"
|
|
26
|
+
// import NotificationsDropdown from "./components/NotificationsDropdown"
|
|
27
|
+
// import PhoneDropdown from "components/phone/PhoneDropdown"
|
|
28
|
+
// import EnvSettingsDropdown from "./components/EnvSettingsDropdown"
|
|
29
|
+
|
|
30
|
+
import "./header.scss"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
// const DefaultHeader = () => {
|
|
34
|
+
// return (
|
|
35
|
+
// <>
|
|
36
|
+
// <LogoNav isSignedIn={false} />
|
|
37
|
+
|
|
38
|
+
// <div className="nav-default d-flex flex-row justify-content-between w-100">
|
|
39
|
+
// <div className="d-flex flex-row"></div>
|
|
40
|
+
|
|
41
|
+
// <div className="d-flex flex-row">
|
|
42
|
+
// <a className="nav-link px-2 me-2 fw-bold" href="/signin">
|
|
43
|
+
// Sign In
|
|
44
|
+
// </a>
|
|
45
|
+
// </div>
|
|
46
|
+
// </div>
|
|
47
|
+
// </>
|
|
48
|
+
// )
|
|
49
|
+
// }
|
|
50
|
+
|
|
51
|
+
// const SignedInHeader = () => {
|
|
52
|
+
// const loc = window.location.pathname
|
|
53
|
+
|
|
54
|
+
// const [accounts, setAccounts] = useState([])
|
|
55
|
+
|
|
56
|
+
// const {envs, activeEnv, setFilter: setEnvsFilter} = useFilteredEnvs()
|
|
57
|
+
// const {groups, activeGroup, setFilter: setGroupsFilter} = useFilteredGroups()
|
|
58
|
+
|
|
59
|
+
// useEffect(() => {
|
|
60
|
+
// // TODO: useapi hook with cache
|
|
61
|
+
// const load = async() => {
|
|
62
|
+
// const res = await apiClient.post("/api/v1/auth/get_accounts")
|
|
63
|
+
// assert(res.data.status === "ok")
|
|
64
|
+
// setAccounts(res.data.accounts)
|
|
65
|
+
// // console.log("accounts", res.data.accounts)
|
|
66
|
+
// }
|
|
67
|
+
|
|
68
|
+
// load()
|
|
69
|
+
// }, [])
|
|
70
|
+
|
|
71
|
+
// return (
|
|
72
|
+
// <>
|
|
73
|
+
// <MorphingDropdown.Provider side="left">
|
|
74
|
+
// <LogoNav isSignedIn={true} />
|
|
75
|
+
|
|
76
|
+
// <EnvSelectorToggle activeEnv={activeEnv} />
|
|
77
|
+
// <GroupSelectorToggle activeGroup={activeGroup} />
|
|
78
|
+
|
|
79
|
+
// <MorphingDropdown.Portal>
|
|
80
|
+
// <EnvSelectorDropdown
|
|
81
|
+
// id="env-selector"
|
|
82
|
+
// envs={envs}
|
|
83
|
+
// activeEnv={activeEnv}
|
|
84
|
+
// setFilter={setEnvsFilter}
|
|
85
|
+
// />
|
|
86
|
+
|
|
87
|
+
// <GroupSelectorDropdown
|
|
88
|
+
// id="group-selector"
|
|
89
|
+
// groups={groups}
|
|
90
|
+
// activeGroup={activeGroup}
|
|
91
|
+
// setFilter={setGroupsFilter}
|
|
92
|
+
// />
|
|
93
|
+
// </MorphingDropdown.Portal>
|
|
94
|
+
// </MorphingDropdown.Provider>
|
|
95
|
+
|
|
96
|
+
// {/* Search anything */}
|
|
97
|
+
// <SearchAnything />
|
|
98
|
+
|
|
99
|
+
// <div className="ms-auto d-none d-md-flex flex-row text-truncate">
|
|
100
|
+
// <a className="nav-link mx-2 px-1" href="/docs">
|
|
101
|
+
// Docs
|
|
102
|
+
// </a>
|
|
103
|
+
// <a
|
|
104
|
+
// className={cx("nav-link px-1", {active: loc.startsWith("/marketplace")})}
|
|
105
|
+
// href="/marketplace"
|
|
106
|
+
// >
|
|
107
|
+
// Marketplace
|
|
108
|
+
// </a>
|
|
109
|
+
// </div>
|
|
110
|
+
|
|
111
|
+
// <PublishControl />
|
|
112
|
+
|
|
113
|
+
// {/* Phone */}
|
|
114
|
+
// <PhoneDropdown />
|
|
115
|
+
|
|
116
|
+
// <MorphingDropdown.Provider side="right">
|
|
117
|
+
// {/* WARNING: update the anchor-${ids} on the toggle to match the menus if adding or removing */}
|
|
118
|
+
// <NotificationsToggle />
|
|
119
|
+
|
|
120
|
+
// <EnvSettingsToggle />
|
|
121
|
+
|
|
122
|
+
// <AccountsToggle />
|
|
123
|
+
|
|
124
|
+
// <MorphingDropdown.Portal>
|
|
125
|
+
// {/* Notifications */}
|
|
126
|
+
// <NotificationsDropdown id="notifications" />
|
|
127
|
+
|
|
128
|
+
// {/* Env Settings */}
|
|
129
|
+
// <EnvSettingsDropdown id="env-setup" />
|
|
130
|
+
|
|
131
|
+
// {/* Accounts + Org */}
|
|
132
|
+
// <AccountsDropdown id="accounts" accounts={accounts} />
|
|
133
|
+
// </MorphingDropdown.Portal>
|
|
134
|
+
// </MorphingDropdown.Provider>
|
|
135
|
+
// </>
|
|
136
|
+
// )
|
|
137
|
+
// }
|
|
138
|
+
|
|
139
|
+
export const HeaderContainer = ({children}: {children: ReactNode}) => {
|
|
140
|
+
|
|
141
|
+
if (!children) return null
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<nav
|
|
145
|
+
id="header-container"
|
|
146
|
+
className={"d-flex align-items-center fixed-top bg-dark flex-md-nowrap shadow p-0 text-light"}
|
|
147
|
+
>
|
|
148
|
+
{children}
|
|
149
|
+
</nav>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import assert from "assert"
|
|
2
|
+
import {
|
|
3
|
+
ReactNode,
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useId,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
useEffect,
|
|
10
|
+
useCallback,
|
|
11
|
+
} from "react"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const TOGGLE_OFF_DELAY = 180
|
|
15
|
+
|
|
16
|
+
export const MorphingDropdownContext = createContext(undefined!)
|
|
17
|
+
|
|
18
|
+
export const useMorphingDropdown = () => useContext(MorphingDropdownContext)
|
|
19
|
+
|
|
20
|
+
export const MorphingDropdownProvider = ({
|
|
21
|
+
children,
|
|
22
|
+
side,
|
|
23
|
+
}: {
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
side: "left" | "right";
|
|
26
|
+
}) => {
|
|
27
|
+
const portalId = useId()
|
|
28
|
+
|
|
29
|
+
const setNextMountedRectRef = useRef(() => null)
|
|
30
|
+
const onResetStateRef = useRef(() => null)
|
|
31
|
+
const mousePosRef = useRef({x: 0, y: 0})
|
|
32
|
+
const isMouseOverToggleRef = useRef(false)
|
|
33
|
+
|
|
34
|
+
const [activeId, _setActiveId] = useState()
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
assert(["left", "right"].includes(side))
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const listener = (e) => {
|
|
42
|
+
mousePosRef.current = {
|
|
43
|
+
x: e.pageX,
|
|
44
|
+
y: e.pageY,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
document.addEventListener("mousemove", listener)
|
|
49
|
+
|
|
50
|
+
return () => {
|
|
51
|
+
document.removeEventListener("mousemove", listener)
|
|
52
|
+
}
|
|
53
|
+
}, [])
|
|
54
|
+
|
|
55
|
+
const setActiveId = useCallback((nextId) => {
|
|
56
|
+
let shouldReset = false
|
|
57
|
+
_setActiveId((currentId) => {
|
|
58
|
+
if (nextId === currentId || nextId === null) {
|
|
59
|
+
shouldReset = true
|
|
60
|
+
|
|
61
|
+
return null
|
|
62
|
+
} else return nextId
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (shouldReset) {
|
|
66
|
+
const fn = onResetStateRef.current
|
|
67
|
+
if (typeof fn === "function") {
|
|
68
|
+
fn()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
const close = useCallback(() => {
|
|
74
|
+
setActiveId(null)
|
|
75
|
+
}, [setActiveId])
|
|
76
|
+
|
|
77
|
+
const hideAfterMouseLeave = useCallback(() => {
|
|
78
|
+
// hovering a toggle, skip
|
|
79
|
+
if (isMouseOverToggleRef.current) return
|
|
80
|
+
|
|
81
|
+
// check if we are hovering the menu's portal
|
|
82
|
+
const portalEl = document.getElementById(portalId)
|
|
83
|
+
|
|
84
|
+
if (!portalEl) {
|
|
85
|
+
setActiveId(null)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// portal exists, check if cursor is hovering it
|
|
90
|
+
const rect = portalEl.getBoundingClientRect()
|
|
91
|
+
const {x: mouseX, y: mouseY} = mousePosRef.current
|
|
92
|
+
|
|
93
|
+
const isHovering =
|
|
94
|
+
mouseX >= rect.left &&
|
|
95
|
+
mouseX <= rect.right &&
|
|
96
|
+
mouseY >= rect.top &&
|
|
97
|
+
mouseY <= rect.bottom
|
|
98
|
+
|
|
99
|
+
if (!isHovering) {
|
|
100
|
+
setActiveId(null)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
}, [])
|
|
104
|
+
|
|
105
|
+
const registerSetNextMountedHandler = (fn) => {
|
|
106
|
+
setNextMountedRectRef.current = fn
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const registerOnResetState = (fn) => {
|
|
110
|
+
onResetStateRef.current = fn
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// when a toggle is hovered
|
|
114
|
+
const applyMouseEnterToggle = (id) => {
|
|
115
|
+
isMouseOverToggleRef.current = true
|
|
116
|
+
_setActiveId(id)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const applyMouseLeaveToggle = () => {
|
|
120
|
+
isMouseOverToggleRef.current = false
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
hideAfterMouseLeave()
|
|
123
|
+
}, TOGGLE_OFF_DELAY)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const applyMouseLeaveMenu = () => {
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
hideAfterMouseLeave()
|
|
129
|
+
}, TOGGLE_OFF_DELAY)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<MorphingDropdownContext.Provider
|
|
134
|
+
value={{
|
|
135
|
+
registerSetNextMountedHandler,
|
|
136
|
+
setNextMountedRect: setNextMountedRectRef.current,
|
|
137
|
+
registerOnResetState,
|
|
138
|
+
applyMouseEnterToggle,
|
|
139
|
+
applyMouseLeaveToggle,
|
|
140
|
+
applyMouseLeaveMenu,
|
|
141
|
+
activeId,
|
|
142
|
+
setActiveId,
|
|
143
|
+
close,
|
|
144
|
+
side,
|
|
145
|
+
portalId,
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
{children}
|
|
149
|
+
</MorphingDropdownContext.Provider>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from "assert"
|
|
2
|
+
import {useRef, useLayoutEffect} from "react"
|
|
3
|
+
|
|
4
|
+
import {useMorphingDropdown} from "./MorphingDropdownContext"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const MorphingDropdownMenu = ({
|
|
8
|
+
ref: parentRef,
|
|
9
|
+
id,
|
|
10
|
+
style = {},
|
|
11
|
+
children,
|
|
12
|
+
className = "",
|
|
13
|
+
...props
|
|
14
|
+
}) => {
|
|
15
|
+
assert(id, "missing dropdown menu id")
|
|
16
|
+
|
|
17
|
+
const selfRef = useRef(null)
|
|
18
|
+
|
|
19
|
+
const dropdownContext = useMorphingDropdown()
|
|
20
|
+
|
|
21
|
+
const ref = parentRef || selfRef
|
|
22
|
+
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
const rect = ref.current.getBoundingClientRect()
|
|
25
|
+
dropdownContext.setNextMountedRect(id, rect)
|
|
26
|
+
}, [])
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
ref={ref}
|
|
31
|
+
{...props}
|
|
32
|
+
className={cx("text-light", className)}
|
|
33
|
+
style={{...style}}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import assert from "assert"
|
|
2
|
+
import {useState, useEffect, useRef, ReactNode} from "react"
|
|
3
|
+
import {createPortal} from "react-dom"
|
|
4
|
+
import {CSSTransition, TransitionGroup} from "react-transition-group"
|
|
5
|
+
|
|
6
|
+
import {motion} from "framer-motion"
|
|
7
|
+
|
|
8
|
+
import {SPRING_DEFAULT} from "../../springs"
|
|
9
|
+
|
|
10
|
+
import {useMorphingDropdown} from "./MorphingDropdownContext"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const RIGHT_OFFSET = 0
|
|
14
|
+
|
|
15
|
+
const getElementId = (id) => `header-dd-transition-${id}`
|
|
16
|
+
|
|
17
|
+
export const MorphingDropdownPortal = ({children}: {children: ReactNode}) => {
|
|
18
|
+
const portalElRef = useRef(document.createElement("div"))
|
|
19
|
+
const previousMountedIdRef = useRef(null)
|
|
20
|
+
const transitionNodeRef = useRef(null)
|
|
21
|
+
|
|
22
|
+
const dropdownContext = useMorphingDropdown()
|
|
23
|
+
const {portalId} = dropdownContext
|
|
24
|
+
|
|
25
|
+
const [animatedVals, setAnimatedVals] = useState({})
|
|
26
|
+
const [shouldAnimate, setShouldAnimate] = useState(false)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
document.body.appendChild(portalElRef.current)
|
|
30
|
+
|
|
31
|
+
return () => {
|
|
32
|
+
document.body.removeChild(portalElRef.current)
|
|
33
|
+
}
|
|
34
|
+
}, [])
|
|
35
|
+
|
|
36
|
+
// on click outside dismiss
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
// do nothing if menu isn't open
|
|
39
|
+
if (!dropdownContext.activeId) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const onClick = (e) => {
|
|
44
|
+
const wrapperEl = document.getElementById(portalId)
|
|
45
|
+
|
|
46
|
+
// click is inside the menu itself, so don't do anything
|
|
47
|
+
if (wrapperEl.contains(e.target)) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// clicked outside, but we must ensure we didn't click on an anchor
|
|
52
|
+
let isInsideAnchor = false
|
|
53
|
+
const anchorElements = Array.from(document.querySelectorAll(".morphing-dropdown-anchor"))
|
|
54
|
+
anchorElements.forEach((anchorEl) => {
|
|
55
|
+
if (anchorEl.contains(e.target)) {
|
|
56
|
+
isInsideAnchor = true
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// we have clicked, not in the menu and not on a menu anchor, dismiss
|
|
61
|
+
if (!isInsideAnchor) {
|
|
62
|
+
dropdownContext.setActiveId(null)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
document.body.addEventListener("click", onClick)
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
document.body.removeEventListener("click", onClick)
|
|
70
|
+
}
|
|
71
|
+
}, [dropdownContext.activeId])
|
|
72
|
+
|
|
73
|
+
//
|
|
74
|
+
dropdownContext.registerSetNextMountedHandler((id, rect) => {
|
|
75
|
+
const {height: nextHeight, width: nextWidth} = rect
|
|
76
|
+
|
|
77
|
+
const anchorElement = document.getElementById(`anchor-${id}`)
|
|
78
|
+
assert(anchorElement, "anchor element not found")
|
|
79
|
+
const anchorRect = anchorElement.getBoundingClientRect()
|
|
80
|
+
const centerX = anchorRect.x + anchorRect.width / 2
|
|
81
|
+
|
|
82
|
+
const viewportWidth = window.innerWidth
|
|
83
|
+
|
|
84
|
+
const nextAnimatedVals = {opacity: 1, height: nextHeight, width: nextWidth}
|
|
85
|
+
|
|
86
|
+
// LEFT
|
|
87
|
+
if (dropdownContext.side === "left") {
|
|
88
|
+
const nextLeft = centerX - nextWidth / 2
|
|
89
|
+
let left = 0 + nextLeft
|
|
90
|
+
|
|
91
|
+
if (left < 0) {
|
|
92
|
+
left = 0
|
|
93
|
+
// handles the right corner radius + border
|
|
94
|
+
document.getElementById(portalId).classList.add("docked-left")
|
|
95
|
+
} else {
|
|
96
|
+
document.getElementById(portalId).classList.remove("docked-left")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
nextAnimatedVals.left = left
|
|
100
|
+
}
|
|
101
|
+
// RIGHT
|
|
102
|
+
else if (dropdownContext.side === "right") {
|
|
103
|
+
const nextRight = centerX + nextWidth / 2
|
|
104
|
+
let right = viewportWidth - nextRight
|
|
105
|
+
|
|
106
|
+
if (right < 0) {
|
|
107
|
+
right = 0
|
|
108
|
+
// handles the right corner radius + border
|
|
109
|
+
document.getElementById(portalId).classList.add("docked-right")
|
|
110
|
+
} else {
|
|
111
|
+
document.getElementById(portalId).classList.remove("docked-right")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
nextAnimatedVals.right = right
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// do not animate on first mount / render
|
|
118
|
+
const isNewMount = !previousMountedIdRef.current
|
|
119
|
+
previousMountedIdRef.current = id
|
|
120
|
+
|
|
121
|
+
if (isNewMount) {
|
|
122
|
+
setShouldAnimate(false)
|
|
123
|
+
} else {
|
|
124
|
+
setShouldAnimate(true)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
setAnimatedVals(nextAnimatedVals)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
dropdownContext.registerOnResetState(() => {
|
|
131
|
+
setShouldAnimate(false)
|
|
132
|
+
previousMountedIdRef.current = null
|
|
133
|
+
setAnimatedVals({opacity: 0})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const activeComp = children.find((c) => c.props.id === dropdownContext.activeId)
|
|
137
|
+
|
|
138
|
+
if (!activeComp) return null
|
|
139
|
+
|
|
140
|
+
return createPortal(
|
|
141
|
+
<motion.div
|
|
142
|
+
id={portalId}
|
|
143
|
+
className="morphing-dropdown-wrapper"
|
|
144
|
+
style={{position: "fixed", top: 45, right: RIGHT_OFFSET, overflow: "hidden"}}
|
|
145
|
+
transition={shouldAnimate ? SPRING_DEFAULT : {duration: 0}}
|
|
146
|
+
animate={animatedVals}
|
|
147
|
+
onMouseLeave={dropdownContext.applyMouseLeaveMenu}
|
|
148
|
+
>
|
|
149
|
+
<TransitionGroup>
|
|
150
|
+
<CSSTransition
|
|
151
|
+
id={getElementId(dropdownContext.activeId)}
|
|
152
|
+
key={dropdownContext.activeId}
|
|
153
|
+
nodeRef={transitionNodeRef}
|
|
154
|
+
timeout={60}
|
|
155
|
+
className="header-dropdown"
|
|
156
|
+
classNames="header-dropdown-fade"
|
|
157
|
+
unmountOnExit
|
|
158
|
+
in={true}
|
|
159
|
+
>
|
|
160
|
+
<div ref={transitionNodeRef} style={{position: "absolute", overflow: "hidden", minWidth: 100}}>{activeComp}</div>
|
|
161
|
+
</CSSTransition>
|
|
162
|
+
</TransitionGroup>
|
|
163
|
+
</motion.div>,
|
|
164
|
+
portalElRef.current,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {CSSProperties, ReactNode} from "react"
|
|
2
|
+
import {useMorphingDropdown} from "./MorphingDropdownContext"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export const MorphingDropdownToggle = ({
|
|
6
|
+
id,
|
|
7
|
+
className = "",
|
|
8
|
+
style = {},
|
|
9
|
+
children,
|
|
10
|
+
}: {
|
|
11
|
+
id: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
style?: CSSProperties;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}) => {
|
|
16
|
+
const morphingDropdown = useMorphingDropdown()
|
|
17
|
+
|
|
18
|
+
const onMouseEnter = () => {
|
|
19
|
+
morphingDropdown.applyMouseEnterToggle(id)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
id={`anchor-${id}`}
|
|
25
|
+
className={cx("morphing-dropdown-anchor", className)}
|
|
26
|
+
onClick={() => morphingDropdown.setActiveId(id)}
|
|
27
|
+
onMouseEnter={onMouseEnter}
|
|
28
|
+
onMouseLeave={morphingDropdown.applyMouseLeaveToggle}
|
|
29
|
+
style={{cursor: "pointer", ...style}}
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "./morphing-dropdown.scss"
|
|
2
|
+
|
|
3
|
+
import {MorphingDropdownPortal} from "./MorphingDropdownPortal"
|
|
4
|
+
import {useMorphingDropdown, MorphingDropdownProvider} from "./MorphingDropdownContext"
|
|
5
|
+
import {MorphingDropdownMenu} from "./MorphingDropdownMenu"
|
|
6
|
+
import {MorphingDropdownToggle} from "./MorphingDropdownToggle"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
MorphingDropdownPortal as Portal,
|
|
11
|
+
MorphingDropdownProvider as Provider,
|
|
12
|
+
MorphingDropdownMenu as Menu,
|
|
13
|
+
MorphingDropdownToggle as Toggle,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export {useMorphingDropdown}
|