@hanzogui/context-menu 2.0.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.
Files changed (44) hide show
  1. package/dist/cjs/ContextMenu.cjs +182 -0
  2. package/dist/cjs/ContextMenu.native.js +191 -0
  3. package/dist/cjs/ContextMenu.native.js.map +1 -0
  4. package/dist/cjs/createNonNativeContextMenu.cjs +417 -0
  5. package/dist/cjs/createNonNativeContextMenu.native.js +443 -0
  6. package/dist/cjs/createNonNativeContextMenu.native.js.map +1 -0
  7. package/dist/cjs/index.cjs +39 -0
  8. package/dist/cjs/index.native.js +42 -0
  9. package/dist/cjs/index.native.js.map +1 -0
  10. package/dist/esm/ContextMenu.mjs +148 -0
  11. package/dist/esm/ContextMenu.mjs.map +1 -0
  12. package/dist/esm/ContextMenu.native.js +154 -0
  13. package/dist/esm/ContextMenu.native.js.map +1 -0
  14. package/dist/esm/createNonNativeContextMenu.mjs +382 -0
  15. package/dist/esm/createNonNativeContextMenu.mjs.map +1 -0
  16. package/dist/esm/createNonNativeContextMenu.native.js +405 -0
  17. package/dist/esm/createNonNativeContextMenu.native.js.map +1 -0
  18. package/dist/esm/index.js +15 -0
  19. package/dist/esm/index.js.map +1 -0
  20. package/dist/esm/index.mjs +15 -0
  21. package/dist/esm/index.mjs.map +1 -0
  22. package/dist/esm/index.native.js +15 -0
  23. package/dist/esm/index.native.js.map +1 -0
  24. package/dist/jsx/ContextMenu.mjs +148 -0
  25. package/dist/jsx/ContextMenu.mjs.map +1 -0
  26. package/dist/jsx/ContextMenu.native.js +191 -0
  27. package/dist/jsx/ContextMenu.native.js.map +1 -0
  28. package/dist/jsx/createNonNativeContextMenu.mjs +382 -0
  29. package/dist/jsx/createNonNativeContextMenu.mjs.map +1 -0
  30. package/dist/jsx/createNonNativeContextMenu.native.js +443 -0
  31. package/dist/jsx/createNonNativeContextMenu.native.js.map +1 -0
  32. package/dist/jsx/index.js +15 -0
  33. package/dist/jsx/index.js.map +1 -0
  34. package/dist/jsx/index.mjs +15 -0
  35. package/dist/jsx/index.mjs.map +1 -0
  36. package/dist/jsx/index.native.js +42 -0
  37. package/dist/jsx/index.native.js.map +1 -0
  38. package/package.json +53 -0
  39. package/src/ContextMenu.tsx +187 -0
  40. package/src/createNonNativeContextMenu.tsx +641 -0
  41. package/src/index.tsx +17 -0
  42. package/types/ContextMenu.d.ts +134 -0
  43. package/types/createNonNativeContextMenu.d.ts +180 -0
  44. package/types/index.d.ts +132 -0
