@asup/context-menu 1.5.2 → 2.0.2

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/README.md CHANGED
@@ -10,84 +10,136 @@
10
10
 
11
11
  # @asup/context-menu
12
12
 
13
- REACT Context menu, because I couldn't quite find what I wanted.
13
+ A small, highly-configurable React + TypeScript context menu component and helpers.
14
14
 
15
- ## Installation
15
+ Key points:
16
+
17
+ - Works with React 19 (package is built and tested against React 19; React is a peer dependency).
18
+ - TypeScript types included.
19
+ - Lightweight and focused on accessibility and nested sub-menus.
20
+
21
+ ## Storybook
22
+
23
+ Run Storybook to see interactive component examples and documentation.
24
+
25
+ ```powershell
26
+ # install dependencies
27
+ npm install
16
28
 
29
+ # run Storybook
30
+ npm run storybook
31
+
32
+ # run tests
33
+ npm run test
34
+
35
+ # build library bundle
36
+ npm run build
17
37
  ```
18
- # with npm
38
+
39
+ ## Installation
40
+
41
+ Install from npm:
42
+
43
+ ```powershell
19
44
  npm install @asup/context-menu
20
45
  ```
21
46
 
47
+ Note: React and ReactDOM are peer dependencies — install a compatible React version in your application.
48
+
22
49
  ## Usage
23
50
 
24
- Context menu provider, takes a list of available actions and renders a context menu on appropriate click.
25
- Sub menus can be added within each item.
26
- Wrap around the elements that need to have the menu.
51
+ ### ContextMenuHandler
52
+
53
+ ```tsx
54
+ import { ContextMenuHandler, IMenuItem } from "@asup/context-menu";
55
+
56
+ const menuItems: IMenuItem[] = [
57
+ { label: "Item 1", action: () => console.log("Item 1") },
58
+ {
59
+ label: "Item 2",
60
+ action: () => console.log("Item 2"),
61
+ group: [{ label: "Subitem 2.1", action: () => console.log("Subitem 2.1") }],
62
+ },
63
+ { label: "Item 3 (disabled)", disabled: true },
64
+ ];
27
65
 
66
+ <ContextMenuHandler menuItems={menuItems}>
67
+ <div>Right click here to open the menu</div>
68
+ </ContextMenuHandler>;
28
69
  ```
29
- import { ContextMenuProvider, IMenuItem } from '@asup/context-menu';
30
-
31
- <ContextMenuHandler
32
- menuItems={[
33
- {
34
- label: 'Item 1',
35
- action: () => {
36
- console.log('Item 1 function run');
37
- },
38
- },
39
- {
40
- label: 'Item 2',
41
- action: () => console.log('Item 2 function run'),
42
- group: [
43
- { label: 'Subitem 2.1', action: () => console.log('Item 2.1 function run') },
44
- ],
45
- },
46
- {
47
- label: 'Item 3',
48
- action: () => console.log('Item 3 function run'),
49
- disabled: true,
50
- },
51
- ]}
52
- >
53
- <Chilren
54
- where the context menu is applied...
55
- />
56
- </ContextMenuHandler>
57
70
 
71
+ ### AutoHeight
72
+
73
+ Use `AutoHeight` to wrap content that may expand/contract — it will manage layout height for smoother transitions.
74
+
75
+ ```tsx
76
+ import { AutoHeight } from "@asup/context-menu";
77
+
78
+ <AutoHeight>
79
+ <div style={{ padding: 12 }}>
80
+ This content can change size; AutoHeight will help the layout adjust smoothly.
81
+ </div>
82
+ </AutoHeight>;
58
83
  ```
59
84
 
85
+ ### ClickForMenu
86
+
87
+ `ClickForMenu` attaches a click-based menu to any element (useful for toolbar buttons or inline actions). Give each instance a stable `id` so the trigger element can be referenced and cleaned up correctly.
88
+
89
+ ```tsx
90
+ import { ClickForMenu, IMenuItem } from "@asup/context-menu";
91
+
92
+ const clickItems: IMenuItem[] = [
93
+ { label: "Edit", action: () => console.log("Edit") },
94
+ { label: "Delete", action: () => console.log("Delete") },
95
+ ];
96
+
97
+ <ClickForMenu
98
+ id="actions-menu"
99
+ menuItems={clickItems}
100
+ >
101
+ <button type="button">Actions</button>
102
+ </ClickForMenu>;
60
103
  ```
