@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.
- package/dist/cjs/ContextMenu.cjs +182 -0
- package/dist/cjs/ContextMenu.native.js +191 -0
- package/dist/cjs/ContextMenu.native.js.map +1 -0
- package/dist/cjs/createNonNativeContextMenu.cjs +417 -0
- package/dist/cjs/createNonNativeContextMenu.native.js +443 -0
- package/dist/cjs/createNonNativeContextMenu.native.js.map +1 -0
- package/dist/cjs/index.cjs +39 -0
- package/dist/cjs/index.native.js +42 -0
- package/dist/cjs/index.native.js.map +1 -0
- package/dist/esm/ContextMenu.mjs +148 -0
- package/dist/esm/ContextMenu.mjs.map +1 -0
- package/dist/esm/ContextMenu.native.js +154 -0
- package/dist/esm/ContextMenu.native.js.map +1 -0
- package/dist/esm/createNonNativeContextMenu.mjs +382 -0
- package/dist/esm/createNonNativeContextMenu.mjs.map +1 -0
- package/dist/esm/createNonNativeContextMenu.native.js +405 -0
- package/dist/esm/createNonNativeContextMenu.native.js.map +1 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/index.mjs +15 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/index.native.js +15 -0
- package/dist/esm/index.native.js.map +1 -0
- package/dist/jsx/ContextMenu.mjs +148 -0
- package/dist/jsx/ContextMenu.mjs.map +1 -0
- package/dist/jsx/ContextMenu.native.js +191 -0
- package/dist/jsx/ContextMenu.native.js.map +1 -0
- package/dist/jsx/createNonNativeContextMenu.mjs +382 -0
- package/dist/jsx/createNonNativeContextMenu.mjs.map +1 -0
- package/dist/jsx/createNonNativeContextMenu.native.js +443 -0
- package/dist/jsx/createNonNativeContextMenu.native.js.map +1 -0
- package/dist/jsx/index.js +15 -0
- package/dist/jsx/index.js.map +1 -0
- package/dist/jsx/index.mjs +15 -0
- package/dist/jsx/index.mjs.map +1 -0
- package/dist/jsx/index.native.js +42 -0
- package/dist/jsx/index.native.js.map +1 -0
- package/package.json +53 -0
- package/src/ContextMenu.tsx +187 -0
- package/src/createNonNativeContextMenu.tsx +641 -0
- package/src/index.tsx +17 -0
- package/types/ContextMenu.d.ts +134 -0
- package/types/createNonNativeContextMenu.d.ts +180 -0
- 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
|
+
})
|