@tamagui/create-menu 2.0.0-rc.4 → 2.0.0-rc.40
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/MenuPredefined.cjs +159 -157
- package/dist/cjs/MenuPredefined.native.js +159 -157
- package/dist/cjs/MenuPredefined.native.js.map +1 -1
- package/dist/cjs/createBaseMenu.cjs +1144 -933
- package/dist/cjs/createBaseMenu.native.js +1266 -1100
- package/dist/cjs/createBaseMenu.native.js.map +1 -1
- package/dist/cjs/createNativeMenu/createNativeMenu.cjs +282 -159
- package/dist/cjs/createNativeMenu/createNativeMenu.native.js +390 -268
- package/dist/cjs/createNativeMenu/createNativeMenu.native.js.map +1 -1
- package/dist/cjs/createNativeMenu/createNativeMenuTypes.cjs +7 -5
- package/dist/cjs/createNativeMenu/createNativeMenuTypes.native.js +7 -5
- package/dist/cjs/createNativeMenu/createNativeMenuTypes.native.js.map +1 -1
- package/dist/cjs/createNativeMenu/utils.cjs +85 -42
- package/dist/cjs/createNativeMenu/utils.native.js +83 -58
- package/dist/cjs/createNativeMenu/utils.native.js.map +1 -1
- package/dist/cjs/createNativeMenu/withNativeMenu.cjs +27 -17
- package/dist/cjs/createNativeMenu/withNativeMenu.native.js +22 -14
- package/dist/cjs/createNativeMenu/withNativeMenu.native.js.map +1 -1
- package/dist/cjs/index.cjs +15 -12
- package/dist/cjs/index.native.js +15 -12
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/MenuPredefined.mjs +144 -144
- package/dist/esm/MenuPredefined.mjs.map +1 -1
- package/dist/esm/MenuPredefined.native.js +144 -144
- package/dist/esm/MenuPredefined.native.js.map +1 -1
- package/dist/esm/createBaseMenu.mjs +1104 -895
- package/dist/esm/createBaseMenu.mjs.map +1 -1
- package/dist/esm/createBaseMenu.native.js +1226 -1062
- package/dist/esm/createBaseMenu.native.js.map +1 -1
- package/dist/esm/createNativeMenu/createNativeMenu.mjs +255 -134
- package/dist/esm/createNativeMenu/createNativeMenu.mjs.map +1 -1
- package/dist/esm/createNativeMenu/createNativeMenu.native.js +336 -216
- package/dist/esm/createNativeMenu/createNativeMenu.native.js.map +1 -1
- package/dist/esm/createNativeMenu/utils.mjs +58 -17
- package/dist/esm/createNativeMenu/utils.mjs.map +1 -1
- package/dist/esm/createNativeMenu/utils.native.js +57 -34
- package/dist/esm/createNativeMenu/utils.native.js.map +1 -1
- package/dist/esm/createNativeMenu/withNativeMenu.mjs +13 -5
- package/dist/esm/createNativeMenu/withNativeMenu.mjs.map +1 -1
- package/dist/esm/createNativeMenu/withNativeMenu.native.js +8 -2
- package/dist/esm/createNativeMenu/withNativeMenu.native.js.map +1 -1
- package/dist/esm/index.js +5 -6
- package/dist/esm/index.js.map +1 -6
- package/dist/esm/index.mjs +2 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +2 -1
- package/dist/esm/index.native.js.map +1 -1
- package/dist/jsx/MenuPredefined.mjs +144 -144
- package/dist/jsx/MenuPredefined.mjs.map +1 -1
- package/dist/jsx/MenuPredefined.native.js +159 -157
- package/dist/jsx/MenuPredefined.native.js.map +1 -1
- package/dist/jsx/createBaseMenu.mjs +1104 -895
- package/dist/jsx/createBaseMenu.mjs.map +1 -1
- package/dist/jsx/createBaseMenu.native.js +1266 -1100
- package/dist/jsx/createBaseMenu.native.js.map +1 -1
- package/dist/jsx/createNativeMenu/createNativeMenu.mjs +255 -134
- package/dist/jsx/createNativeMenu/createNativeMenu.mjs.map +1 -1
- package/dist/jsx/createNativeMenu/createNativeMenu.native.js +390 -268
- package/dist/jsx/createNativeMenu/createNativeMenu.native.js.map +1 -1
- package/dist/jsx/createNativeMenu/createNativeMenuTypes.native.js +7 -5
- package/dist/jsx/createNativeMenu/utils.mjs +58 -17
- package/dist/jsx/createNativeMenu/utils.mjs.map +1 -1
- package/dist/jsx/createNativeMenu/utils.native.js +83 -58
- package/dist/jsx/createNativeMenu/utils.native.js.map +1 -1
- package/dist/jsx/createNativeMenu/withNativeMenu.mjs +13 -5
- package/dist/jsx/createNativeMenu/withNativeMenu.mjs.map +1 -1
- package/dist/jsx/createNativeMenu/withNativeMenu.native.js +22 -14
- package/dist/jsx/createNativeMenu/withNativeMenu.native.js.map +1 -1
- package/dist/jsx/index.js +5 -6
- package/dist/jsx/index.js.map +1 -6
- package/dist/jsx/index.mjs +2 -1
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +15 -12
- package/dist/jsx/index.native.js.map +1 -1
- package/package.json +25 -27
- package/src/MenuPredefined.tsx +1 -1
- package/src/createBaseMenu.tsx +359 -271
- package/src/createNativeMenu/createNativeMenu.tsx +383 -222
- package/src/createNativeMenu/createNativeMenuTypes.ts +20 -20
- package/src/createNativeMenu/withNativeMenu.tsx +5 -3
- package/src/index.tsx +3 -5
- package/types/createBaseMenu.d.ts +121 -35
- package/types/createBaseMenu.d.ts.map +1 -1
- package/types/createNativeMenu/createNativeMenu.d.ts +21 -21
- package/types/createNativeMenu/createNativeMenu.d.ts.map +1 -1
- package/types/createNativeMenu/createNativeMenuTypes.d.ts +20 -20
- package/types/createNativeMenu/createNativeMenuTypes.d.ts.map +1 -1
- package/types/createNativeMenu/withNativeMenu.d.ts +3 -3
- package/types/createNativeMenu/withNativeMenu.d.ts.map +1 -1
- package/types/index.d.ts +3 -2
- package/types/index.d.ts.map +1 -1
- package/dist/cjs/MenuPredefined.js +0 -168
- package/dist/cjs/MenuPredefined.js.map +0 -6
- package/dist/cjs/createBaseMenu.js +0 -832
- package/dist/cjs/createBaseMenu.js.map +0 -6
- package/dist/cjs/createNativeMenu/createNativeMenu.js +0 -177
- package/dist/cjs/createNativeMenu/createNativeMenu.js.map +0 -6
- package/dist/cjs/createNativeMenu/createNativeMenuTypes.js +0 -14
- package/dist/cjs/createNativeMenu/createNativeMenuTypes.js.map +0 -6
- package/dist/cjs/createNativeMenu/index.cjs +0 -19
- package/dist/cjs/createNativeMenu/index.js +0 -16
- package/dist/cjs/createNativeMenu/index.js.map +0 -6
- package/dist/cjs/createNativeMenu/index.native.js +0 -22
- package/dist/cjs/createNativeMenu/index.native.js.map +0 -1
- package/dist/cjs/createNativeMenu/utils.js +0 -66
- package/dist/cjs/createNativeMenu/utils.js.map +0 -6
- package/dist/cjs/createNativeMenu/withNativeMenu.js +0 -30
- package/dist/cjs/createNativeMenu/withNativeMenu.js.map +0 -6
- package/dist/cjs/index.js +0 -23
- package/dist/cjs/index.js.map +0 -6
- package/dist/esm/MenuPredefined.js +0 -154
- package/dist/esm/MenuPredefined.js.map +0 -6
- package/dist/esm/createBaseMenu.js +0 -838
- package/dist/esm/createBaseMenu.js.map +0 -6
- package/dist/esm/createNativeMenu/createNativeMenu.js +0 -156
- package/dist/esm/createNativeMenu/createNativeMenu.js.map +0 -6
- package/dist/esm/createNativeMenu/createNativeMenuTypes.js +0 -1
- package/dist/esm/createNativeMenu/createNativeMenuTypes.js.map +0 -6
- package/dist/esm/createNativeMenu/index.js +0 -3
- package/dist/esm/createNativeMenu/index.js.map +0 -6
- package/dist/esm/createNativeMenu/index.mjs +0 -3
- package/dist/esm/createNativeMenu/index.mjs.map +0 -1
- package/dist/esm/createNativeMenu/index.native.js +0 -3
- package/dist/esm/createNativeMenu/index.native.js.map +0 -1
- package/dist/esm/createNativeMenu/utils.js +0 -47
- package/dist/esm/createNativeMenu/utils.js.map +0 -6
- package/dist/esm/createNativeMenu/withNativeMenu.js +0 -15
- package/dist/esm/createNativeMenu/withNativeMenu.js.map +0 -6
- package/dist/jsx/MenuPredefined.js +0 -154
- package/dist/jsx/MenuPredefined.js.map +0 -6
- package/dist/jsx/createBaseMenu.js +0 -838
- package/dist/jsx/createBaseMenu.js.map +0 -6
- package/dist/jsx/createNativeMenu/createNativeMenu.js +0 -156
- package/dist/jsx/createNativeMenu/createNativeMenu.js.map +0 -6
- package/dist/jsx/createNativeMenu/createNativeMenuTypes.js +0 -1
- package/dist/jsx/createNativeMenu/createNativeMenuTypes.js.map +0 -6
- package/dist/jsx/createNativeMenu/index.js +0 -3
- package/dist/jsx/createNativeMenu/index.js.map +0 -6
- package/dist/jsx/createNativeMenu/index.mjs +0 -3
- package/dist/jsx/createNativeMenu/index.mjs.map +0 -1
- package/dist/jsx/createNativeMenu/index.native.js +0 -22
- package/dist/jsx/createNativeMenu/index.native.js.map +0 -1
- package/dist/jsx/createNativeMenu/utils.js +0 -47
- package/dist/jsx/createNativeMenu/utils.js.map +0 -6
- package/dist/jsx/createNativeMenu/withNativeMenu.js +0 -15
- package/dist/jsx/createNativeMenu/withNativeMenu.js.map +0 -6
- package/src/createNativeMenu/index.tsx +0 -7
- package/types/createNativeMenu/index.d.ts +0 -4
- package/types/createNativeMenu/index.d.ts.map +0 -1
package/src/createBaseMenu.tsx
CHANGED
|
@@ -15,16 +15,18 @@ import type { RovingFocusGroupProps } from '@tamagui/roving-focus'
|
|
|
15
15
|
import { RovingFocusGroup } from '@tamagui/roving-focus'
|
|
16
16
|
import { useCallbackRef } from '@tamagui/use-callback-ref'
|
|
17
17
|
import { useDirection } from '@tamagui/use-direction'
|
|
18
|
-
import type {
|
|
18
|
+
import type { TextProps } from '@tamagui/web'
|
|
19
19
|
import {
|
|
20
20
|
type ViewProps,
|
|
21
21
|
composeEventHandlers,
|
|
22
22
|
composeRefs,
|
|
23
23
|
createStyledContext,
|
|
24
24
|
isWeb,
|
|
25
|
+
styled,
|
|
25
26
|
Text,
|
|
26
27
|
Theme,
|
|
27
28
|
useComposedRefs,
|
|
29
|
+
useIsTouchDevice,
|
|
28
30
|
useThemeName,
|
|
29
31
|
View,
|
|
30
32
|
withStaticProperties,
|
|
@@ -105,7 +107,7 @@ interface MenuAnchorProps extends PopperAnchorProps {}
|
|
|
105
107
|
* MenuPortal
|
|
106
108
|
* -----------------------------------------------------------------------------------------------*/
|
|
107
109
|
|
|
108
|
-
type PortalContextValue = { forceMount?:
|
|
110
|
+
type PortalContextValue = { forceMount?: boolean }
|
|
109
111
|
|
|
110
112
|
interface MenuPortalProps {
|
|
111
113
|
children?: React.ReactNode
|
|
@@ -113,7 +115,7 @@ interface MenuPortalProps {
|
|
|
113
115
|
* Used to force mounting when more control is needed. Useful when
|
|
114
116
|
* controlling animation with React animation libraries.
|
|
115
117
|
*/
|
|
116
|
-
forceMount?:
|
|
118
|
+
forceMount?: boolean
|
|
117
119
|
zIndex?: number
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -141,7 +143,7 @@ interface MenuContentProps extends MenuRootContentTypeProps {
|
|
|
141
143
|
* Used to force mounting when more control is needed. Useful when
|
|
142
144
|
* controlling animation with React animation libraries.
|
|
143
145
|
*/
|
|
144
|
-
forceMount?:
|
|
146
|
+
forceMount?: boolean
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
/* ---------------------------------------------------------------------------------------------- */
|
|
@@ -215,6 +217,11 @@ type MenuItemElement = MenuItemImplElement
|
|
|
215
217
|
interface MenuItemProps extends Omit<MenuItemImplProps, 'onSelect'> {
|
|
216
218
|
onSelect?: (event: Event) => void
|
|
217
219
|
unstyled?: boolean
|
|
220
|
+
/**
|
|
221
|
+
* Prevents the menu from closing when this item is selected.
|
|
222
|
+
* Useful for toggle items or multi-select scenarios.
|
|
223
|
+
*/
|
|
224
|
+
preventCloseOnSelect?: boolean
|
|
218
225
|
}
|
|
219
226
|
|
|
220
227
|
type MenuItemImplElement = TamaguiElement
|
|
@@ -285,7 +292,7 @@ interface MenuItemIndicatorProps extends PrimitiveSpanProps {
|
|
|
285
292
|
* Used to force mounting when more control is needed. Useful when
|
|
286
293
|
* controlling animation with React animation libraries.
|
|
287
294
|
*/
|
|
288
|
-
forceMount?:
|
|
295
|
+
forceMount?: boolean
|
|
289
296
|
}
|
|
290
297
|
|
|
291
298
|
/* -------------------------------------------------------------------------------------------------
|
|
@@ -346,7 +353,7 @@ export interface MenuSubContentProps extends Omit<
|
|
|
346
353
|
* Used to force mounting when more control is needed. Useful when
|
|
347
354
|
* controlling animation with React animation libraries.
|
|
348
355
|
*/
|
|
349
|
-
forceMount?:
|
|
356
|
+
forceMount?: boolean
|
|
350
357
|
}
|
|
351
358
|
|
|
352
359
|
type Point = { x: number; y: number }
|
|
@@ -369,15 +376,15 @@ const { Provider: MenuRootProvider, useStyledContext: useMenuRootContext } =
|
|
|
369
376
|
const MENU_CONTEXT = 'MenuContext'
|
|
370
377
|
|
|
371
378
|
export type CreateBaseMenuProps = {
|
|
372
|
-
Item?:
|
|
373
|
-
MenuGroup?:
|
|
374
|
-
Title?:
|
|
375
|
-
SubTitle?:
|
|
379
|
+
Item?: typeof MenuPredefined.MenuItem
|
|
380
|
+
MenuGroup?: typeof MenuPredefined.MenuGroup
|
|
381
|
+
Title?: typeof MenuPredefined.Title
|
|
382
|
+
SubTitle?: typeof MenuPredefined.SubTitle
|
|
376
383
|
Image?: React.ElementType
|
|
377
|
-
Icon?:
|
|
378
|
-
Indicator?:
|
|
379
|
-
Separator?:
|
|
380
|
-
Label?:
|
|
384
|
+
Icon?: typeof MenuPredefined.MenuIcon
|
|
385
|
+
Indicator?: typeof MenuPredefined.MenuIndicator
|
|
386
|
+
Separator?: typeof MenuPredefined.MenuSeparator
|
|
387
|
+
Label?: typeof MenuPredefined.MenuLabel
|
|
381
388
|
}
|
|
382
389
|
|
|
383
390
|
export function createBaseMenu({
|
|
@@ -392,6 +399,9 @@ export function createBaseMenu({
|
|
|
392
399
|
Label: _Label = MenuPredefined.MenuLabel,
|
|
393
400
|
}: CreateBaseMenuProps) {
|
|
394
401
|
const MenuComp = (props: ScopedProps<MenuBaseProps>) => {
|
|
402
|
+
const direction = useDirection(props.dir)
|
|
403
|
+
// default placement: bottom-start for LTR, bottom-end for RTL
|
|
404
|
+
const defaultPlacement = direction === 'rtl' ? 'bottom-end' : 'bottom-start'
|
|
395
405
|
const {
|
|
396
406
|
scope = MENU_CONTEXT,
|
|
397
407
|
open = false,
|
|
@@ -399,12 +409,16 @@ export function createBaseMenu({
|
|
|
399
409
|
dir,
|
|
400
410
|
onOpenChange,
|
|
401
411
|
modal = true,
|
|
412
|
+
allowFlip = { padding: 10 },
|
|
413
|
+
stayInFrame = { padding: 10 },
|
|
414
|
+
placement = defaultPlacement,
|
|
415
|
+
resize = true,
|
|
416
|
+
offset = 10,
|
|
402
417
|
...rest
|
|
403
418
|
} = props
|
|
404
419
|
const [content, setContent] = React.useState<MenuContentElement | null>(null)
|
|
405
420
|
const isUsingKeyboardRef = React.useRef(false)
|
|
406
421
|
const handleOpenChange = useCallbackRef(onOpenChange)
|
|
407
|
-
const direction = useDirection(dir)
|
|
408
422
|
|
|
409
423
|
if (isWeb) {
|
|
410
424
|
React.useEffect(() => {
|
|
@@ -433,7 +447,16 @@ export function createBaseMenu({
|
|
|
433
447
|
}
|
|
434
448
|
|
|
435
449
|
return (
|
|
436
|
-
<PopperPrimitive.Popper
|
|
450
|
+
<PopperPrimitive.Popper
|
|
451
|
+
scope={scope}
|
|
452
|
+
open={open}
|
|
453
|
+
placement={placement}
|
|
454
|
+
allowFlip={allowFlip}
|
|
455
|
+
stayInFrame={stayInFrame}
|
|
456
|
+
resize={resize}
|
|
457
|
+
offset={offset}
|
|
458
|
+
{...rest}
|
|
459
|
+
>
|
|
437
460
|
<MenuProvider
|
|
438
461
|
scope={scope}
|
|
439
462
|
open={open}
|
|
@@ -549,7 +572,7 @@ export function createBaseMenu({
|
|
|
549
572
|
|
|
550
573
|
return (
|
|
551
574
|
<Animate type="presence" present={isPresent}>
|
|
552
|
-
<PortalPrimitive>
|
|
575
|
+
<PortalPrimitive stackZIndex>
|
|
553
576
|
<>
|
|
554
577
|
<PortalProvider scope={scope} forceMount={forceMount}>
|
|
555
578
|
<View zIndex={zIndex || 100} inset={0} position="absolute">
|
|
@@ -580,7 +603,11 @@ export function createBaseMenu({
|
|
|
580
603
|
const { Provider: MenuContentProvider, useStyledContext: useMenuContentContext } =
|
|
581
604
|
createStyledContext<MenuContentContextValue>()
|
|
582
605
|
|
|
583
|
-
const
|
|
606
|
+
const MenuContentFrame = styled(PopperPrimitive.PopperContentFrame, {
|
|
607
|
+
name: CONTENT_NAME,
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
const MenuContent = MenuContentFrame.styleable<ScopedProps<MenuContentProps>>(
|
|
584
611
|
(props, forwardedRef) => {
|
|
585
612
|
const scope = props.scope || MENU_CONTEXT
|
|
586
613
|
const portalContext = usePortalContext(scope)
|
|
@@ -690,6 +717,9 @@ export function createBaseMenu({
|
|
|
690
717
|
const getItems = useCollection(scope)
|
|
691
718
|
const [currentItemId, setCurrentItemId] = React.useState<string | null>(null)
|
|
692
719
|
const contentRef = React.useRef<TamaguiElement>(null)
|
|
720
|
+
// the actual focusable content element (PopperContentFrame) differs from contentRef
|
|
721
|
+
// due to PopperContent's wrapper structure, so we capture it on mount
|
|
722
|
+
const focusableContentRef = React.useRef<HTMLElement | null>(null)
|
|
693
723
|
const composedRefs = useComposedRefs(
|
|
694
724
|
forwardedRef,
|
|
695
725
|
contentRef,
|
|
@@ -733,10 +763,28 @@ export function createBaseMenu({
|
|
|
733
763
|
return () => clearTimeout(timerRef.current)
|
|
734
764
|
}, [])
|
|
735
765
|
|
|
736
|
-
//
|
|
766
|
+
// capture the actual focusable content element on mount
|
|
767
|
+
// PopperContent has a wrapper structure where the ref points to the outer
|
|
768
|
+
// TamaguiView but props like tabIndex go to the inner PopperContentFrame
|
|
769
|
+
React.useEffect(() => {
|
|
770
|
+
if (!isWeb || !context.open) return
|
|
771
|
+
// use requestAnimationFrame to ensure DOM is ready
|
|
772
|
+
const frame = requestAnimationFrame(() => {
|
|
773
|
+
// scope the query to within this menu's content to avoid grabbing a submenu's element
|
|
774
|
+
const container = contentRef.current as unknown as HTMLElement
|
|
775
|
+
const el = container?.querySelector('[data-tamagui-menu-content]') as HTMLElement
|
|
776
|
+
if (el) focusableContentRef.current = el
|
|
777
|
+
})
|
|
778
|
+
return () => cancelAnimationFrame(frame)
|
|
779
|
+
}, [context.open])
|
|
780
|
+
|
|
781
|
+
// dismiss on scroll (web only) - but not when scrolling inside the menu
|
|
737
782
|
React.useEffect(() => {
|
|
738
783
|
if (!isWeb || disableDismissOnScroll || !context.open) return
|
|
739
|
-
const handleScroll = () => {
|
|
784
|
+
const handleScroll = (event: Event) => {
|
|
785
|
+
// don't dismiss if scrolling inside the menu content
|
|
786
|
+
const target = event.target as HTMLElement
|
|
787
|
+
if ((contentRef.current as unknown as HTMLElement)?.contains(target)) return
|
|
740
788
|
onDismiss?.()
|
|
741
789
|
}
|
|
742
790
|
window.addEventListener('scroll', handleScroll, { capture: true, passive: true })
|
|
@@ -761,8 +809,11 @@ export function createBaseMenu({
|
|
|
761
809
|
const content = (
|
|
762
810
|
<PopperPrimitive.PopperContent
|
|
763
811
|
role="menu"
|
|
812
|
+
// tabIndex allows the content to be focusable so that onItemLeave can
|
|
813
|
+
// focus the content frame and properly blur the previously focused item
|
|
814
|
+
tabIndex={-1}
|
|
815
|
+
unstyled={unstyled}
|
|
764
816
|
{...(!unstyled && {
|
|
765
|
-
padding: 4,
|
|
766
817
|
backgroundColor: '$background',
|
|
767
818
|
borderWidth: 1,
|
|
768
819
|
borderColor: '$borderColor',
|
|
@@ -851,7 +902,7 @@ export function createBaseMenu({
|
|
|
851
902
|
onItemLeave={React.useCallback(
|
|
852
903
|
(event) => {
|
|
853
904
|
if (isPointerMovingToSubmenu(event)) return
|
|
854
|
-
|
|
905
|
+
focusableContentRef.current?.focus()
|
|
855
906
|
setCurrentItemId(null)
|
|
856
907
|
},
|
|
857
908
|
[isPointerMovingToSubmenu]
|
|
@@ -881,7 +932,7 @@ export function createBaseMenu({
|
|
|
881
932
|
const content = document.querySelector(
|
|
882
933
|
'[data-tamagui-menu-content]'
|
|
883
934
|
) as HTMLElement | null
|
|
884
|
-
content?.focus()
|
|
935
|
+
content?.focus({ preventScroll: true })
|
|
885
936
|
})}
|
|
886
937
|
onUnmountAutoFocus={onCloseAutoFocus}
|
|
887
938
|
>
|
|
@@ -928,106 +979,109 @@ export function createBaseMenu({
|
|
|
928
979
|
const ITEM_NAME = 'MenuItem'
|
|
929
980
|
const ITEM_SELECT = 'menu.itemSelect'
|
|
930
981
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
rootContext.onClose()
|
|
972
|
-
}
|
|
973
|
-
} else {
|
|
974
|
-
// TODO: find a better way to handle this on native
|
|
975
|
-
onSelect?.({ target: menuItem } as unknown as Event)
|
|
982
|
+
// use styleable so styled(Menu.Item, { focusStyle }) passes pseudo styles through correctly
|
|
983
|
+
const MenuItem = _Item.styleable<ScopedProps<MenuItemProps>>((props, forwardedRef) => {
|
|
984
|
+
const {
|
|
985
|
+
disabled = false,
|
|
986
|
+
onSelect,
|
|
987
|
+
preventCloseOnSelect,
|
|
988
|
+
children,
|
|
989
|
+
scope = MENU_CONTEXT,
|
|
990
|
+
// filter out native-only props that shouldn't reach the DOM
|
|
991
|
+
// @ts-ignore
|
|
992
|
+
destructive,
|
|
993
|
+
// @ts-ignore
|
|
994
|
+
hidden,
|
|
995
|
+
// @ts-ignore
|
|
996
|
+
androidIconName,
|
|
997
|
+
// @ts-ignore
|
|
998
|
+
iosIconName,
|
|
999
|
+
...itemProps
|
|
1000
|
+
} = props
|
|
1001
|
+
const ref = React.useRef<TamaguiElement>(null)
|
|
1002
|
+
const rootContext = useMenuRootContext(scope)
|
|
1003
|
+
const contentContext = useMenuContentContext(scope)
|
|
1004
|
+
const composedRefs = useComposedRefs(forwardedRef, ref)
|
|
1005
|
+
const isPointerDownRef = React.useRef(false)
|
|
1006
|
+
|
|
1007
|
+
const handleSelect = () => {
|
|
1008
|
+
const menuItem = ref.current
|
|
1009
|
+
if (!disabled && menuItem) {
|
|
1010
|
+
if (isWeb) {
|
|
1011
|
+
const menuItemEl = menuItem as HTMLElement
|
|
1012
|
+
const itemSelectEvent = new CustomEvent(ITEM_SELECT, {
|
|
1013
|
+
bubbles: true,
|
|
1014
|
+
cancelable: true,
|
|
1015
|
+
})
|
|
1016
|
+
menuItemEl.addEventListener(ITEM_SELECT, (event) => onSelect?.(event), {
|
|
1017
|
+
once: true,
|
|
1018
|
+
})
|
|
1019
|
+
dispatchDiscreteCustomEvent(menuItemEl, itemSelectEvent)
|
|
1020
|
+
// close the menu unless preventCloseOnSelect is set or event.preventDefault() was called
|
|
1021
|
+
if (itemSelectEvent.defaultPrevented || preventCloseOnSelect) {
|
|
976
1022
|
isPointerDownRef.current = false
|
|
1023
|
+
} else {
|
|
1024
|
+
rootContext.onClose()
|
|
1025
|
+
}
|
|
1026
|
+
} else {
|
|
1027
|
+
// TODO: find a better way to handle this on native
|
|
1028
|
+
onSelect?.({ target: menuItem } as unknown as Event)
|
|
1029
|
+
isPointerDownRef.current = false
|
|
1030
|
+
if (!preventCloseOnSelect) {
|
|
977
1031
|
rootContext.onClose()
|
|
978
1032
|
}
|
|
979
1033
|
}
|
|
980
1034
|
}
|
|
1035
|
+
}
|
|
981
1036
|
|
|
982
|
-
|
|
1037
|
+
const content = typeof children === 'string' ? <Text>{children}</Text> : children
|
|
983
1038
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1039
|
+
return (
|
|
1040
|
+
<MenuItemImpl
|
|
1041
|
+
outlineStyle="none"
|
|
1042
|
+
{...itemProps}
|
|
1043
|
+
scope={scope}
|
|
1044
|
+
// @ts-ignore
|
|
1045
|
+
ref={composedRefs}
|
|
1046
|
+
disabled={disabled}
|
|
1047
|
+
onPress={composeEventHandlers(props.onPress, handleSelect)}
|
|
1048
|
+
onPointerDown={(event) => {
|
|
1049
|
+
props.onPointerDown?.(event)
|
|
1050
|
+
isPointerDownRef.current = true
|
|
1051
|
+
}}
|
|
1052
|
+
onPointerUp={composeEventHandlers(props.onPointerUp, (event) => {
|
|
1053
|
+
// Pointer down can move to a different menu item which should activate it on pointer up.
|
|
1054
|
+
// We dispatch a click for selection to allow composition with click based triggers and to
|
|
1055
|
+
// prevent Firefox from getting stuck in text selection mode when the menu closes.
|
|
1056
|
+
if (isWeb) {
|
|
1057
|
+
// @ts-ignore
|
|
1058
|
+
if (!isPointerDownRef.current) event.currentTarget?.click()
|
|
1059
|
+
}
|
|
1060
|
+
})}
|
|
1061
|
+
{...(isWeb
|
|
1062
|
+
? {
|
|
1063
|
+
onKeyDown: composeEventHandlers(props.onKeyDown, (event) => {
|
|
1064
|
+
const isTypingAhead = contentContext.searchRef.current !== ''
|
|
1065
|
+
if (disabled || (isTypingAhead && event.key === ' ')) return
|
|
1066
|
+
if (SELECTION_KEYS.includes(event.key)) {
|
|
1067
|
+
// @ts-ignore
|
|
1068
|
+
event.currentTarget?.click()
|
|
1069
|
+
/**
|
|
1070
|
+
* We prevent default browser behaviour for selection keys as they should trigger
|
|
1071
|
+
* a selection only:
|
|
1072
|
+
* - prevents space from scrolling the page.
|
|
1073
|
+
* - if keydown causes focus to move, prevents keydown from firing on the new target.
|
|
1074
|
+
*/
|
|
1075
|
+
event.preventDefault()
|
|
1076
|
+
}
|
|
1077
|
+
}),
|
|
1004
1078
|
}
|
|
1005
|
-
})}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
if (SELECTION_KEYS.includes(event.key)) {
|
|
1012
|
-
// @ts-ignore
|
|
1013
|
-
event.currentTarget?.click()
|
|
1014
|
-
/**
|
|
1015
|
-
* We prevent default browser behaviour for selection keys as they should trigger
|
|
1016
|
-
* a selection only:
|
|
1017
|
-
* - prevents space from scrolling the page.
|
|
1018
|
-
* - if keydown causes focus to move, prevents keydown from firing on the new target.
|
|
1019
|
-
*/
|
|
1020
|
-
event.preventDefault()
|
|
1021
|
-
}
|
|
1022
|
-
}),
|
|
1023
|
-
}
|
|
1024
|
-
: {})}
|
|
1025
|
-
>
|
|
1026
|
-
{content}
|
|
1027
|
-
</MenuItemImpl>
|
|
1028
|
-
)
|
|
1029
|
-
}
|
|
1030
|
-
)
|
|
1079
|
+
: {})}
|
|
1080
|
+
>
|
|
1081
|
+
{content}
|
|
1082
|
+
</MenuItemImpl>
|
|
1083
|
+
)
|
|
1084
|
+
})
|
|
1031
1085
|
|
|
1032
1086
|
const MenuItemImpl = React.forwardRef<
|
|
1033
1087
|
MenuItemImplElement,
|
|
@@ -1067,13 +1121,9 @@ export function createBaseMenu({
|
|
|
1067
1121
|
asChild
|
|
1068
1122
|
__scopeRovingFocusGroup={scope}
|
|
1069
1123
|
focusable={!disabled}
|
|
1070
|
-
{...(!unstyled && {
|
|
1071
|
-
flexDirection: 'row',
|
|
1072
|
-
alignItems: 'center',
|
|
1073
|
-
})}
|
|
1074
|
-
{...itemProps}
|
|
1075
1124
|
>
|
|
1076
1125
|
<_Item
|
|
1126
|
+
unstyled={unstyled}
|
|
1077
1127
|
componentName={ITEM_NAME}
|
|
1078
1128
|
role="menuitem"
|
|
1079
1129
|
data-highlighted={isFocused ? '' : undefined}
|
|
@@ -1199,40 +1249,39 @@ export function createBaseMenu({
|
|
|
1199
1249
|
|
|
1200
1250
|
const CHECKBOX_ITEM_NAME = 'MenuCheckboxItem'
|
|
1201
1251
|
|
|
1202
|
-
const MenuCheckboxItem =
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
})
|
|
1252
|
+
const MenuCheckboxItem = _Item.styleable<ScopedProps<MenuCheckboxItemProps>>(
|
|
1253
|
+
(props, forwardedRef) => {
|
|
1254
|
+
const {
|
|
1255
|
+
checked = false,
|
|
1256
|
+
onCheckedChange,
|
|
1257
|
+
scope = MENU_CONTEXT,
|
|
1258
|
+
// filter out native-only props
|
|
1259
|
+
// @ts-ignore - native menu value state
|
|
1260
|
+
value,
|
|
1261
|
+
// @ts-ignore - native menu value change handler
|
|
1262
|
+
onValueChange,
|
|
1263
|
+
...checkboxItemProps
|
|
1264
|
+
} = props
|
|
1265
|
+
return (
|
|
1266
|
+
<ItemIndicatorProvider scope={scope} checked={checked}>
|
|
1267
|
+
<MenuItem
|
|
1268
|
+
componentName={CHECKBOX_ITEM_NAME}
|
|
1269
|
+
role={(isWeb ? 'menuitemcheckbox' : 'menuitem') as 'menuitem'}
|
|
1270
|
+
aria-checked={isIndeterminate(checked) ? 'mixed' : checked}
|
|
1271
|
+
{...checkboxItemProps}
|
|
1272
|
+
scope={scope}
|
|
1273
|
+
ref={forwardedRef}
|
|
1274
|
+
data-state={getCheckedState(checked)}
|
|
1275
|
+
onSelect={composeEventHandlers(
|
|
1276
|
+
checkboxItemProps.onSelect,
|
|
1277
|
+
() => onCheckedChange?.(isIndeterminate(checked) ? true : !checked),
|
|
1278
|
+
{ checkDefaultPrevented: false }
|
|
1279
|
+
)}
|
|
1280
|
+
/>
|
|
1281
|
+
</ItemIndicatorProvider>
|
|
1282
|
+
)
|
|
1283
|
+
}
|
|
1284
|
+
)
|
|
1236
1285
|
|
|
1237
1286
|
MenuCheckboxItem.displayName = CHECKBOX_ITEM_NAME
|
|
1238
1287
|
/* -------------------------------------------------------------------------------------------------
|
|
@@ -1267,7 +1316,7 @@ export function createBaseMenu({
|
|
|
1267
1316
|
|
|
1268
1317
|
const RADIO_ITEM_NAME = 'MenuRadioItem'
|
|
1269
1318
|
|
|
1270
|
-
const MenuRadioItem =
|
|
1319
|
+
const MenuRadioItem = _Item.styleable<ScopedProps<MenuRadioItemProps>>(
|
|
1271
1320
|
(props, forwardedRef) => {
|
|
1272
1321
|
const { value, scope = MENU_CONTEXT, ...radioItemProps } = props
|
|
1273
1322
|
const context = useRadioGroupContext(scope)
|
|
@@ -1344,6 +1393,7 @@ export function createBaseMenu({
|
|
|
1344
1393
|
<PopperPrimitive.PopperArrow
|
|
1345
1394
|
scope={scope}
|
|
1346
1395
|
componentName="PopperArrow"
|
|
1396
|
+
unstyled={unstyled}
|
|
1347
1397
|
{...(!unstyled && {
|
|
1348
1398
|
backgroundColor: '$background',
|
|
1349
1399
|
})}
|
|
@@ -1364,15 +1414,58 @@ export function createBaseMenu({
|
|
|
1364
1414
|
createStyledContext<MenuSubContextValue>()
|
|
1365
1415
|
|
|
1366
1416
|
const MenuSub: React.FC<ScopedProps<MenuSubProps>> = (props) => {
|
|
1417
|
+
const isTouchDevice = useIsTouchDevice()
|
|
1418
|
+
const { scope = MENU_CONTEXT } = props
|
|
1419
|
+
const rootContext = useMenuRootContext(scope)
|
|
1420
|
+
|
|
1421
|
+
// detect if this sub is nested inside another sub that opened to a side
|
|
1422
|
+
const parentPopperContext = PopperPrimitive.usePopperContext(scope)
|
|
1423
|
+
const parentSide = parentPopperContext.placement?.split('-')[0]
|
|
1424
|
+
const isNestedSubmenu = parentSide === 'left' || parentSide === 'right'
|
|
1425
|
+
|
|
1426
|
+
// nested submenus inherit the parent's direction so they cascade
|
|
1427
|
+
// rather than flipping back on top of the grandparent
|
|
1428
|
+
const defaultPlacement = isTouchDevice
|
|
1429
|
+
? 'bottom'
|
|
1430
|
+
: isNestedSubmenu
|
|
1431
|
+
? (`${parentSide}-start` as any)
|
|
1432
|
+
: rootContext.dir === 'rtl'
|
|
1433
|
+
? 'left-start'
|
|
1434
|
+
: 'right-start'
|
|
1367
1435
|
const {
|
|
1368
|
-
scope = MENU_CONTEXT,
|
|
1369
1436
|
children,
|
|
1370
1437
|
open = false,
|
|
1371
1438
|
onOpenChange,
|
|
1372
|
-
allowFlip = { padding: 10 },
|
|
1439
|
+
allowFlip: allowFlipProp = { padding: 10 },
|
|
1373
1440
|
stayInFrame = { padding: 10 },
|
|
1441
|
+
placement = defaultPlacement,
|
|
1374
1442
|
...rest
|
|
1375
1443
|
} = props
|
|
1444
|
+
|
|
1445
|
+
// for nested submenus, flip to opposite side (never top/bottom which overlap parent)
|
|
1446
|
+
const allowFlip = React.useMemo(() => {
|
|
1447
|
+
if (!isNestedSubmenu || typeof allowFlipProp === 'boolean') return allowFlipProp
|
|
1448
|
+
if ((allowFlipProp as any).fallbackPlacements) return allowFlipProp
|
|
1449
|
+
|
|
1450
|
+
const side = placement.split('-')[0]
|
|
1451
|
+
const align = placement.split('-')[1] || 'start'
|
|
1452
|
+
const otherAlign = align === 'start' ? 'end' : 'start'
|
|
1453
|
+
|
|
1454
|
+
if (side === 'left' || side === 'right') {
|
|
1455
|
+
const oppositeSide = side === 'right' ? 'left' : 'right'
|
|
1456
|
+
return {
|
|
1457
|
+
...(typeof allowFlipProp === 'object' ? allowFlipProp : {}),
|
|
1458
|
+
fallbackPlacements: [
|
|
1459
|
+
`${side}-${otherAlign}`,
|
|
1460
|
+
`${oppositeSide}-${align}`,
|
|
1461
|
+
`${oppositeSide}-${otherAlign}`,
|
|
1462
|
+
] as any,
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
return allowFlipProp
|
|
1467
|
+
}, [isNestedSubmenu, allowFlipProp, placement])
|
|
1468
|
+
|
|
1376
1469
|
const parentMenuContext = useMenuContext(scope)
|
|
1377
1470
|
const [trigger, setTrigger] = React.useState<MenuSubTriggerElement | null>(null)
|
|
1378
1471
|
const [content, setContent] = React.useState<MenuContentElement | null>(null)
|
|
@@ -1386,6 +1479,8 @@ export function createBaseMenu({
|
|
|
1386
1479
|
|
|
1387
1480
|
return (
|
|
1388
1481
|
<PopperPrimitive.Popper
|
|
1482
|
+
open={open}
|
|
1483
|
+
placement={placement}
|
|
1389
1484
|
allowFlip={allowFlip}
|
|
1390
1485
|
stayInFrame={stayInFrame}
|
|
1391
1486
|
{...rest}
|
|
@@ -1432,15 +1527,9 @@ export function createBaseMenu({
|
|
|
1432
1527
|
const openTimerRef = React.useRef<number | null>(null)
|
|
1433
1528
|
const { pointerGraceTimerRef, onPointerGraceIntentChange } = contentContext
|
|
1434
1529
|
|
|
1435
|
-
//
|
|
1436
|
-
//
|
|
1437
|
-
const
|
|
1438
|
-
const effectiveDir: Direction =
|
|
1439
|
-
placementSide === 'left'
|
|
1440
|
-
? 'rtl'
|
|
1441
|
-
: placementSide === 'right'
|
|
1442
|
-
? 'ltr'
|
|
1443
|
-
: rootContext.dir
|
|
1530
|
+
// keyboard navigation follows text direction, not placement side
|
|
1531
|
+
// ArrowRight always opens, ArrowLeft always closes (flipped only for RTL)
|
|
1532
|
+
const effectiveDir: Direction = rootContext.dir
|
|
1444
1533
|
|
|
1445
1534
|
const clearOpenTimer = React.useCallback(() => {
|
|
1446
1535
|
if (openTimerRef.current) window.clearTimeout(openTimerRef.current)
|
|
@@ -1666,114 +1755,113 @@ export function createBaseMenu({
|
|
|
1666
1755
|
|
|
1667
1756
|
const SUB_CONTENT_NAME = 'MenuSubContent'
|
|
1668
1757
|
|
|
1669
|
-
const
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
>((props, forwardedRef) => {
|
|
1673
|
-
const scope = props.scope || MENU_CONTEXT
|
|
1674
|
-
const portalContext = usePortalContext(scope)
|
|
1675
|
-
const { forceMount = portalContext.forceMount, ...subContentProps } = props
|
|
1676
|
-
const context = useMenuContext(scope)
|
|
1677
|
-
const rootContext = useMenuRootContext(scope)
|
|
1678
|
-
const subContext = useMenuSubContext(scope)
|
|
1679
|
-
const popperContext = PopperPrimitive.usePopperContext(scope)
|
|
1680
|
-
const ref = React.useRef<MenuSubContentElement>(null)
|
|
1681
|
-
const composedRefs = useComposedRefs(forwardedRef, ref)
|
|
1758
|
+
const MenuSubContentFrame = styled(PopperPrimitive.PopperContentFrame, {
|
|
1759
|
+
name: SUB_CONTENT_NAME,
|
|
1760
|
+
})
|
|
1682
1761
|
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
placementSide === 'left' || placementSide === 'right'
|
|
1695
|
-
? placementSide
|
|
1696
|
-
: rootContext.dir === 'rtl'
|
|
1697
|
-
? 'left'
|
|
1698
|
-
: 'right'
|
|
1762
|
+
const MenuSubContent = MenuSubContentFrame.styleable<ScopedProps<MenuSubContentProps>>(
|
|
1763
|
+
(props, forwardedRef) => {
|
|
1764
|
+
const scope = props.scope || MENU_CONTEXT
|
|
1765
|
+
const portalContext = usePortalContext(scope)
|
|
1766
|
+
const { forceMount = portalContext.forceMount, ...subContentProps } = props
|
|
1767
|
+
const context = useMenuContext(scope)
|
|
1768
|
+
const rootContext = useMenuRootContext(scope)
|
|
1769
|
+
const subContext = useMenuSubContext(scope)
|
|
1770
|
+
const popperContext = PopperPrimitive.usePopperContext(scope)
|
|
1771
|
+
const ref = React.useRef<MenuSubContentElement>(null)
|
|
1772
|
+
const composedRefs = useComposedRefs(forwardedRef, ref)
|
|
1699
1773
|
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
placementSide
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1774
|
+
// determine side from actual placement, not just RTL direction
|
|
1775
|
+
// placement like "left-start" or "right-end" - extract the side
|
|
1776
|
+
const placementSide = popperContext.placement?.split('-')[0] as
|
|
1777
|
+
| 'left'
|
|
1778
|
+
| 'right'
|
|
1779
|
+
| 'top'
|
|
1780
|
+
| 'bottom'
|
|
1781
|
+
| undefined
|
|
1782
|
+
// for submenus, we care about horizontal placement (left/right)
|
|
1783
|
+
// default to 'right' for LTR, 'left' for RTL
|
|
1784
|
+
const dataSide: Side =
|
|
1785
|
+
placementSide === 'left' || placementSide === 'right'
|
|
1786
|
+
? placementSide
|
|
1787
|
+
: rootContext.dir === 'rtl'
|
|
1788
|
+
? 'left'
|
|
1789
|
+
: 'right'
|
|
1790
|
+
|
|
1791
|
+
// keyboard navigation follows text direction, not placement side
|
|
1792
|
+
const effectiveDir: Direction = rootContext.dir
|
|
1707
1793
|
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
event.preventDefault()
|
|
1731
|
-
}}
|
|
1732
|
-
// The menu might close because of focusing another menu item in the parent menu. We
|
|
1733
|
-
// don't want it to refocus the trigger in that case so we handle trigger focus ourselves.
|
|
1734
|
-
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
1735
|
-
onFocusOutside={composeEventHandlers(props.onFocusOutside, (event) => {
|
|
1736
|
-
// We prevent closing when the trigger is focused to avoid triggering a re-open animation
|
|
1737
|
-
// on pointer interaction.
|
|
1738
|
-
if (event.target !== subContext.trigger) context.onOpenChange(false)
|
|
1739
|
-
})}
|
|
1740
|
-
onEscapeKeyDown={composeEventHandlers(props.onEscapeKeyDown, (event) => {
|
|
1741
|
-
// close only this submenu, not the root menu
|
|
1742
|
-
context.onOpenChange(false)
|
|
1743
|
-
// return focus to the submenu trigger with focusVisible since this is keyboard navigation
|
|
1744
|
-
// @ts-ignore focusVisible is a newer API
|
|
1745
|
-
subContext.trigger?.focus({ focusVisible: true })
|
|
1746
|
-
// ensure pressing escape in submenu doesn't escape full screen mode
|
|
1747
|
-
event.preventDefault()
|
|
1748
|
-
})}
|
|
1749
|
-
{...(isWeb
|
|
1750
|
-
? {
|
|
1751
|
-
onKeyDown: composeEventHandlers(props.onKeyDown, (event) => {
|
|
1752
|
-
// Submenu key events bubble through portals. We only care about keys in this menu.
|
|
1753
|
-
// @ts-ignore
|
|
1754
|
-
const isKeyDownInside = event.currentTarget.contains(
|
|
1755
|
-
event.target as HTMLElement
|
|
1756
|
-
)
|
|
1757
|
-
// use effectiveDir so arrow keys match the submenu's actual position
|
|
1758
|
-
// (e.g., ArrowRight closes a left-side submenu)
|
|
1759
|
-
const isCloseKey = SUB_CLOSE_KEYS[effectiveDir].includes(event.key)
|
|
1760
|
-
if (isKeyDownInside && isCloseKey) {
|
|
1761
|
-
context.onOpenChange(false)
|
|
1762
|
-
// We focus manually because we prevented it in `onCloseAutoFocus`
|
|
1763
|
-
// use focusVisible: true since this is keyboard navigation
|
|
1764
|
-
// @ts-ignore focusVisible is a newer API
|
|
1765
|
-
subContext.trigger?.focus({ focusVisible: true })
|
|
1766
|
-
// prevent window from scrolling
|
|
1767
|
-
event.preventDefault()
|
|
1768
|
-
}
|
|
1769
|
-
}),
|
|
1794
|
+
return (
|
|
1795
|
+
<Collection.Provider scope={scope}>
|
|
1796
|
+
<Collection.Slot scope={scope}>
|
|
1797
|
+
<MenuContentImpl
|
|
1798
|
+
id={subContext.contentId}
|
|
1799
|
+
aria-labelledby={subContext.triggerId}
|
|
1800
|
+
{...subContentProps}
|
|
1801
|
+
ref={composedRefs}
|
|
1802
|
+
data-side={dataSide}
|
|
1803
|
+
disableOutsidePointerEvents={false}
|
|
1804
|
+
disableOutsideScroll={false}
|
|
1805
|
+
trapFocus={false}
|
|
1806
|
+
onOpenAutoFocus={(event) => {
|
|
1807
|
+
// when opening a submenu, focus content for keyboard users only
|
|
1808
|
+
if (rootContext.isUsingKeyboardRef.current) {
|
|
1809
|
+
// scope query to this submenu's subtree so nested submenus
|
|
1810
|
+
// don't accidentally focus a sibling/parent submenu content
|
|
1811
|
+
const root = ref.current as unknown as HTMLElement
|
|
1812
|
+
const content = root?.querySelector?.(
|
|
1813
|
+
'[data-tamagui-menu-content]'
|
|
1814
|
+
) as HTMLElement | null
|
|
1815
|
+
;(content || root)?.focus({ preventScroll: true })
|
|
1770
1816
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1817
|
+
event.preventDefault()
|
|
1818
|
+
}}
|
|
1819
|
+
// The menu might close because of focusing another menu item in the parent menu. We
|
|
1820
|
+
// don't want it to refocus the trigger in that case so we handle trigger focus ourselves.
|
|
1821
|
+
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
1822
|
+
onFocusOutside={composeEventHandlers(props.onFocusOutside, (event) => {
|
|
1823
|
+
// We prevent closing when the trigger is focused to avoid triggering a re-open animation
|
|
1824
|
+
// on pointer interaction.
|
|
1825
|
+
if (event.target !== subContext.trigger) context.onOpenChange(false)
|
|
1826
|
+
})}
|
|
1827
|
+
onEscapeKeyDown={composeEventHandlers(props.onEscapeKeyDown, (event) => {
|
|
1828
|
+
// close only this submenu, not the root menu
|
|
1829
|
+
context.onOpenChange(false)
|
|
1830
|
+
// return focus to the submenu trigger with focusVisible since this is keyboard navigation
|
|
1831
|
+
// @ts-ignore focusVisible is a newer API
|
|
1832
|
+
subContext.trigger?.focus({ focusVisible: true })
|
|
1833
|
+
// ensure pressing escape in submenu doesn't escape full screen mode
|
|
1834
|
+
event.preventDefault()
|
|
1835
|
+
})}
|
|
1836
|
+
{...(isWeb
|
|
1837
|
+
? {
|
|
1838
|
+
onKeyDown: composeEventHandlers(props.onKeyDown, (event) => {
|
|
1839
|
+
// Submenu key events bubble through portals. We only care about keys in this menu.
|
|
1840
|
+
// @ts-ignore
|
|
1841
|
+
const isKeyDownInside = event.currentTarget.contains(
|
|
1842
|
+
event.target as HTMLElement
|
|
1843
|
+
)
|
|
1844
|
+
// use effectiveDir so arrow keys match the submenu's actual position
|
|
1845
|
+
// (e.g., ArrowRight closes a left-side submenu)
|
|
1846
|
+
const isCloseKey = SUB_CLOSE_KEYS[effectiveDir].includes(event.key)
|
|
1847
|
+
if (isKeyDownInside && isCloseKey) {
|
|
1848
|
+
context.onOpenChange(false)
|
|
1849
|
+
// We focus manually because we prevented it in `onCloseAutoFocus`
|
|
1850
|
+
// use focusVisible: true since this is keyboard navigation
|
|
1851
|
+
// @ts-ignore focusVisible is a newer API
|
|
1852
|
+
subContext.trigger?.focus({ focusVisible: true })
|
|
1853
|
+
// prevent window from scrolling
|
|
1854
|
+
event.preventDefault()
|
|
1855
|
+
}
|
|
1856
|
+
}),
|
|
1857
|
+
}
|
|
1858
|
+
: null)}
|
|
1859
|
+
/>
|
|
1860
|
+
</Collection.Slot>
|
|
1861
|
+
</Collection.Provider>
|
|
1862
|
+
)
|
|
1863
|
+
}
|
|
1864
|
+
)
|
|
1777
1865
|
|
|
1778
1866
|
MenuSubContent.displayName = SUB_CONTENT_NAME
|
|
1779
1867
|
|
|
@@ -1853,7 +1941,7 @@ function focusFirst(candidates: HTMLElement[], options?: { focusVisible?: boolea
|
|
|
1853
1941
|
// if focus is already where we want to go, we don't want to keep going through the candidates
|
|
1854
1942
|
if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return
|
|
1855
1943
|
// @ts-ignore focusVisible is a newer API not yet in all TS libs
|
|
1856
|
-
candidate.focus({ focusVisible: options?.focusVisible })
|
|
1944
|
+
candidate.focus({ preventScroll: true, focusVisible: options?.focusVisible })
|
|
1857
1945
|
if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return
|
|
1858
1946
|
}
|
|
1859
1947
|
}
|