@w5-ui/react 0.7.0 → 0.8.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@w5-ui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "React 18 components for the Dash UI design system - ~170 dashboard-focused components with full type defs and SSR support. Part of Webber's design systems.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@w5-ui/assets": "^0.2.1",
|
|
42
|
-
"@w5-ui/tokens": "^0.
|
|
42
|
+
"@w5-ui/tokens": "^0.8.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/react": "^18.3.0",
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { useState, useRef, useEffect, useId } from 'react'
|
|
1
|
+
import { useState, useRef, useEffect, useId, useLayoutEffect } from 'react'
|
|
2
2
|
import type { KeyboardEvent } from 'react'
|
|
3
|
+
import { computeMenuPosition } from './menu-position'
|
|
4
|
+
|
|
5
|
+
/** Space kept between the menu and every viewport edge. */
|
|
6
|
+
const MENU_MARGIN = 8
|
|
3
7
|
|
|
4
8
|
export interface ContextMenuItem {
|
|
5
9
|
id: string
|
|
@@ -36,6 +40,25 @@ export function ContextMenu({
|
|
|
36
40
|
const menuId = `${uid}-ctxmenu`
|
|
37
41
|
const [activeIdx, setActiveIdx] = useState(0)
|
|
38
42
|
const menuRef = useRef<HTMLUListElement>(null)
|
|
43
|
+
// Placed from the menu's MEASURED size once it is in the DOM. A layout
|
|
44
|
+
// effect runs before the browser paints, so the menu is never seen at
|
|
45
|
+
// this starting point.
|
|
46
|
+
const [position, setPosition] = useState({ left: 0, top: 0 })
|
|
47
|
+
useLayoutEffect(() => {
|
|
48
|
+
if (!open || menuRef.current === null) return
|
|
49
|
+
const { width, height } = menuRef.current.getBoundingClientRect()
|
|
50
|
+
setPosition(
|
|
51
|
+
computeMenuPosition({
|
|
52
|
+
x,
|
|
53
|
+
y,
|
|
54
|
+
menuWidth: width,
|
|
55
|
+
menuHeight: height,
|
|
56
|
+
viewportWidth: window.innerWidth,
|
|
57
|
+
viewportHeight: window.innerHeight,
|
|
58
|
+
margin: MENU_MARGIN,
|
|
59
|
+
}),
|
|
60
|
+
)
|
|
61
|
+
}, [open, x, y, items])
|
|
39
62
|
|
|
40
63
|
const actionItems = items.reduce<ContextMenuItem[]>((acc, e) => {
|
|
41
64
|
if (!('separator' in e)) acc.push(e)
|
|
@@ -85,11 +108,6 @@ export function ContextMenu({
|
|
|
85
108
|
|
|
86
109
|
if (!open) return null
|
|
87
110
|
|
|
88
|
-
const menuW = 168
|
|
89
|
-
const menuH = items.length * 30 + 8
|
|
90
|
-
const cx = Math.min(x, window.innerWidth - menuW - 8)
|
|
91
|
-
const cy = Math.min(y, window.innerHeight - menuH - 8)
|
|
92
|
-
|
|
93
111
|
return (
|
|
94
112
|
<ul
|
|
95
113
|
ref={menuRef}
|
|
@@ -98,7 +116,7 @@ export function ContextMenu({
|
|
|
98
116
|
aria-label={label}
|
|
99
117
|
tabIndex={-1}
|
|
100
118
|
className="ctx-menu"
|
|
101
|
-
style={{ left:
|
|
119
|
+
style={{ left: position.left, top: position.top }}
|
|
102
120
|
onKeyDown={onKeyDown}
|
|
103
121
|
>
|
|
104
122
|
{items.map((entry, i) => {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a context menu opens: at the pointer, the way OS menus do.
|
|
3
|
+
*
|
|
4
|
+
* Takes the menu's MEASURED size, never a guess: menus hold labels of
|
|
5
|
+
* any length, and a guessed width let a long one run off the edge.
|
|
6
|
+
* - It opens at the pointer when it fits.
|
|
7
|
+
* - It flips to the pointer's left (or above it) when it would cross
|
|
8
|
+
* the right edge (or the bottom).
|
|
9
|
+
* - When neither side has room, it is clamped inside the margins, and
|
|
10
|
+
* a menu bigger than the viewport is pinned to the margin.
|
|
11
|
+
*
|
|
12
|
+
* Pure. The Svelte twin is `packages/svelte/src/lib/components/menu-position.ts`.
|
|
13
|
+
*/
|
|
14
|
+
export interface MenuPositionInput {
|
|
15
|
+
/** The pointer, in viewport coordinates. */
|
|
16
|
+
x: number
|
|
17
|
+
y: number
|
|
18
|
+
menuWidth: number
|
|
19
|
+
menuHeight: number
|
|
20
|
+
viewportWidth: number
|
|
21
|
+
viewportHeight: number
|
|
22
|
+
/** Space kept between the menu and every viewport edge. */
|
|
23
|
+
margin: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function along(pointer: number, size: number, viewport: number, margin: number): number {
|
|
27
|
+
const far = viewport - margin
|
|
28
|
+
if (pointer + size <= far) return Math.max(margin, pointer)
|
|
29
|
+
if (pointer - size >= margin) return pointer - size
|
|
30
|
+
return Math.max(margin, Math.min(pointer, far - size))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function computeMenuPosition(input: MenuPositionInput): { left: number; top: number } {
|
|
34
|
+
return {
|
|
35
|
+
left: along(input.x, input.menuWidth, input.viewportWidth, input.margin),
|
|
36
|
+
top: along(input.y, input.menuHeight, input.viewportHeight, input.margin),
|
|
37
|
+
}
|
|
38
|
+
}
|