61
- import { ContextWindowStack, ContextWindow }
62
-
63
- // Context window stack needs to cover the application, or portion where context windows cannot clash with each other
64
- <ContextWindowStack>
65
- ...rest of app
66
-
67
- <ContextWindow
68
- id='window-1'
69
- title={'Window 1'}
70
- visible={visible}
71
- style={ window styling, applied to the window div}
72
- onOpen={ called function on opening}
73
- onClose={ called function on closing (close cross in the window)}
74
- >
75
- {window contents}
76
- </ContextWindow>
77
-
78
- <ContextWindow
79
- id='window-2'
80
- title={'Window 2'}
81
- visible={visible}
82
- style={ window styling, applied to the window div}
83
- onOpen={ called function on opening}
84
- onClose={ called function on closing (close cross in the window)}
85
- >
86
- {window contents}
87
- </ContextWindow>
88
-
89
- ...end of app
90
-
91
- </ContextWindowStack>
92
104
 
105
+ ### ContextWindow
106
+
107
+ ```tsx
108
+ import { ContextWindow } from "@asup/context-menu";
109
+
110
+ <ContextWindow
111
+ id="window-1"
112
+ title="Window 1"
113
+ visible={true}
114
+ onClose={() => {}}
115
+ >
116
+ Window content
117
+ </ContextWindow>;
93
118
  ```
