@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.7.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.7.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: cx, top: cy }}
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
+ }