@octanejs/radix 0.1.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/package.json +45 -0
  4. package/src/AccessibleIcon.ts +26 -0
  5. package/src/Accordion.ts +282 -0
  6. package/src/AlertDialog.ts +110 -0
  7. package/src/Arrow.ts +21 -0
  8. package/src/AspectRatio.ts +34 -0
  9. package/src/Avatar.ts +152 -0
  10. package/src/Checkbox.ts +325 -0
  11. package/src/Collapsible.ts +170 -0
  12. package/src/ContextMenu.ts +303 -0
  13. package/src/Dialog.ts +308 -0
  14. package/src/DismissableLayer.ts +413 -0
  15. package/src/DropdownMenu.ts +275 -0
  16. package/src/FocusScope.ts +266 -0
  17. package/src/Form.ts +621 -0
  18. package/src/HoverCard.ts +318 -0
  19. package/src/Label.ts +25 -0
  20. package/src/Menu.ts +1057 -0
  21. package/src/Menubar.ts +572 -0
  22. package/src/NavigationMenu.ts +1236 -0
  23. package/src/OneTimePasswordField.ts +977 -0
  24. package/src/PasswordToggleField.ts +495 -0
  25. package/src/Popover.ts +354 -0
  26. package/src/Popper.ts +401 -0
  27. package/src/Portal.ts +19 -0
  28. package/src/Presence.ts +225 -0
  29. package/src/Primitive.ts +53 -0
  30. package/src/Progress.ts +92 -0
  31. package/src/Radio.ts +262 -0
  32. package/src/RadioGroup.ts +212 -0
  33. package/src/RovingFocusGroup.ts +259 -0
  34. package/src/ScrollArea.ts +966 -0
  35. package/src/Select.ts +1901 -0
  36. package/src/Separator.ts +36 -0
  37. package/src/Slider.ts +745 -0
  38. package/src/Slot.ts +105 -0
  39. package/src/Switch.ts +278 -0
  40. package/src/Tabs.ts +177 -0
  41. package/src/Toast.ts +923 -0
  42. package/src/Toggle.ts +31 -0
  43. package/src/ToggleGroup.ts +157 -0
  44. package/src/Toolbar.ts +130 -0
  45. package/src/Tooltip.ts +669 -0
  46. package/src/VisuallyHidden.ts +29 -0
  47. package/src/collection.ts +97 -0
  48. package/src/compose-event-handlers.ts +15 -0
  49. package/src/compose-refs.ts +53 -0
  50. package/src/context.ts +109 -0
  51. package/src/direction.ts +19 -0
  52. package/src/focus-guards.ts +56 -0
  53. package/src/index.ts +54 -0
  54. package/src/internal.ts +42 -0
  55. package/src/scroll-lock.ts +57 -0
  56. package/src/use-callback-ref.ts +28 -0
  57. package/src/use-effect-event.ts +36 -0
  58. package/src/use-is-hydrated.ts +23 -0
  59. package/src/use-previous.ts +28 -0
  60. package/src/use-size.ts +57 -0
  61. package/src/useControllableState.ts +78 -0
  62. package/src/useId.ts +15 -0