@@ -0,0 +1,641 @@
1
+ import type BaseMenuTypes from '@hanzogui/create-menu'
2
+ import { createBaseMenu, type CreateBaseMenuProps } from '@hanzogui/create-menu'
3
+ import { useControllableState } from '@hanzogui/use-controllable-state'
4
+ import {
5
+ composeEventHandlers,
6
+ composeRefs,
7
+ createStyledContext,
8
+ isAndroid,
9
+ isWeb,
10
+ Slot,
11
+ type GuiElement,
12
+ View,
13
+ type ViewProps,
14
+ withStaticProperties,
15
+ } from '@hanzogui/web'
16
+ import React, { useId } from 'react'
17
+
18
+ type Direction = 'ltr' | 'rtl'
19
+ type Point = { x: number; y: number }
20
+
21
+ export const CONTEXTMENU_CONTEXT = 'ContextMenuContext'
22
+
23
+ /* -------------------------------------------------------------------------------------------------
24
+ * Types
25
+ * -----------------------------------------------------------------------------------------------*/
26
+
27
+ type ScopedProps<P> = P & { scope?: string }
28
+
29
+ type BaseMenu = ReturnType<typeof createBaseMenu>['Menu']
30
+
31
+ type ContextMenuOpenChangeEvent = {
32
+ preventDefault(): void
33
+ defaultPrevented: boolean
34
+ }
35
+
36
+ type ContextMenuContextValue = {
37
+ triggerId: string
38
+ triggerRef: React.RefObject<GuiElement | null>
39
+ contentId: string
40
+ open: boolean
41
+ onOpenChange(open: boolean, event?: ContextMenuOpenChangeEvent): void
42
+ modal: boolean
43
+ }
44
+
45
+ interface ContextMenuProps extends BaseMenuTypes.MenuProps {
46
+ children?: React.ReactNode
47
+ onOpenChange?(open: boolean, event?: ContextMenuOpenChangeEvent): void
48
+ dir?: Direction
49
+ modal?: boolean
50
+ }
51
+
52
+ interface ContextMenuTriggerProps extends ViewProps {
53
+ disabled?: boolean
54
+ }
55
+
56
+ type ContextMenuPortalProps = React.ComponentPropsWithoutRef<BaseMenu['Portal']>
57
+
58
+ type ContextMenuContentElement = React.ComponentRef<BaseMenu['Content']>
59
+ interface ContextMenuContentProps extends Omit<
60
+ React.ComponentPropsWithoutRef<BaseMenu['Content']>,
61
+ 'onEntryFocus' | 'side' | 'sideOffset' | 'align'
62
+ > {}
63
+
64
+ type ContextMenuGroupProps = React.ComponentPropsWithoutRef<BaseMenu['Group']>
65
+ type ContextMenuItemProps = React.ComponentPropsWithoutRef<BaseMenu['Item']>
66
+ type ContextMenuItemImageProps = React.ComponentPropsWithoutRef<BaseMenu['ItemImage']>
67
+ type ContextMenuItemIconProps = React.ComponentPropsWithoutRef<BaseMenu['ItemIcon']>
68
+ type ContextMenuCheckboxItemProps = React.ComponentPropsWithoutRef<
69
+ BaseMenu['CheckboxItem']
70
+ >
71
+ type ContextMenuRadioGroupElement = React.ComponentRef<BaseMenu['RadioGroup']>
72
+ type ContextMenuRadioGroupProps = React.ComponentPropsWithoutRef<BaseMenu['RadioGroup']>
73
+ type ContextMenuRadioItemProps = React.ComponentPropsWithoutRef<BaseMenu['RadioItem']>
74
+ type ContextMenuItemIndicatorProps = React.ComponentPropsWithoutRef<
75
+ BaseMenu['ItemIndicator']
76
+ >
77
+ type ContextMenuSeparatorProps = React.ComponentPropsWithoutRef<BaseMenu['Separator']>
78
+ type ContextMenuArrowProps = React.ComponentPropsWithoutRef<BaseMenu['Arrow']>
79
+
80
+ interface ContextMenuSubProps extends BaseMenuTypes.MenuSubProps {
81
+ children?: React.ReactNode
82
+ open?: boolean
83
+ defaultOpen?: boolean
84
+ onOpenChange?(open: boolean): void
85
+ }
86
+
87
+ type ContextMenuSubTriggerProps = React.ComponentPropsWithoutRef<BaseMenu['SubTrigger']>
88
+ type ContextMenuSubContentElement = React.ComponentRef<BaseMenu['Content']>
89
+ type ContextMenuSubContentProps = React.ComponentPropsWithoutRef<BaseMenu['SubContent']>
90
+
91
+ /* -----------------------------------------------------------------------------------------------*/
92
+
93
+ export function createNonNativeContextMenu(params: CreateBaseMenuProps) {
94
+ const { Menu } = createBaseMenu(params)
95
+
96
+ /* -------------------------------------------------------------------------------------------------
97
+ * ContextMenu
98
+ * -----------------------------------------------------------------------------------------------*/
99
+
100
+ const CONTEXT_MENU_NAME = 'ContextMenu'
101
+
102
+ const { Provider: ContextMenuProvider, useStyledContext: useContextMenuContext } =
103
+ createStyledContext<ContextMenuContextValue>()
104
+
105
+ const ContextMenuComp = (props: ScopedProps<ContextMenuProps>) => {
106
+ const { scope, children, onOpenChange, dir, modal = true, ...rest } = props
107
+ const [open, setOpen] = React.useState(false)
108
+ const triggerRef = React.useRef<GuiElement>(null)
109
+
110
+ const handleOpenChange = React.useCallback(
111
+ (open: boolean, event?: ContextMenuOpenChangeEvent) => {
112
+ onOpenChange?.(open, event)
113
+ if (event?.defaultPrevented) return
114
+ setOpen(open)
115
+ },
116
+ [onOpenChange]
117
+ )
118
+
119
+ return (
120
+ <ContextMenuProvider
121
+ scope={scope}
122
+ triggerId={useId()}
123
+ triggerRef={triggerRef}
124
+ contentId={useId()}
125
+ open={open}
126
+ onOpenChange={handleOpenChange}
127
+ modal={modal}
128
+ >
129
+ <Menu
130
+ scope={scope || CONTEXTMENU_CONTEXT}
131
+ dir={dir}
132
+ open={open}
133
+ onOpenChange={handleOpenChange}
134
+ modal={modal}
135
+ {...rest}
136
+ >
137
+ {children}
138
+ </Menu>
139
+ </ContextMenuProvider>
140
+ )
141
+ }
142
+
143
+ ContextMenuComp.displayName = CONTEXT_MENU_NAME
144
+
145
+ /* -------------------------------------------------------------------------------------------------
146
+ * ContextMenuTrigger - The only real difference from Menu: uses onContextMenu instead of onClick
147
+ * -----------------------------------------------------------------------------------------------*/
148
+
149
+ const TRIGGER_NAME = 'ContextMenuTrigger'
150
+
151
+ const ContextMenuTrigger = View.styleable<ScopedProps<ContextMenuTriggerProps>>(
152
+ (props, forwardedRef) => {
153
+ const { scope, style, disabled = false, asChild, children, ...triggerProps } = props
154
+ const context = useContextMenuContext(scope)
155
+ const pointRef = React.useRef<Point>({ x: 0, y: 0 })
156
+ const virtualRef = React.useMemo(
157
+ () => ({
158
+ current: {
159
+ getBoundingClientRect: () => {
160
+ if (isWeb) {
161
+ // Subtract scroll offset because floating-ui adds it back for absolute positioning
162
+ const scrollX = window.scrollX || document.documentElement.scrollLeft
163
+ const scrollY = window.scrollY || document.documentElement.scrollTop
164
+ return DOMRect.fromRect({
165
+ width: 0,
166
+ height: 0,
167
+ x: pointRef.current.x - scrollX,
168
+ y: pointRef.current.y - scrollY,
169
+ })
170
+ }
171
+ return { width: 0, height: 0, top: 0, left: 0, ...pointRef.current }
172
+ },
173
+ ...(!isWeb && {
174
+ measure: (c: any) => c(pointRef.current.x, pointRef.current.y, 0, 0),
175
+ measureInWindow: (c: any) =>
176
+ c(pointRef.current.x, pointRef.current.y, 0, 0),
177
+ }),
178
+ },
179
+ }),
180
+ [pointRef.current.x, pointRef.current.y]
181
+ )
182
+ const longPressTimerRef = React.useRef(0)
183
+ const clearLongPress = React.useCallback(
184
+ () => window.clearTimeout(longPressTimerRef.current),
185
+ []
186
+ )
187
+ const createOpenChangeEvent = (): ContextMenuOpenChangeEvent => {
188
+ let defaultPrevented = false
189
+ return {
190
+ get defaultPrevented() {
191
+ return defaultPrevented
192
+ },
193
+ preventDefault() {
194
+ defaultPrevented = true
195
+ },
196
+ }
197
+ }
198
+
199
+ const handleOpen = (event: React.MouseEvent | React.PointerEvent) => {
200
+ if (isWeb && (event instanceof MouseEvent || event instanceof PointerEvent)) {
201
+ pointRef.current = { x: event.clientX, y: event.clientY }
202
+ } else {
203
+ pointRef.current = {
204
+ x: (event as any).nativeEvent.pageX,
205
+ y: (event as any).nativeEvent.pageY,
206
+ }
207
+ }
208
+ const openChangeEvent = createOpenChangeEvent()
209
+ context.onOpenChange(true, openChangeEvent)
210
+ return openChangeEvent
211
+ }
212
+
213
+ React.useEffect(() => clearLongPress, [clearLongPress])
214
+ React.useEffect(
215
+ () => void (disabled && clearLongPress()),
216
+ [disabled, clearLongPress]
217
+ )
218
+
219
+ const Comp = asChild ? Slot : View
220
+
221
+ return (
222
+ <>
223
+ <Menu.Anchor scope={scope || CONTEXTMENU_CONTEXT} virtualRef={virtualRef} />
224
+ <Comp
225
+ render="span"
226
+ componentName={TRIGGER_NAME}
227
+ id={context.triggerId}
228
+ data-state={context.open ? 'open' : 'closed'}
229
+ data-disabled={disabled ? '' : undefined}
230
+ {...triggerProps}
231
+ ref={composeRefs(forwardedRef, context.triggerRef)}
232
+ style={isWeb ? { WebkitTouchCallout: 'none', ...(style as object) } : null}
233
+ {...(isWeb && {
234
+ onContextMenu: disabled
235
+ ? props.onContextMenu
236
+ : composeEventHandlers(props.onContextMenu, (event: any) => {
237
+ clearLongPress()
238
+ const openChangeEvent = handleOpen(event)
239
+ if (!openChangeEvent.defaultPrevented) {
240
+ event.preventDefault()
241
+ }
242
+ }),
243
+ onPointerDown: disabled
244
+ ? props.onPointerDown
245
+ : composeEventHandlers(props.onPointerDown, (event: any) => {
246
+ if (event.pointerType === 'mouse') return
247
+ clearLongPress()
248
+ longPressTimerRef.current = window.setTimeout(
249
+ () => handleOpen(event),
250
+ 700
251
+ )
252
+ }),
253
+ onPointerMove: disabled
254
+ ? props.onPointerMove
255
+ : composeEventHandlers(props.onPointerMove, (event: any) => {
256
+ if (event.pointerType === 'mouse') return
257
+ clearLongPress()
258
+ }),
259
+ onPointerCancel: disabled
260
+ ? props.onPointerCancel
261
+ : composeEventHandlers(props.onPointerCancel, (event: any) => {
262
+ if (event.pointerType === 'mouse') return
263
+ clearLongPress()
264
+ }),
265
+ onPointerUp: disabled
266
+ ? props.onPointerUp
267
+ : composeEventHandlers(props.onPointerUp, (event: any) => {
268
+ if (event.pointerType === 'mouse') return
269
+ clearLongPress()
270
+ }),
271
+ })}
272
+ {...(!isWeb && {
273
+ onLongPress: disabled
274
+ ? props.onLongPress
275
+ : composeEventHandlers(props.onLongPress, (event: any) => {
276
+ clearLongPress()
277
+ const openChangeEvent = handleOpen(event)
278
+ if (!openChangeEvent.defaultPrevented) {
279
+ event.preventDefault()
280
+ }
281
+ }),
282
+ })}
283
+ >
284
+ {children}
285
+ </Comp>
286
+ </>
287
+ )
288
+ }
289
+ )
290
+
291
+ ContextMenuTrigger.displayName = TRIGGER_NAME
292
+
293
+ /* -------------------------------------------------------------------------------------------------
294
+ * ContextMenuPortal
295
+ * -----------------------------------------------------------------------------------------------*/
296
+
297
+ const PORTAL_NAME = 'ContextMenuPortal'
298
+
299
+ const ContextMenuPortal = (props: ScopedProps<ContextMenuPortalProps>) => {
300
+ const { scope, children, ...portalProps } = props
301
+ const context = isAndroid ? useContextMenuContext(scope) : null
302
+ const content = isAndroid ? (
303
+ <ContextMenuProvider {...(context as any)}>{children}</ContextMenuProvider>
304
+ ) : (
305
+ children
306
+ )
307
+ return (
308
+ <Menu.Portal scope={scope || CONTEXTMENU_CONTEXT} {...portalProps}>
309
+ {content}
310
+ </Menu.Portal>
311
+ )
312
+ }
313
+
314
+ ContextMenuPortal.displayName = PORTAL_NAME
315
+
316
+ /* -------------------------------------------------------------------------------------------------
317
+ * ContextMenuContent
318
+ * -----------------------------------------------------------------------------------------------*/
319
+
320
+ const CONTENT_NAME = 'ContextMenuContent'
321
+
322
+ const ContextMenuContent = React.forwardRef<
323
+ ContextMenuContentElement,
324
+ ScopedProps<ContextMenuContentProps>
325
+ >((props, forwardedRef) => {
326
+ const { scope, ...contentProps } = props
327
+ const context = useContextMenuContext(scope)
328
+ const hasInteractedOutsideRef = React.useRef(false)
329
+
330
+ return (
331
+ <Menu.Content
332
+ id={context.contentId}
333
+ aria-labelledby={context.triggerId}
334
+ scope={scope || CONTEXTMENU_CONTEXT}
335
+ {...contentProps}
336
+ ref={forwardedRef}
337
+ onCloseAutoFocus={composeEventHandlers(props.onCloseAutoFocus, (event) => {
338
+ if (!hasInteractedOutsideRef.current) context.triggerRef.current?.focus()
339
+ hasInteractedOutsideRef.current = false
340
+ event.preventDefault()
341
+ })}
342
+ onInteractOutside={composeEventHandlers(props.onInteractOutside, (event) => {
343
+ const originalEvent = event.detail.originalEvent as PointerEvent
344
+ const ctrlLeftClick =
345
+ originalEvent.button === 0 && originalEvent.ctrlKey === true
346
+ const isRightClick = originalEvent.button === 2 || ctrlLeftClick
347
+ if (!context.modal || isRightClick) hasInteractedOutsideRef.current = true
348
+ })}
349
+ />
350
+ )
351
+ })
352
+
353
+ ContextMenuContent.displayName = CONTENT_NAME
354
+
355
+ /* -------------------------------------------------------------------------------------------------
356
+ * ContextMenuItem
357
+ * -----------------------------------------------------------------------------------------------*/
358
+
359
+ const ITEM_NAME = 'ContextMenuItem'
360
+
361
+ const ContextMenuItem = React.forwardRef<
362
+ GuiElement,
363
+ ScopedProps<ContextMenuItemProps>
364
+ >((props, forwardedRef) => {
365
+ const { scope, ...itemProps } = props
366
+ return (
367
+ <Menu.Item
368
+ componentName={ITEM_NAME}
369
+ scope={scope || CONTEXTMENU_CONTEXT}
370
+ {...itemProps}
371
+ ref={forwardedRef}
372
+ />
373
+ )
374
+ })
375
+
376
+ ContextMenuItem.displayName = ITEM_NAME
377
+
378
+ /* -------------------------------------------------------------------------------------------------
379
+ * ContextMenuCheckboxItem
380
+ * -----------------------------------------------------------------------------------------------*/
381
+
382
+ const CHECKBOX_ITEM_NAME = 'ContextMenuCheckboxItem'
383
+
384
+ const ContextMenuCheckboxItem = React.forwardRef<
385
+ GuiElement,
386
+ ScopedProps<ContextMenuCheckboxItemProps>
387
+ >((props, forwardedRef) => {
388
+ const { scope, ...checkboxItemProps } = props
389
+ return (
390
+ <Menu.CheckboxItem
391
+ componentName={CHECKBOX_ITEM_NAME}
392
+ scope={scope || CONTEXTMENU_CONTEXT}
393
+ {...checkboxItemProps}
394
+ ref={forwardedRef}
395
+ />
396
+ )
397
+ })
398
+
399
+ ContextMenuCheckboxItem.displayName = CHECKBOX_ITEM_NAME
400
+
401
+ /* -------------------------------------------------------------------------------------------------
402
+ * ContextMenuRadioGroup
403
+ * -----------------------------------------------------------------------------------------------*/
404
+
405
+ const RADIO_GROUP_NAME = 'ContextMenuRadioGroup'
406
+
407
+ const ContextMenuRadioGroup = React.forwardRef<
408
+ ContextMenuRadioGroupElement,
409
+ ScopedProps<ContextMenuRadioGroupProps>
410
+ >((props, forwardedRef) => {
411
+ const { scope, ...radioGroupProps } = props
412
+ return (
413
+ <Menu.RadioGroup
414
+ scope={scope || CONTEXTMENU_CONTEXT}
415
+ {...radioGroupProps}
416
+ ref={forwardedRef}
417
+ />
418
+ )
419
+ })
420
+
421
+ ContextMenuRadioGroup.displayName = RADIO_GROUP_NAME
422
+
423
+ /* -------------------------------------------------------------------------------------------------
424
+ * ContextMenuRadioItem
425
+ * -----------------------------------------------------------------------------------------------*/
426
+
427
+ const RADIO_ITEM_NAME = 'ContextMenuRadioItem'
428
+
429
+ const ContextMenuRadioItem = React.forwardRef<
430
+ GuiElement,
431
+ ScopedProps<ContextMenuRadioItemProps>
432
+ >((props, forwardedRef) => {
433
+ const { scope, ...radioItemProps } = props
434
+ return (
435
+ <Menu.RadioItem
436
+ componentName={RADIO_ITEM_NAME}
437
+ scope={scope || CONTEXTMENU_CONTEXT}
438
+ {...radioItemProps}
439
+ ref={forwardedRef}
440
+ />
441
+ )
442
+ })
443
+
444
+ ContextMenuRadioItem.displayName = RADIO_ITEM_NAME
445
+
446
+ /* -------------------------------------------------------------------------------------------------
447
+ * ContextMenuItemIndicator
448
+ * -----------------------------------------------------------------------------------------------*/
449
+
450
+ const INDICATOR_NAME = 'ContextMenuItemIndicator'
451
+
452
+ const ContextMenuItemIndicator = Menu.ItemIndicator.styleable<
453
+ ScopedProps<ContextMenuItemIndicatorProps>
454
+ >((props, forwardedRef) => {
455
+ const { scope, ...itemIndicatorProps } = props
456
+ return (
457
+ <Menu.ItemIndicator
458
+ componentName={INDICATOR_NAME}
459
+ scope={scope || CONTEXTMENU_CONTEXT}
460
+ {...itemIndicatorProps}
461
+ ref={forwardedRef}
462
+ />
463
+ )
464
+ })
465
+
466
+ ContextMenuItemIndicator.displayName = INDICATOR_NAME
467
+
468
+ /* -------------------------------------------------------------------------------------------------
469
+ * ContextMenuSub
470
+ * -----------------------------------------------------------------------------------------------*/
471
+
472
+ const SUB_NAME = 'ContextMenuSub'
473
+
474
+ const ContextMenuSub = (props: ScopedProps<ContextMenuSubProps>) => {
475
+ const { scope, children, onOpenChange, open: openProp, defaultOpen, ...rest } = props
476
+ const [open, setOpen] = useControllableState({
477
+ prop: openProp,
478
+ defaultProp: defaultOpen!,
479
+ onChange: onOpenChange,
480
+ })
481
+
482
+ return (
483
+ <Menu.Sub
484
+ scope={scope || CONTEXTMENU_CONTEXT}
485
+ open={open}
486
+ onOpenChange={setOpen}
487
+ {...rest}
488
+ >
489
+ {children}
490
+ </Menu.Sub>
491
+ )
492
+ }
493
+
494
+ ContextMenuSub.displayName = SUB_NAME
495
+
496
+ /* -------------------------------------------------------------------------------------------------
497
+ * ContextMenuSubTrigger
498
+ * -----------------------------------------------------------------------------------------------*/
499
+
500
+ const SUB_TRIGGER_NAME = 'ContextMenuSubTrigger'
501
+
502
+ const ContextMenuSubTrigger = View.styleable<ScopedProps<ContextMenuSubTriggerProps>>(
503
+ (props, forwardedRef) => {
504
+ const { scope, ...subTriggerProps } = props
505
+ return (
506
+ <Menu.SubTrigger
507
+ componentName={SUB_TRIGGER_NAME}
508
+ scope={scope || CONTEXTMENU_CONTEXT}
509
+ {...subTriggerProps}
510
+ ref={forwardedRef}
511
+ />
512
+ )
513
+ }
514
+ )
515
+
516
+ ContextMenuSubTrigger.displayName = SUB_TRIGGER_NAME
517
+
518
+ /* -------------------------------------------------------------------------------------------------
519
+ * ContextMenuSubContent
520
+ * -----------------------------------------------------------------------------------------------*/
521
+
522
+ const SUB_CONTENT_NAME = 'ContextMenuSubContent'
523
+
524
+ const ContextMenuSubContent = React.forwardRef<
525
+ ContextMenuSubContentElement,
526
+ ScopedProps<ContextMenuSubContentProps>
527
+ >((props, forwardedRef) => {
528
+ const { scope, ...subContentProps } = props
529
+ return (
530
+ <Menu.SubContent
531
+ scope={scope || CONTEXTMENU_CONTEXT}
532
+ {...subContentProps}
533
+ ref={forwardedRef}
534
+ style={
535
+ isWeb
536
+ ? {
537
+ ...(props.style as object),
538
+ ...({
539
+ '--gui-context-menu-content-transform-origin':
540
+ 'var(--gui-popper-transform-origin)',
541
+ '--gui-context-menu-content-available-width':
542
+ 'var(--gui-popper-available-width)',
543
+ '--gui-context-menu-content-available-height':
544
+ 'var(--gui-popper-available-height)',
545
+ '--gui-context-menu-trigger-width':
546
+ 'var(--gui-popper-anchor-width)',
547
+ '--gui-context-menu-trigger-height':
548
+ 'var(--gui-popper-anchor-height)',
549
+ } as React.CSSProperties),
550
+ }
551
+ : null
552
+ }
553
+ />
554
+ )
555
+ })
556
+
557
+ ContextMenuSubContent.displayName = SUB_CONTENT_NAME
558
+
559
+ /* -------------------------------------------------------------------------------------------------
560
+ * ContextMenuArrow
561
+ * -----------------------------------------------------------------------------------------------*/
562
+
563
+ const ARROW_NAME = 'ContextMenuArrow'
564
+
565
+ const ContextMenuArrow = React.forwardRef<
566
+ GuiElement,
567
+ ScopedProps<ContextMenuArrowProps>
568
+ >((props, forwardedRef) => {
569
+ const { scope, ...arrowProps } = props
570
+ return (
571
+ <Menu.Arrow
572
+ componentName={ARROW_NAME}
573
+ scope={scope || CONTEXTMENU_CONTEXT}
574
+ {...arrowProps}
575
+ ref={forwardedRef}
576
+ />
577
+ )
578
+ })
579
+
580
+ ContextMenuArrow.displayName = ARROW_NAME
581
+
582
+ /* -------------------------------------------------------------------------------------------------
583
+ * Pass-through components (same as Menu)
584
+ * -----------------------------------------------------------------------------------------------*/
585
+
586
+ const ContextMenuGroup = Menu.Group
587
+ const ContextMenuLabel = Menu.Label
588
+ const ContextMenuSeparator = Menu.Separator
589
+ const ContextMenuItemTitle = Menu.ItemTitle
590
+ const ContextMenuItemSubTitle = Menu.ItemSubtitle
591
+ const ContextMenuItemImage = Menu.ItemImage
592
+ const ContextMenuItemIcon = Menu.ItemIcon
593
+ const ContextMenuPreview = () => null // No-op on web
594
+
595
+ /* -----------------------------------------------------------------------------------------------*/
596
+
597
+ return withStaticProperties(ContextMenuComp, {
598
+ Root: ContextMenuComp,
599
+ Trigger: ContextMenuTrigger,
600
+ Portal: ContextMenuPortal,
601
+ Content: ContextMenuContent,
602
+ Group: ContextMenuGroup,
603
+ Label: ContextMenuLabel,
604
+ Item: ContextMenuItem,
605
+ CheckboxItem: ContextMenuCheckboxItem,
606
+ RadioGroup: ContextMenuRadioGroup,
607
+ RadioItem: ContextMenuRadioItem,
608
+ ItemIndicator: ContextMenuItemIndicator,
609
+ Separator: ContextMenuSeparator,
610
+ Arrow: ContextMenuArrow,
611
+ Sub: ContextMenuSub,
612
+ SubTrigger: ContextMenuSubTrigger,
613
+ SubContent: ContextMenuSubContent,
614
+ ItemTitle: ContextMenuItemTitle,
615
+ ItemSubtitle: ContextMenuItemSubTitle,
616
+ ItemIcon: ContextMenuItemIcon,
617
+ ItemImage: ContextMenuItemImage,
618
+ Preview: ContextMenuPreview,
619
+ })
620
+ }
621
+
622
+ export type {
623
+ ContextMenuArrowProps,
624
+ ContextMenuCheckboxItemProps,
625
+ ContextMenuOpenChangeEvent,
626
+ ContextMenuContentProps,
627
+ ContextMenuGroupProps,
628
+ ContextMenuItemIconProps,
629
+ ContextMenuItemImageProps,
630
+ ContextMenuItemIndicatorProps,
631
+ ContextMenuItemProps,
632
+ ContextMenuPortalProps,
633
+ ContextMenuProps,
634
+ ContextMenuRadioGroupProps,
635
+ ContextMenuRadioItemProps,
636
+ ContextMenuSeparatorProps,
637
+ ContextMenuSubContentProps,
638
+ ContextMenuSubProps,
639
+ ContextMenuSubTriggerProps,
640
+ ContextMenuTriggerProps,
641
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,17 @@
1
+ import '@hanzogui/polyfill-dev'
2
+
3
+ import { MenuPredefined } from '@hanzogui/create-menu'
4
+
5
+ import { createContextMenu } from './ContextMenu'
6
+
7
+ export const ContextMenu = createContextMenu({
8
+ Icon: MenuPredefined.MenuIcon,
9
+ Image: MenuPredefined.MenuImage,
10
+ Indicator: MenuPredefined.MenuIndicator,
11
+ Item: MenuPredefined.MenuItem,
12
+ Label: MenuPredefined.MenuLabel,
13
+ MenuGroup: MenuPredefined.MenuGroup,
14
+ Separator: MenuPredefined.MenuSeparator,
15
+ SubTitle: MenuPredefined.SubTitle,
16
+ Title: MenuPredefined.Title,
17
+ })