119
+
120
+ See the Storybook for interactive examples and more options.
121
+
122
+ ## Development
123
+
124
+ Useful scripts (from `package.json`):
125
+
126
+ - `npm run prepare` — run Husky (prepares Git hooks).
127
+ - `npm run storybook` — start Storybook to view component examples.
128
+ - `npm run build-storybook` — build a static Storybook site.
129
+ - `npm run test` — run Jest and collect coverage (`jest --collectCoverage=true`).
130
+ - `npm run test-watch` — run Jest in watch mode with coverage (`jest --watch --collectCoverage=true --maxWorkers=4`).
131
+ - `npm run eslint` — run ESLint over `src` (pattern: `src/**/*.{js,jsx,ts,tsx}`).
132
+ - `npm run build` — build the library bundle with Parcel. This script clears the Parcel cache before building (`parcel build src/main.ts`).
133
+
134
+ ## Contributing
135
+
136
+ Contributions and PRs are welcome. Please follow the repository conventions (linting, types, and tests).
137
+
138
+ 1. Fork the repo and create a feature branch.
139
+ 2. Run `npm install`.
140
+ 3. Run and update tests: `npm run test`.
141
+ 4. Submit a PR and describe your changes.
142
+
143
+ ## License
144
+
145
+ MIT — see the `LICENCE` file for details.
package/dist/cjs/main.js CHANGED
@@ -40,7 +40,6 @@ $parcel$export($a68bd8a6c0fd98c2$exports, "ClickForMenu", function () { return $
40
40
  $parcel$export($a68bd8a6c0fd98c2$exports, "ContextMenu", function () { return $5150b66b01c99189$export$8dc6765e8be191c7; });
41
41
  $parcel$export($a68bd8a6c0fd98c2$exports, "ContextMenuHandler", function () { return $3c568ee547c732c3$export$ed4f9641643dc7e4; });
42
42
  $parcel$export($a68bd8a6c0fd98c2$exports, "ContextWindow", function () { return $46fb0088a1bbb6d8$export$1af8984c69ba1b24; });
43
- $parcel$export($a68bd8a6c0fd98c2$exports, "ContextWindowStack", function () { return $16208d559c772441$export$9f37482ccd50dad2; });
44
43
 
45
44
 
46
45
 
@@ -57,7 +56,7 @@ $796f463330153c24$export$563bd8f955c52746 = `aiw-AutoHeight-module-pDlSVW-autoHe
57
56
 
58
57
 
59
58
  function $95149596d5a7ed2b$export$77bf000da9303d1(_param) {
60
- var { children: children, hide: hide, duration: duration = 300 } = _param, rest = (0, $gTuX4$swchelperscjs_object_without_propertiescjs._)(_param, [
59
+ var { children: children, hide: hide = false, duration: duration = 300 } = _param, rest = (0, $gTuX4$swchelperscjs_object_without_propertiescjs._)(_param, [
61
60
  "children",
62
61
  "hide",
63
62
  "duration"
@@ -65,29 +64,63 @@ function $95149596d5a7ed2b$export$77bf000da9303d1(_param) {
65
64
  const wrapperRef = (0, $gTuX4$react.useRef)(null);
66
65
  const innerRef = (0, $gTuX4$react.useRef)(null);
67
66
  const [height, setHeight] = (0, $gTuX4$react.useState)(null);
68
- const targetChildren = (0, $gTuX4$react.useMemo)(()=>hide || !children ? /*#__PURE__*/ (0, $gTuX4$reactjsxruntime.jsx)("div", {
69
- style: {
70
- height: "1px"
71
- }
72
- }) : children, [
73
- children,
74
- hide
75
- ]);
76
- const setTargetHeight = (0, $gTuX4$react.useCallback)(()=>{
77
- const inner = innerRef.current;
78
- var _inner_offsetHeight;
79
- // Initial draw to get the height of children and deployed children
80
- const deployedHeight = hide ? 1 : (_inner_offsetHeight = inner === null || inner === void 0 ? void 0 : inner.offsetHeight) !== null && _inner_offsetHeight !== void 0 ? _inner_offsetHeight : 0;
81
- setHeight(deployedHeight);
67
+ const [animationState, setAnimationState] = (0, $gTuX4$react.useState)(!hide ? "open" : "closed");
68
+ const rafRef = (0, $gTuX4$react.useRef)(null);
69
+ const timeoutRef = (0, $gTuX4$react.useRef)(null);
70
+ const targetChildren = animationState === "closed" || !children ? null : children;
71
+ const setTargetHeight = (0, $gTuX4$react.useEffectEvent)((newHeight)=>{
72
+ setHeight(newHeight);
73
+ });
74
+ const transitionToOpening = (0, $gTuX4$react.useEffectEvent)(()=>{
75
+ // Cancel any pending close timeout
76
+ if (timeoutRef.current !== null) {
77
+ clearTimeout(timeoutRef.current);
78
+ timeoutRef.current = null;
79
+ }
80
+ setAnimationState("opening");
81
+ const id = window.requestAnimationFrame(()=>{
82
+ rafRef.current = null;
83
+ setTargetHeight(1);
84
+ const frameId = window.requestAnimationFrame(()=>{
85
+ const inner = innerRef.current;
86
+ if (inner) {
87
+ setTargetHeight(inner.offsetHeight);
88
+ setAnimationState("open");
89
+ }
90
+ });
91
+ rafRef.current = frameId;
92
+ });
93
+ rafRef.current = id;
94
+ });
95
+ const transitionToClosing = (0, $gTuX4$react.useEffectEvent)(()=>{
96
+ // Cancel any pending RAF
97
+ if (rafRef.current !== null) {
98
+ cancelAnimationFrame(rafRef.current);
99
+ rafRef.current = null;
100
+ }
101
+ setAnimationState("closing");
102
+ setTargetHeight(1);
103
+ timeoutRef.current = window.setTimeout(()=>{
104
+ timeoutRef.current = null;
105
+ setAnimationState("closed");
106
+ }, duration);
107
+ });
108
+ (0, $gTuX4$react.useLayoutEffect)(()=>{
109
+ if (!hide) // Want to show: transition to open
110
+ {
111
+ if (animationState === "closed" || animationState === "closing") transitionToOpening();
112
+ } else // Want to hide: transition to closed
113
+ if (animationState === "open" || animationState === "opening") transitionToClosing();
82
114
  }, [
83
- hide
115
+ hide,
116
+ animationState
84
117
  ]);
85
- // Add ResizeObserver to update height on content resize, debounced
118
+ // Setup ResizeObserver to track content size changes
86
119
  (0, $gTuX4$react.useEffect)(()=>{
87
120
  const transition = innerRef.current;
88
121
  if (transition) {
89
122
  const observer = new ResizeObserver(()=>{
90
- setTargetHeight();
123
+ if (animationState === "open") setTargetHeight(transition.offsetHeight);
91
124
  });
92
125
  observer.observe(transition);
93
126
  return ()=>{
@@ -95,19 +128,26 @@ function $95149596d5a7ed2b$export$77bf000da9303d1(_param) {
95
128
  };
96
129
  }
97
130
  }, [
98
- setTargetHeight
99
- ]);
100
- // Trigger height change on children update
101
- (0, $gTuX4$react.useLayoutEffect)(()=>{
102
- setTargetHeight();
103
- }, [
104
- setTargetHeight,
105
- children
131
+ animationState
106
132
  ]);
133
+ // Cleanup on unmount
134
+ (0, $gTuX4$react.useEffect)(()=>{
135
+ return ()=>{
136
+ if (rafRef.current !== null) {
137
+ cancelAnimationFrame(rafRef.current);
138
+ rafRef.current = null;
139
+ }
140
+ if (timeoutRef.current !== null) {
141
+ clearTimeout(timeoutRef.current);
142
+ timeoutRef.current = null;
143
+ }
144
+ };
145
+ }, []);
107
146
  return /*#__PURE__*/ (0, $gTuX4$reactjsxruntime.jsx)("div", (0, $gTuX4$swchelperscjs_object_spread_propscjs._)((0, $gTuX4$swchelperscjs_object_spreadcjs._)({}, rest), {
108
147
  className: (0, (/*@__PURE__*/$parcel$interopDefault($796f463330153c24$exports))).autoHeightWrapper,
109
148
  ref: wrapperRef,
110
149
  style: (0, $gTuX4$swchelperscjs_object_spread_propscjs._)((0, $gTuX4$swchelperscjs_object_spreadcjs._)({}, rest.style), {
150
+ display: animationState === "closed" ? "none" : undefined,
111
151
  height: height ? `${height}px` : "auto",
112
152
  transitionDuration: `${duration}ms`
113
153
  }),
@@ -247,13 +287,25 @@ const $5150b66b01c99189$var$ESTIMATED_MENU_ITEM_HEIGHT = 34;
247
287
  const $5150b66b01c99189$var$ESTIMATED_MENU_PADDING = 4;
248
288
  const $5150b66b01c99189$var$ESTIMATED_MENU_WIDTH = 200;
249
289
  const $5150b66b01c99189$export$8dc6765e8be191c7 = /*#__PURE__*/ (0, $gTuX4$react.forwardRef)(({ visible: visible, entries: entries, xPos: xPos, yPos: yPos, toClose: toClose }, ref)=>{
250
- // Check that menu can fit inside the window
251
- let menuHeight = entries.length * $5150b66b01c99189$var$ESTIMATED_MENU_ITEM_HEIGHT + $5150b66b01c99189$var$ESTIMATED_MENU_PADDING;
252
- let menuWidth = $5150b66b01c99189$var$ESTIMATED_MENU_WIDTH;
253
- if (ref && typeof ref !== "function" && ref.current instanceof HTMLDivElement) {
254
- menuHeight = ref.current.offsetHeight;
255
- menuWidth = ref.current.offsetWidth;
256
- }
290
+ // Measure menu size after mount/render to avoid accessing refs during render
291
+ const [menuHeight, setMenuHeight] = (0, $gTuX4$react.useState)(entries.length * $5150b66b01c99189$var$ESTIMATED_MENU_ITEM_HEIGHT + $5150b66b01c99189$var$ESTIMATED_MENU_PADDING);
292
+ const [menuWidth, setMenuWidth] = (0, $gTuX4$react.useState)($5150b66b01c99189$var$ESTIMATED_MENU_WIDTH);
293
+ (0, $gTuX4$react.useLayoutEffect)(()=>{
294
+ // Only measure when visible; ref access inside effect is allowed
295
+ if (visible && ref && typeof ref !== "function" && ref.current instanceof HTMLDivElement) {
296
+ setMenuHeight(ref.current.offsetHeight);
297
+ setMenuWidth(ref.current.offsetWidth);
298
+ }
299
+ // When not visible, fall back to estimates
300
+ if (!visible) {
301
+ setMenuHeight(entries.length * $5150b66b01c99189$var$ESTIMATED_MENU_ITEM_HEIGHT + $5150b66b01c99189$var$ESTIMATED_MENU_PADDING);
302
+ setMenuWidth($5150b66b01c99189$var$ESTIMATED_MENU_WIDTH);
303
+ }
304
+ }, [
305
+ visible,
306
+ entries,
307
+ ref
308
+ ]);
257
309
  const adjustedYPos = yPos + menuHeight > window.innerHeight ? Math.max(window.innerHeight - menuHeight - $5150b66b01c99189$var$ESTIMATED_MENU_PADDING, 0) : yPos;
258
310
  const adjustedXPos = xPos + menuWidth > window.innerWidth ? Math.max(window.innerWidth - menuWidth - $5150b66b01c99189$var$ESTIMATED_MENU_PADDING, 0) : xPos;
259
311
  return /*#__PURE__*/ (0, $gTuX4$reactjsxruntime.jsx)("div", {
@@ -294,9 +346,10 @@ const $a79b75d040e03c92$export$d4ebdd58e04c6ace = (_param)=>{
294
346
  // Set up outsideClick handler
295
347
  const menuRef = (0, $gTuX4$react.useRef)(null);
296
348
  // Handle click off the menu
297
- const handleClick = (0, $gTuX4$react.useCallback)((e)=>{
349
+ // eslint-disable-next-line react-hooks/exhaustive-deps
350
+ const handleClick = (e)=>{
298
351
  if (menuRef.current && (e.target instanceof Element && !menuRef.current.contains(e.target) || !(e.target instanceof Element))) setMenuInDom(false);
299
- }, []);
352
+ };
300
353
  const removeController = (0, $gTuX4$react.useRef)(null);
301
354
  const removeTimeoutRef = (0, $gTuX4$react.useRef)(null);
302
355
  (0, $gTuX4$react.useEffect)(()=>{
@@ -531,30 +584,26 @@ const $3c568ee547c732c3$export$ed4f9641643dc7e4 = (_param)=>{
531
584
  "menuItems",
532
585
  "showLowMenu"
533
586
  ]);
534
- var _divHandlderRef_current;
535
587
  // Check for higher content menu
536
588
  const higherContext = (0, $gTuX4$react.useContext)($3c568ee547c732c3$export$fc58dc71afe92de2);
537
- const thisMenuItems = (0, $gTuX4$react.useMemo)(()=>[
538
- ...higherContext !== null ? [
539
- ...higherContext.menuItems,
540
- ...[
541
- higherContext.menuItems.length > 0 && !$3c568ee547c732c3$var$isDivider(higherContext.menuItems[higherContext.menuItems.length - 1].label) && menuItems.length > 0 && !$3c568ee547c732c3$var$isDivider(menuItems[0].label) ? {
542
- label: /*#__PURE__*/ (0, $gTuX4$reactjsxruntime.jsx)("hr", {
543
- style: {
544
- flexGrow: 1,
545
- cursor: "none",
546
- margin: "0",
547
- padding: "0"
548
- }
549
- })
550
- } : null
551
- ].filter((item)=>item !== null)
552
- ] : [],
553
- ...menuItems
554
- ], [
555
- higherContext,
556
- menuItems
557
- ]);
589
+ const thisMenuItems = [
590
+ ...higherContext !== null ? [
591
+ ...higherContext.menuItems,
592
+ ...[
593
+ higherContext.menuItems.length > 0 && !$3c568ee547c732c3$var$isDivider(higherContext.menuItems[higherContext.menuItems.length - 1].label) && menuItems.length > 0 && !$3c568ee547c732c3$var$isDivider(menuItems[0].label) ? {
594
+ label: /*#__PURE__*/ (0, $gTuX4$reactjsxruntime.jsx)("hr", {
595
+ style: {
596
+ flexGrow: 1,
597
+ cursor: "none",
598
+ margin: "0",
599
+ padding: "0"
600
+ }
601
+ })
602
+ } : null
603
+ ].filter((item)=>item !== null)
604
+ ] : [],
605
+ ...menuItems
606
+ ];
558
607
  // Menu resources
559
608
  const divHandlderRef = (0, $gTuX4$react.useRef)(null);
560
609
  const menuRef = (0, $gTuX4$react.useRef)(null);
@@ -564,9 +613,34 @@ const $3c568ee547c732c3$export$ed4f9641643dc7e4 = (_param)=>{
564
613
  const [menuInDom, setMenuInDom] = (0, $gTuX4$react.useState)(false);
565
614
  const [mouseOverHandlerDiv, setMouseOverHandlerDiv] = (0, $gTuX4$react.useState)(false);
566
615
  const [mouseOverMenu, setMouseOverMenu] = (0, $gTuX4$react.useState)(false);
567
- var _divHandlderRef_current_getBoundingClientRect;
568
- // Get holder position
569
- const divHandlerPos = (_divHandlderRef_current_getBoundingClientRect = (_divHandlderRef_current = divHandlderRef.current) === null || _divHandlderRef_current === void 0 ? void 0 : _divHandlderRef_current.getBoundingClientRect()) !== null && _divHandlderRef_current_getBoundingClientRect !== void 0 ? _divHandlderRef_current_getBoundingClientRect : null;
616
+ // Holder position - measured in an effect to avoid reading refs during render
617
+ const [divHandlerPos, setDivHandlerPos] = (0, $gTuX4$react.useState)(null);
618
+ (0, $gTuX4$react.useLayoutEffect)(()=>{
619
+ function updatePos() {
620
+ if (divHandlderRef.current) setDivHandlerPos(divHandlderRef.current.getBoundingClientRect());
621
+ }
622
+ // When the handler is hovered or the menu is mounted, ensure we have a fresh position
623
+ if (mouseOverHandlerDiv || menuInDom) updatePos();
624
+ // Attach listeners while the menu/low-menu may be visible so the position stays correct
625
+ if (mouseOverHandlerDiv || menuInDom) {
626
+ window.addEventListener("resize", updatePos);
627
+ // listen on capture to catch scrolls from ancestor elements as well
628
+ window.addEventListener("scroll", updatePos, true);
629
+ let ro = null;
630
+ if (typeof ResizeObserver !== "undefined" && divHandlderRef.current) {
631
+ ro = new ResizeObserver(()=>updatePos());
632
+ ro.observe(divHandlderRef.current);
633
+ }
634
+ return ()=>{
635
+ window.removeEventListener("resize", updatePos);
636
+ window.removeEventListener("scroll", updatePos, true);
637
+ if (ro) ro.disconnect();
638
+ };
639
+ }
640
+ }, [
641
+ mouseOverHandlerDiv,
642
+ menuInDom
643
+ ]);
570
644
  // Handle click off the menu
571
645
  const handleClick = (0, $gTuX4$react.useCallback)((e)=>{
572
646
  var _menuRef_current;
@@ -761,17 +835,19 @@ const $46fb0088a1bbb6d8$export$1af8984c69ba1b24 = (_param)=>{
761
835
  var _rest_style, _rest_style1, _rest_style2, _rest_style3;
762
836
  const divRef = (0, $gTuX4$react.useRef)(null);
763
837
  const windowRef = (0, $gTuX4$react.useRef)(null);
764
- const [windowInDOM, setWindowInDOM] = (0, $gTuX4$react.useState)(false);
765
- const [windowVisible, setWindowVisible] = (0, $gTuX4$react.useState)(false);
766
838
  const [zIndex, setZIndex] = (0, $gTuX4$react.useState)(minZIndex);
767
839
  const resizeListenerRef = (0, $gTuX4$react.useRef)(null);
840
+ // Track internal state: whether window is in DOM and whether it's been positioned
841
+ const [windowInDOM, setWindowInDOM] = (0, $gTuX4$react.useState)(false);
842
+ const [windowVisible, setWindowVisible] = (0, $gTuX4$react.useState)(false);
843
+ const [, startTransition] = (0, $gTuX4$react.useTransition)();
768
844
  // Position
769
845
  const windowPos = (0, $gTuX4$react.useRef)({
770
846
  x: 0,
771
847
  y: 0
772
848
  });
773
849
  const [moving, setMoving] = (0, $gTuX4$react.useState)(false);
774
- const move = (0, $gTuX4$react.useCallback)((x, y)=>{
850
+ const move = (x, y)=>{
775
851
  if (windowRef.current && windowPos.current) {
776
852
  const window1 = windowRef.current;
777
853
  const pos = windowPos.current;
@@ -779,24 +855,21 @@ const $46fb0088a1bbb6d8$export$1af8984c69ba1b24 = (_param)=>{
779
855
  pos.y += y;
780
856
  window1.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
781
857
  }
782
- }, []);
783
- const checkPosition = (0, $gTuX4$react.useCallback)(()=>{
858
+ };
859
+ // eslint-disable-next-line react-hooks/exhaustive-deps
860
+ const checkPosition = ()=>{
784
861
  const chkPos = (0, $c986bcdcae4b83bd$export$d81cfea7c54be196)(windowRef);
785
862
  move(chkPos.translateX, chkPos.translateY);
786
- }, [
787
- move
788
- ]);
789
- const mouseMove = (0, $gTuX4$react.useCallback)((e)=>{
863
+ };
864
+ const mouseMove = (e)=>{
790
865
  e.preventDefault();
791
866
  e.stopPropagation();
792
867
  move(e.movementX, e.movementY);
793
- }, [
794
- move
795
- ]);
868
+ };
796
869
  // Store stable references to mouseMove and mouseUp for cleanup
797
870
  const mouseMoveRef = (0, $gTuX4$react.useRef)(mouseMove);
798
871
  const mouseUpRef = (0, $gTuX4$react.useRef)(()=>{});
799
- const mouseUp = (0, $gTuX4$react.useCallback)((e)=>{
872
+ const mouseUp = (e)=>{
800
873
  e.preventDefault();
801
874
  e.stopPropagation();
802
875
  setMoving(false);
@@ -808,55 +881,65 @@ const $46fb0088a1bbb6d8$export$1af8984c69ba1b24 = (_param)=>{
808
881
  resizeListenerRef.current = null;
809
882
  }
810
883
  if (e.target && (e.target instanceof HTMLElement || e.target instanceof SVGElement)) e.target.style.userSelect = "auto";
811
- }, [
812
- checkPosition
813
- ]);
884
+ };
814
885
  // Update refs when callbacks change
815
886
  (0, $gTuX4$react.useEffect)(()=>{
816
887
  mouseMoveRef.current = mouseMove;
817
888
  mouseUpRef.current = mouseUp;
818
- }, [
819
- mouseMove,
820
- mouseUp
821
- ]);
889
+ });
822
890
  // Helper function to push this window to the top
823
- const pushToTop = (0, $gTuX4$react.useCallback)(()=>{
891
+ const pushToTop = ()=>{
824
892
  const maxZIndex = $46fb0088a1bbb6d8$var$getMaxZIndex(minZIndex);
825
893
  setZIndex(maxZIndex + 1);
894
+ };
895
+ // Sync windowInDOM with visible prop using a layout effect to avoid ESLint warnings
896
+ // This effect derives state from props, which is acceptable when there's no synchronous setState
897
+ (0, $gTuX4$react.useEffect)(()=>{
898
+ if (visible && !windowInDOM) // Window should be in DOM when visible becomes true
899
+ startTransition(()=>{
900
+ setWindowInDOM(true);
901
+ });
902
+ else if (!visible && windowInDOM) // Window should leave DOM when visible becomes false
903
+ startTransition(()=>{
904
+ setWindowInDOM(false);
905
+ setWindowVisible(false);
906
+ });
826
907
  }, [
827
- minZIndex
908
+ visible,
909
+ windowInDOM,
910
+ startTransition
828
911
  ]);
829
- // Update visibility
912
+ // Position and show window after it's added to DOM
830
913
  (0, $gTuX4$react.useEffect)(()=>{
831
- // Visible set, but not in DOM
832
- if (visible && !windowInDOM) setWindowInDOM(true);
833
- else if (visible && windowInDOM && !windowVisible) {
834
- pushToTop();
835
- setWindowVisible(visible);
836
- onOpen === null || onOpen === void 0 ? void 0 : onOpen();
837
- // Get starting position
838
- if (divRef.current && windowRef.current) {
839
- const parentPos = divRef.current.getBoundingClientRect();
840
- const pos = windowRef.current.getBoundingClientRect();
841
- const windowHeight = pos.bottom - pos.top;
842
- windowRef.current.style.left = `${parentPos.left}px`;
843
- windowRef.current.style.top = `${parentPos.bottom + windowHeight < window.innerHeight ? parentPos.bottom : Math.max(0, parentPos.top - windowHeight)}px`;
844
- windowRef.current.style.transform = "";
845
- windowPos.current = {
846
- x: 0,
847
- y: 0
848
- };
849
- }
914
+ if (windowInDOM && !windowVisible && visible && divRef.current && windowRef.current) {
915
+ // Position the window
916
+ const parentPos = divRef.current.getBoundingClientRect();
917
+ const pos = windowRef.current.getBoundingClientRect();
918
+ const windowHeight = pos.bottom - pos.top;
919
+ windowRef.current.style.left = `${parentPos.left}px`;
920
+ windowRef.current.style.top = `${parentPos.bottom + windowHeight < window.innerHeight ? parentPos.bottom : Math.max(0, parentPos.top - windowHeight)}px`;
921
+ windowRef.current.style.transform = "";
922
+ windowPos.current = {
923
+ x: 0,
924
+ y: 0
925
+ };
850
926
  checkPosition();
851
- } else if (!visible && windowVisible) setWindowVisible(false);
852
- else if (!visible && windowInDOM) setWindowInDOM(false);
927
+ // Update z-index and make visible - use startTransition
928
+ const maxZ = $46fb0088a1bbb6d8$var$getMaxZIndex(minZIndex);
929
+ onOpen === null || onOpen === void 0 ? void 0 : onOpen();
930
+ startTransition(()=>{
931
+ setZIndex(maxZ + 1);
932
+ setWindowVisible(true);
933
+ });
934
+ }
853
935
  }, [
936
+ windowInDOM,
937
+ windowVisible,
938
+ visible,
854
939
  checkPosition,
940
+ minZIndex,
855
941
  onOpen,
856
- pushToTop,
857
- visible,
858
- windowInDOM,
859
- windowVisible
942
+ startTransition
860
943
  ]);
861
944
  // Cleanup effect to remove event listeners on unmount
862
945
  (0, $gTuX4$react.useEffect)(()=>{
@@ -952,39 +1035,6 @@ $46fb0088a1bbb6d8$export$1af8984c69ba1b24.displayName = "ContextWindow";
952
1035
 
953
1036
 
954
1037
 
955
- const $16208d559c772441$var$SESSION_KEY = "context-menu.ContextWindowStack.rendered";
956
- const $16208d559c772441$export$9f37482ccd50dad2 = ({ children: children })=>{
957
- (0, $gTuX4$react.useEffect)(()=>{
958
- const doWarn = ()=>console.warn("ContextWindowStack is deprecated and no longer required. ContextWindow now manages z-index automatically. Please remove the ContextWindowStack wrapper from your code.");
959
- try {
960
- // Prefer sessionStorage so the warning lasts for the browser session.
961
- if (typeof window !== "undefined" && window.sessionStorage) {
962
- const already = window.sessionStorage.getItem($16208d559c772441$var$SESSION_KEY);
963
- if (!already) {
964
- window.sessionStorage.setItem($16208d559c772441$var$SESSION_KEY, "1");
965
- doWarn();
966
- }
967
- return;
968
- }
969
- } catch (e) {
970
- // sessionStorage may be unavailable (privacy mode). Fall through to global fallback.
971
- }
972
- // Fallback: use a global flag for environments where sessionStorage isn't available.
973
- const g = globalThis;
974
- if (!g.__ContextWindowStackRendered) {
975
- g.__ContextWindowStackRendered = true;
976
- doWarn();
977
- }
978
- }, []);
979
- return /*#__PURE__*/ (0, $gTuX4$reactjsxruntime.jsx)((0, $gTuX4$reactjsxruntime.Fragment), {
980
- children: children
981
- });
982
- };
983
- $16208d559c772441$export$9f37482ccd50dad2.displayName = "ContextWindowStack";
984
-
985
-
986
-
987
-
988
1038
  $parcel$exportWildcard(module.exports, $a68bd8a6c0fd98c2$exports);
989
1039
 
990
1040