@@ -0,0 +1,413 @@
1
+ // Ported from @radix-ui/react-dismissable-layer. A layer that can be dismissed by Escape,
2
+ // pointer-down outside, or focus outside — with a module-level layer stack so nested layers
3
+ // dismiss top-down, optional body pointer-events disabling (modal), branches (outside
4
+ // elements that shouldn't dismiss), and dismissable "surfaces" (e.g. a dialog overlay —
5
+ // pressing one dismisses even if it stops propagation). React's context-with-default (no
6
+ // Provider is ever rendered for it in Radix) → a module-level singleton; `useCallbackRef`/
7
+ // `useEffectEvent` → octane `useEffectEvent`; discrete custom-event dispatch → flushSync.
8
+ import { createElement, useEffect, useEffectEvent, useRef, useState } from 'octane';
9
+
10
+ import { composeEventHandlers } from './compose-event-handlers';
11
+ import { useComposedRefs } from './compose-refs';
12
+ import { S, subSlot } from './internal';
13
+ import { dispatchDiscreteCustomEvent, Primitive } from './Primitive';
14
+
15
+ const CONTEXT_UPDATE = 'dismissableLayer.update';
16
+ const POINTER_DOWN_OUTSIDE = 'dismissableLayer.pointerDownOutside';
17
+ const FOCUS_OUTSIDE = 'dismissableLayer.focusOutside';
18
+
19
+ let originalBodyPointerEvents: string;
20
+
21
+ // Radix models this as React context but never renders a Provider — every consumer sees
22
+ // the module-level default. Port it as a plain singleton.
23
+ const layerContext = {
24
+ layers: new Set<HTMLElement>(),
25
+ layersWithOutsidePointerEventsDisabled: new Set<HTMLElement>(),
26
+ branches: new Set<HTMLElement>(),
27
+ // Outside elements that belong to a layer's own dismiss affordance (eg, a dialog
28
+ // overlay). Pressing them should dismiss the layer regardless of whether or not they
29
+ // stop propagation.
30
+ dismissableSurfaces: new Set<HTMLElement>(),
31
+ };
32
+
33
+ export function DismissableLayer(props: any): any {
34
+ const slot = S('DismissableLayer');
35
+ const {
36
+ disableOutsidePointerEvents = false,
37
+ deferPointerDownOutside = false,
38
+ onEscapeKeyDown,
39
+ onPointerDownOutside,
40
+ onFocusOutside,
41
+ onInteractOutside,
42
+ onDismiss,
43
+ ref: forwardedRef,
44
+ ...layerProps
45
+ } = props ?? {};
46
+ const context = layerContext;
47
+ const [node, setNode] = useState<HTMLElement | null>(null, subSlot(slot, 'node'));
48
+ const ownerDocument = node?.ownerDocument ?? globalThis?.document;
49
+ const [, force] = useState({}, subSlot(slot, 'force'));
50
+ const composedRefs = useComposedRefs(forwardedRef, setNode, subSlot(slot, 'refs'));
51
+
52
+ const layers = Array.from(context.layers);
53
+ const [highestLayerWithOutsidePointerEventsDisabled] = [
54
+ ...context.layersWithOutsidePointerEventsDisabled,
55
+ ].slice(-1);
56
+ const highestLayerWithOutsidePointerEventsDisabledIndex = layers.indexOf(
57
+ highestLayerWithOutsidePointerEventsDisabled,
58
+ );
59
+ const index = node ? layers.indexOf(node) : -1;
60
+ const isBodyPointerEventsDisabled = context.layersWithOutsidePointerEventsDisabled.size > 0;
61
+ const isPointerEventsEnabled = index >= highestLayerWithOutsidePointerEventsDisabledIndex;
62
+
63
+ const isDeferredPointerDownOutsideRef = useRef(false, subSlot(slot, 'deferred'));
64
+ const pointerDownOutside = usePointerDownOutside(
65
+ (event: any) => {
66
+ const target = event.target;
67
+ if (!(target instanceof Node)) return;
68
+ const isPointerDownOnBranch = [...context.branches].some((branch) => branch.contains(target));
69
+ if (!isPointerEventsEnabled || isPointerDownOnBranch) return;
70
+ onPointerDownOutside?.(event);
71
+ onInteractOutside?.(event);
72
+ if (!event.defaultPrevented) onDismiss?.();
73
+ },
74
+ {
75
+ ownerDocument,
76
+ deferPointerDownOutside,
77
+ isDeferredPointerDownOutsideRef,
78
+ dismissableSurfaces: context.dismissableSurfaces,
79
+ },
80
+ subSlot(slot, 'pdo'),
81
+ );
82
+
83
+ const focusOutside = useFocusOutside(
84
+ (event: any) => {
85
+ if (deferPointerDownOutside && isDeferredPointerDownOutsideRef.current) return;
86
+ const target = event.target;
87
+ const isFocusInBranch = [...context.branches].some((branch) => branch.contains(target));
88
+ if (isFocusInBranch) return;
89
+ onFocusOutside?.(event);
90
+ onInteractOutside?.(event);
91
+ if (!event.defaultPrevented) onDismiss?.();
92
+ },
93
+ ownerDocument,
94
+ subSlot(slot, 'fo'),
95
+ );
96
+
97
+ const isHighestLayer = node ? index === layers.length - 1 : false;
98
+ const handleKeyDown = useEffectEvent(
99
+ (event: KeyboardEvent) => {
100
+ if (event.key !== 'Escape') return;
101
+ onEscapeKeyDown?.(event);
102
+ if (!event.defaultPrevented && onDismiss) {
103
+ event.preventDefault();
104
+ onDismiss();
105
+ }
106
+ },
107
+ subSlot(slot, 'esc'),
108
+ );
109
+
110
+ useEffect(
111
+ () => {
112
+ if (!isHighestLayer) return;
113
+ ownerDocument.addEventListener('keydown', handleKeyDown, { capture: true });
114
+ return () => ownerDocument.removeEventListener('keydown', handleKeyDown, { capture: true });
115
+ },
116
+ [ownerDocument, isHighestLayer],
117
+ subSlot(slot, 'e:esc'),
118
+ );
119
+
120
+ useEffect(
121
+ () => {
122
+ if (!node) return;
123
+ if (disableOutsidePointerEvents) {
124
+ if (context.layersWithOutsidePointerEventsDisabled.size === 0) {
125
+ originalBodyPointerEvents = ownerDocument.body.style.pointerEvents;
126
+ ownerDocument.body.style.pointerEvents = 'none';
127
+ }
128
+ context.layersWithOutsidePointerEventsDisabled.add(node);
129
+ }
130
+ context.layers.add(node);
131
+ dispatchUpdate();
132
+ return () => {
133
+ if (disableOutsidePointerEvents) {
134
+ context.layersWithOutsidePointerEventsDisabled.delete(node);
135
+ if (context.layersWithOutsidePointerEventsDisabled.size === 0) {
136
+ ownerDocument.body.style.pointerEvents = originalBodyPointerEvents;
137
+ }
138
+ }
139
+ };
140
+ },
141
+ [node, ownerDocument, disableOutsidePointerEvents],
142
+ subSlot(slot, 'e:layer'),
143
+ );
144
+
145
+ useEffect(
146
+ () => {
147
+ return () => {
148
+ if (!node) return;
149
+ context.layers.delete(node);
150
+ context.layersWithOutsidePointerEventsDisabled.delete(node);
151
+ dispatchUpdate();
152
+ };
153
+ },
154
+ [node],
155
+ subSlot(slot, 'e:cleanup'),
156
+ );
157
+
158
+ useEffect(
159
+ () => {
160
+ const handleUpdate = (): void => force({});
161
+ document.addEventListener(CONTEXT_UPDATE, handleUpdate);
162
+ return () => document.removeEventListener(CONTEXT_UPDATE, handleUpdate);
163
+ },
164
+ [],
165
+ subSlot(slot, 'e:update'),
166
+ );
167
+
168
+ return createElement(Primitive.div, {
169
+ ...layerProps,
170
+ ref: composedRefs,
171
+ style: {
172
+ pointerEvents: isBodyPointerEventsDisabled
173
+ ? isPointerEventsEnabled
174
+ ? 'auto'
175
+ : 'none'
176
+ : undefined,
177
+ ...props.style,
178
+ },
179
+ onFocusCapture: composeEventHandlers(props.onFocusCapture, focusOutside.onFocusCapture),
180
+ onBlurCapture: composeEventHandlers(props.onBlurCapture, focusOutside.onBlurCapture),
181
+ onPointerDownCapture: composeEventHandlers(
182
+ props.onPointerDownCapture,
183
+ pointerDownOutside.onPointerDownCapture,
184
+ ),
185
+ });
186
+ }
187
+
188
+ export function DismissableLayerBranch(props: any): any {
189
+ const slot = S('DismissableLayerBranch');
190
+ const { ref: forwardedRef, ...branchProps } = props ?? {};
191
+ const ref = useRef<HTMLElement | null>(null, subSlot(slot, 'ref'));
192
+ const composedRefs = useComposedRefs(forwardedRef, ref, subSlot(slot, 'refs'));
193
+ useEffect(
194
+ () => {
195
+ const node = ref.current;
196
+ if (node) {
197
+ layerContext.branches.add(node);
198
+ return () => {
199
+ layerContext.branches.delete(node);
200
+ };
201
+ }
202
+ },
203
+ [],
204
+ subSlot(slot, 'e:branch'),
205
+ );
206
+ return createElement(Primitive.div, { ...branchProps, ref: composedRefs });
207
+ }
208
+
209
+ /** Register an element as a layer's own dismiss affordance (e.g. a dialog overlay). */
210
+ export function useDismissableLayerSurface(...args: any[]): (node: HTMLElement | null) => void {
211
+ const slotArg = args[args.length - 1];
212
+ const slot = typeof slotArg === 'symbol' ? slotArg : S('useDismissableLayerSurface');
213
+ const [node, setNode] = useState<HTMLElement | null>(null, subSlot(slot, 'node'));
214
+ useEffect(
215
+ () => {
216
+ if (!node) return;
217
+ layerContext.dismissableSurfaces.add(node);
218
+ return () => {
219
+ layerContext.dismissableSurfaces.delete(node);
220
+ };
221
+ },
222
+ [node],
223
+ subSlot(slot, 'e:surface'),
224
+ );
225
+ return setNode;
226
+ }
227
+
228
+ /**
229
+ * Listens for `pointerdown` outside the layer subtree. Radix's semantics preserved:
230
+ * a capture-phase flag distinguishes inside-the-component-tree pointerdowns (portals
231
+ * included, since octane events bubble the LOGICAL tree) from true outside presses, and
232
+ * `deferPointerDownOutside` waits for the paired `click` so an outside handler that
233
+ * `stopPropagation()`s (an "intercepted" interaction) doesn't dismiss.
234
+ */
235
+ function usePointerDownOutside(
236
+ onPointerDownOutside: (event: any) => void,
237
+ args: {
238
+ ownerDocument: Document;
239
+ deferPointerDownOutside: boolean;
240
+ isDeferredPointerDownOutsideRef: { current: boolean };
241
+ dismissableSurfaces: Set<HTMLElement>;
242
+ },
243
+ slot: symbol | undefined,
244
+ ): { onPointerDownCapture: () => void } {
245
+ const {
246
+ ownerDocument,
247
+ deferPointerDownOutside,
248
+ isDeferredPointerDownOutsideRef,
249
+ dismissableSurfaces,
250
+ } = args;
251
+ const handlePointerDownOutside = useEffectEvent(onPointerDownOutside, subSlot(slot, 'cb'));
252
+ const isPointerInsideReactTreeRef = useRef(false, subSlot(slot, 'inside'));
253
+ const isPointerDownOutsideRef = useRef(false, subSlot(slot, 'outside'));
254
+ const interceptedRef = useRef(new Map<string, boolean>(), subSlot(slot, 'intercepted'));
255
+ const handleClickRef = useRef<() => void>(() => {}, subSlot(slot, 'click'));
256
+
257
+ useEffect(
258
+ () => {
259
+ function resetOutsideInteraction(): void {
260
+ isPointerDownOutsideRef.current = false;
261
+ isDeferredPointerDownOutsideRef.current = false;
262
+ interceptedRef.current.clear();
263
+ }
264
+ function isOutsideInteractionIntercepted(): boolean {
265
+ return Array.from(interceptedRef.current.values()).some(Boolean);
266
+ }
267
+ function handleInteractionCapture(event: Event): void {
268
+ if (!isPointerDownOutsideRef.current) return;
269
+ const target = event.target;
270
+ const isDismissableSurface =
271
+ target instanceof Node &&
272
+ [...dismissableSurfaces].some((surface) => surface.contains(target));
273
+ if (!isDismissableSurface) {
274
+ interceptedRef.current.set(event.type, true);
275
+ }
276
+ if (event.type === 'click') {
277
+ window.setTimeout(() => {
278
+ if (isPointerDownOutsideRef.current) {
279
+ handleClickRef.current();
280
+ }
281
+ }, 0);
282
+ }
283
+ }
284
+ function handleInteractionBubble(event: Event): void {
285
+ if (isPointerDownOutsideRef.current) {
286
+ interceptedRef.current.set(event.type, false);
287
+ }
288
+ }
289
+ const handlePointerDown = (event: PointerEvent): void => {
290
+ if (event.target && !isPointerInsideReactTreeRef.current) {
291
+ const eventDetail = { originalEvent: event };
292
+ const handleAndDispatchPointerDownOutsideEvent = (): void => {
293
+ ownerDocument.removeEventListener('click', handleClickRef.current);
294
+ const wasIntercepted = isOutsideInteractionIntercepted();
295
+ resetOutsideInteraction();
296
+ if (!wasIntercepted) {
297
+ handleAndDispatchCustomEvent(
298
+ POINTER_DOWN_OUTSIDE,
299
+ handlePointerDownOutside,
300
+ eventDetail,
301
+ { discrete: true },
302
+ );
303
+ }
304
+ };
305
+ isPointerDownOutsideRef.current = true;
306
+ isDeferredPointerDownOutsideRef.current = deferPointerDownOutside && event.button === 0;
307
+ interceptedRef.current.clear();
308
+ if (!deferPointerDownOutside || event.button !== 0) {
309
+ handleAndDispatchPointerDownOutsideEvent();
310
+ } else {
311
+ ownerDocument.removeEventListener('click', handleClickRef.current);
312
+ handleClickRef.current = handleAndDispatchPointerDownOutsideEvent;
313
+ ownerDocument.addEventListener('click', handleClickRef.current, { once: true });
314
+ }
315
+ } else {
316
+ ownerDocument.removeEventListener('click', handleClickRef.current);
317
+ resetOutsideInteraction();
318
+ }
319
+ isPointerInsideReactTreeRef.current = false;
320
+ };
321
+ const outsideInteractionEvents = [
322
+ 'pointerup',
323
+ 'mousedown',
324
+ 'mouseup',
325
+ 'touchstart',
326
+ 'touchend',
327
+ 'click',
328
+ ];
329
+ for (const eventName of outsideInteractionEvents) {
330
+ ownerDocument.addEventListener(eventName, handleInteractionCapture, true);
331
+ ownerDocument.addEventListener(eventName, handleInteractionBubble);
332
+ }
333
+ // Avoid the open-click itself registering as "outside" (it happened before mount).
334
+ const timerId = window.setTimeout(() => {
335
+ ownerDocument.addEventListener('pointerdown', handlePointerDown as any);
336
+ }, 0);
337
+ return () => {
338
+ window.clearTimeout(timerId);
339
+ ownerDocument.removeEventListener('pointerdown', handlePointerDown as any);
340
+ ownerDocument.removeEventListener('click', handleClickRef.current);
341
+ for (const eventName of outsideInteractionEvents) {
342
+ ownerDocument.removeEventListener(eventName, handleInteractionCapture, true);
343
+ ownerDocument.removeEventListener(eventName, handleInteractionBubble);
344
+ }
345
+ };
346
+ },
347
+ [ownerDocument, deferPointerDownOutside],
348
+ subSlot(slot, 'e:pdo'),
349
+ );
350
+
351
+ return {
352
+ // ensures we check the COMPONENT tree (not just DOM tree) — capture on the layer.
353
+ onPointerDownCapture: () => {
354
+ isPointerInsideReactTreeRef.current = true;
355
+ },
356
+ };
357
+ }
358
+
359
+ /** Listens for `focusin` outside the layer subtree. */
360
+ function useFocusOutside(
361
+ onFocusOutside: (event: any) => void,
362
+ ownerDocument: Document,
363
+ slot: symbol | undefined,
364
+ ): { onFocusCapture: () => void; onBlurCapture: () => void } {
365
+ const handleFocusOutside = useEffectEvent(onFocusOutside, subSlot(slot, 'cb'));
366
+ const isFocusInsideReactTreeRef = useRef(false, subSlot(slot, 'inside'));
367
+ useEffect(
368
+ () => {
369
+ const handleFocus = (event: FocusEvent): void => {
370
+ if (event.target && !isFocusInsideReactTreeRef.current) {
371
+ const eventDetail = { originalEvent: event };
372
+ handleAndDispatchCustomEvent(FOCUS_OUTSIDE, handleFocusOutside, eventDetail, {
373
+ discrete: false,
374
+ });
375
+ }
376
+ };
377
+ ownerDocument.addEventListener('focusin', handleFocus);
378
+ return () => ownerDocument.removeEventListener('focusin', handleFocus);
379
+ },
380
+ [ownerDocument],
381
+ subSlot(slot, 'e:fo'),
382
+ );
383
+ return {
384
+ onFocusCapture: () => {
385
+ isFocusInsideReactTreeRef.current = true;
386
+ },
387
+ onBlurCapture: () => {
388
+ isFocusInsideReactTreeRef.current = false;
389
+ },
390
+ };
391
+ }
392
+
393
+ function dispatchUpdate(): void {
394
+ document.dispatchEvent(new CustomEvent(CONTEXT_UPDATE));
395
+ }
396
+
397
+ function handleAndDispatchCustomEvent(
398
+ name: string,
399
+ handler: ((event: any) => void) | undefined,
400
+ detail: { originalEvent: Event },
401
+ { discrete }: { discrete: boolean },
402
+ ): void {
403
+ const target = detail.originalEvent.target as EventTarget;
404
+ const event = new CustomEvent(name, { bubbles: false, cancelable: true, detail });
405
+ if (handler) target.addEventListener(name, handler as any, { once: true });
406
+ if (discrete) {
407
+ dispatchDiscreteCustomEvent(target, event);
408
+ } else {
409
+ target.dispatchEvent(event);
410
+ }
411
+ }
412
+
413
+ export { DismissableLayer as Root, DismissableLayerBranch as Branch };
@@ -0,0 +1,275 @@
1
+ // Ported from @radix-ui/react-dropdown-menu (source:
2
+ // .radix-primitives/packages/react/dropdown-menu/src/dropdown-menu.tsx). A thin,
3
+ // button-triggered wrapper over the shared Menu primitive: Trigger toggles on
4
+ // pointer-down (left button, no ctrl) / Enter / Space / ArrowDown, Content re-namespaces
5
+ // the popper CSS vars and returns focus to the trigger on close (unless the close came
6
+ // from an outside interaction); everything else forwards straight into Menu.
7
+ import { createElement, useCallback, useRef } from 'octane';
8
+
9
+ import { composeEventHandlers } from './compose-event-handlers';
10
+ import { useComposedRefs } from './compose-refs';
11
+ import { createContextScope } from './context';
12
+ import { S, subSlot } from './internal';
13
+ import * as MenuPrimitive from './Menu';
14
+ import { createMenuScope } from './Menu';
15
+ import { Primitive } from './Primitive';
16
+ import { useControllableState } from './useControllableState';
17
+ import { useId } from './useId';
18
+
19
+ const DROPDOWN_MENU_NAME = 'DropdownMenu';
20
+
21
+ const [createDropdownMenuContext, createDropdownMenuScope] = createContextScope(
22
+ DROPDOWN_MENU_NAME,
23
+ [createMenuScope],
24
+ );
25
+ export { createDropdownMenuScope };
26
+ const useMenuScope = createMenuScope();
27
+
28
+ interface DropdownMenuContextValue {
29
+ triggerId: string;
30
+ triggerRef: { current: HTMLElement | null };
31
+ contentId: string;
32
+ open: boolean;
33
+ onOpenChange(open: boolean): void;
34
+ onOpenToggle(): void;
35
+ modal: boolean;
36
+ }
37
+
38
+ const [DropdownMenuProvider, useDropdownMenuContext] =
39
+ createDropdownMenuContext<DropdownMenuContextValue>(DROPDOWN_MENU_NAME);
40
+
41
+ export function Root(props: any): any {
42
+ const slot = S('DropdownMenu.Root');
43
+ const {
44
+ __scopeDropdownMenu,
45
+ children,
46
+ dir,
47
+ open: openProp,
48
+ defaultOpen,
49
+ onOpenChange,
50
+ modal = true,
51
+ } = props ?? {};
52
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
53
+ const triggerRef = useRef<HTMLElement | null>(null, subSlot(slot, 'trigger'));
54
+ const [open, setOpen] = useControllableState<boolean>(
55
+ { prop: openProp, defaultProp: defaultOpen ?? false, onChange: onOpenChange },
56
+ subSlot(slot, 'open'),
57
+ );
58
+
59
+ return createElement(DropdownMenuProvider, {
60
+ scope: __scopeDropdownMenu,
61
+ triggerId: useId(subSlot(slot, 'triggerId')),
62
+ triggerRef,
63
+ contentId: useId(subSlot(slot, 'contentId')),
64
+ open,
65
+ onOpenChange: setOpen,
66
+ onOpenToggle: useCallback(
67
+ () => setOpen((prevOpen) => !prevOpen),
68
+ [setOpen],
69
+ subSlot(slot, 'toggle'),
70
+ ),
71
+ modal,
72
+ children: createElement(MenuPrimitive.Root, {
73
+ ...menuScope,
74
+ open,
75
+ onOpenChange: setOpen,
76
+ dir,
77
+ modal,
78
+ children,
79
+ }),
80
+ });
81
+ }
82
+
83
+ export function Trigger(props: any): any {
84
+ const slot = S('DropdownMenu.Trigger');
85
+ const { __scopeDropdownMenu, disabled = false, ref: forwardedRef, ...triggerProps } = props ?? {};
86
+ const context = useDropdownMenuContext('DropdownMenuTrigger', __scopeDropdownMenu);
87
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
88
+ const composedRefs = useComposedRefs(forwardedRef, context.triggerRef, subSlot(slot, 'refs'));
89
+ return createElement(MenuPrimitive.Anchor, {
90
+ asChild: true,
91
+ ...menuScope,
92
+ children: createElement(Primitive.button, {
93
+ type: 'button',
94
+ id: context.triggerId,
95
+ 'aria-haspopup': 'menu',
96
+ 'aria-expanded': context.open,
97
+ 'aria-controls': context.open ? context.contentId : undefined,
98
+ 'data-state': context.open ? 'open' : 'closed',
99
+ 'data-disabled': disabled ? '' : undefined,
100
+ disabled,
101
+ ...triggerProps,
102
+ ref: composedRefs,
103
+ onPointerDown: composeEventHandlers(props?.onPointerDown, (event: PointerEvent) => {
104
+ // only call handler if it's the left button (mousedown gets triggered by all mouse
105
+ // buttons) but not when the control key is pressed (avoiding MacOS right click)
106
+ if (!disabled && event.button === 0 && event.ctrlKey === false) {
107
+ context.onOpenToggle();
108
+ // prevent trigger focusing when opening
109
+ // this allows the content to be given focus without competition
110
+ if (!context.open) event.preventDefault();
111
+ }
112
+ }),
113
+ onKeyDown: composeEventHandlers(props?.onKeyDown, (event: KeyboardEvent) => {
114
+ if (disabled) return;
115
+ if (['Enter', ' '].includes(event.key)) context.onOpenToggle();
116
+ if (event.key === 'ArrowDown') context.onOpenChange(true);
117
+ // prevent keydown from scrolling window / first focused item to execute
118
+ // that keydown (inadvertently closing the menu)
119
+ if (['Enter', ' ', 'ArrowDown'].includes(event.key)) event.preventDefault();
120
+ }),
121
+ }),
122
+ });
123
+ }
124
+
125
+ export function Portal(props: any): any {
126
+ const slot = S('DropdownMenu.Portal');
127
+ const { __scopeDropdownMenu, ...portalProps } = props ?? {};
128
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
129
+ return createElement(MenuPrimitive.Portal, { ...menuScope, ...portalProps });
130
+ }
131
+
132
+ export function Content(props: any): any {
133
+ const slot = S('DropdownMenu.Content');
134
+ const { __scopeDropdownMenu, ...contentProps } = props ?? {};
135
+ const context = useDropdownMenuContext('DropdownMenuContent', __scopeDropdownMenu);
136
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
137
+ const hasInteractedOutsideRef = useRef(false, subSlot(slot, 'interacted'));
138
+
139
+ return createElement(MenuPrimitive.Content, {
140
+ id: context.contentId,
141
+ 'aria-labelledby': context.triggerId,
142
+ ...menuScope,
143
+ ...contentProps,
144
+ onCloseAutoFocus: composeEventHandlers(props?.onCloseAutoFocus, (event: Event) => {
145
+ if (!hasInteractedOutsideRef.current) context.triggerRef.current?.focus();
146
+ hasInteractedOutsideRef.current = false;
147
+ // Always prevent auto focus because we either focus manually or want user agent focus
148
+ event.preventDefault();
149
+ }),
150
+ onInteractOutside: composeEventHandlers(props?.onInteractOutside, (event: any) => {
151
+ const originalEvent = event.detail.originalEvent as PointerEvent;
152
+ const ctrlLeftClick = originalEvent.button === 0 && originalEvent.ctrlKey === true;
153
+ const isRightClick = originalEvent.button === 2 || ctrlLeftClick;
154
+ if (!context.modal || isRightClick) hasInteractedOutsideRef.current = true;
155
+ }),
156
+ style: {
157
+ ...props?.style,
158
+ // re-namespace exposed content custom properties
159
+ '--radix-dropdown-menu-content-transform-origin': 'var(--radix-popper-transform-origin)',
160
+ '--radix-dropdown-menu-content-available-width': 'var(--radix-popper-available-width)',
161
+ '--radix-dropdown-menu-content-available-height': 'var(--radix-popper-available-height)',
162
+ '--radix-dropdown-menu-trigger-width': 'var(--radix-popper-anchor-width)',
163
+ '--radix-dropdown-menu-trigger-height': 'var(--radix-popper-anchor-height)',
164
+ },
165
+ });
166
+ }
167
+
168
+ export function Group(props: any): any {
169
+ const slot = S('DropdownMenu.Group');
170
+ const { __scopeDropdownMenu, ...groupProps } = props ?? {};
171
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
172
+ return createElement(MenuPrimitive.Group, { ...menuScope, ...groupProps });
173
+ }
174
+
175
+ export function Label(props: any): any {
176
+ const slot = S('DropdownMenu.Label');
177
+ const { __scopeDropdownMenu, ...labelProps } = props ?? {};
178
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
179
+ return createElement(MenuPrimitive.Label, { ...menuScope, ...labelProps });
180
+ }
181
+
182
+ export function Item(props: any): any {
183
+ const slot = S('DropdownMenu.Item');
184
+ const { __scopeDropdownMenu, ...itemProps } = props ?? {};
185
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
186
+ return createElement(MenuPrimitive.Item, { ...menuScope, ...itemProps });
187
+ }
188
+
189
+ export function CheckboxItem(props: any): any {
190
+ const slot = S('DropdownMenu.CheckboxItem');
191
+ const { __scopeDropdownMenu, ...checkboxItemProps } = props ?? {};
192
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
193
+ return createElement(MenuPrimitive.CheckboxItem, { ...menuScope, ...checkboxItemProps });
194
+ }
195
+
196
+ export function RadioGroup(props: any): any {
197
+ const slot = S('DropdownMenu.RadioGroup');
198
+ const { __scopeDropdownMenu, ...radioGroupProps } = props ?? {};
199
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
200
+ return createElement(MenuPrimitive.RadioGroup, { ...menuScope, ...radioGroupProps });
201
+ }
202
+
203
+ export function RadioItem(props: any): any {
204
+ const slot = S('DropdownMenu.RadioItem');
205
+ const { __scopeDropdownMenu, ...radioItemProps } = props ?? {};
206
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
207
+ return createElement(MenuPrimitive.RadioItem, { ...menuScope, ...radioItemProps });
208
+ }
209
+
210
+ export function ItemIndicator(props: any): any {
211
+ const slot = S('DropdownMenu.ItemIndicator');
212
+ const { __scopeDropdownMenu, ...itemIndicatorProps } = props ?? {};
213
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
214
+ return createElement(MenuPrimitive.ItemIndicator, { ...menuScope, ...itemIndicatorProps });
215
+ }
216
+
217
+ export function Separator(props: any): any {
218
+ const slot = S('DropdownMenu.Separator');
219
+ const { __scopeDropdownMenu, ...separatorProps } = props ?? {};
220
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
221
+ return createElement(MenuPrimitive.Separator, { ...menuScope, ...separatorProps });
222
+ }
223
+
224
+ export function Arrow(props: any): any {
225
+ const slot = S('DropdownMenu.Arrow');
226
+ const { __scopeDropdownMenu, ...arrowProps } = props ?? {};
227
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
228
+ return createElement(MenuPrimitive.Arrow, { ...menuScope, ...arrowProps });
229
+ }
230
+
231
+ export function Sub(props: any): any {
232
+ const slot = S('DropdownMenu.Sub');
233
+ const { __scopeDropdownMenu, children, open: openProp, onOpenChange, defaultOpen } = props ?? {};
234
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
235
+ const [open, setOpen] = useControllableState<boolean>(
236
+ { prop: openProp, defaultProp: defaultOpen ?? false, onChange: onOpenChange },
237
+ subSlot(slot, 'open'),
238
+ );
239
+
240
+ return createElement(MenuPrimitive.Sub, { ...menuScope, open, onOpenChange: setOpen, children });
241
+ }
242
+
243
+ export function SubTrigger(props: any): any {
244
+ const slot = S('DropdownMenu.SubTrigger');
245
+ const { __scopeDropdownMenu, ...subTriggerProps } = props ?? {};
246
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
247
+ return createElement(MenuPrimitive.SubTrigger, { ...menuScope, ...subTriggerProps });
248
+ }
249
+
250
+ export function SubContent(props: any): any {
251
+ const slot = S('DropdownMenu.SubContent');
252
+ const { __scopeDropdownMenu, ...subContentProps } = props ?? {};
253
+ const menuScope = useMenuScope(__scopeDropdownMenu, subSlot(slot, 'menu'));
254
+
255
+ return createElement(MenuPrimitive.SubContent, {
256
+ ...menuScope,
257
+ ...subContentProps,
258
+ style: {
259
+ ...props?.style,
260
+ // re-namespace exposed content custom properties
261
+ '--radix-dropdown-menu-content-transform-origin': 'var(--radix-popper-transform-origin)',
262
+ '--radix-dropdown-menu-content-available-width': 'var(--radix-popper-available-width)',
263
+ '--radix-dropdown-menu-content-available-height': 'var(--radix-popper-available-height)',
264
+ '--radix-dropdown-menu-trigger-width': 'var(--radix-popper-anchor-width)',
265
+ '--radix-dropdown-menu-trigger-height': 'var(--radix-popper-anchor-height)',
266
+ },
267
+ });
268
+ }
269
+
270
+ export {
271
+ Root as DropdownMenu,
272
+ Trigger as DropdownMenuTrigger,
273
+ Content as DropdownMenuContent,
274
+ Item as DropdownMenuItem,
275
+ };