@tamagui/create-menu 2.0.0-rc.8 → 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/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 +1150 -941
- package/dist/cjs/createBaseMenu.native.js +1280 -1108
- package/dist/cjs/createBaseMenu.native.js.map +1 -1
- package/dist/cjs/createNativeMenu/createNativeMenu.cjs +318 -159
- package/dist/cjs/createNativeMenu/createNativeMenu.native.js +430 -267
- 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 +1110 -903
- package/dist/esm/createBaseMenu.mjs.map +1 -1
- package/dist/esm/createBaseMenu.native.js +1240 -1070
- package/dist/esm/createBaseMenu.native.js.map +1 -1
- package/dist/esm/createNativeMenu/createNativeMenu.mjs +291 -134
- package/dist/esm/createNativeMenu/createNativeMenu.mjs.map +1 -1
- package/dist/esm/createNativeMenu/createNativeMenu.native.js +377 -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 +1110 -903
- package/dist/jsx/createBaseMenu.mjs.map +1 -1
- package/dist/jsx/createBaseMenu.native.js +1280 -1108
- package/dist/jsx/createBaseMenu.native.js.map +1 -1
- package/dist/jsx/createNativeMenu/createNativeMenu.mjs +291 -134
- package/dist/jsx/createNativeMenu/createNativeMenu.mjs.map +1 -1
- package/dist/jsx/createNativeMenu/createNativeMenu.native.js +430 -267
- 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 +26 -29
- package/src/createBaseMenu.tsx +367 -266
- package/src/createNativeMenu/createNativeMenu.tsx +448 -220
- 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 +117 -31
- 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 -843
- 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 -849
- 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 -849
- 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,
|
|
@@ -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
|
|
@@ -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,
|
|
@@ -401,12 +411,14 @@ export function createBaseMenu({
|
|
|
401
411
|
modal = true,
|
|
402
412
|
allowFlip = { padding: 10 },
|
|
403
413
|
stayInFrame = { padding: 10 },
|
|
414
|
+
placement = defaultPlacement,
|
|
415
|
+
resize = true,
|
|
416
|
+
offset = 10,
|
|
404
417
|
...rest
|
|
405
418
|
} = props
|
|
406
419
|
const [content, setContent] = React.useState<MenuContentElement | null>(null)
|
|
407
420
|
const isUsingKeyboardRef = React.useRef(false)
|
|
408
421
|
const handleOpenChange = useCallbackRef(onOpenChange)
|
|
409
|
-
const direction = useDirection(dir)
|
|
410
422
|
|
|
411
423
|
if (isWeb) {
|
|
412
424
|
React.useEffect(() => {
|
|
@@ -437,8 +449,12 @@ export function createBaseMenu({
|
|
|
437
449
|
return (
|
|
438
450
|
<PopperPrimitive.Popper
|
|
439
451
|
scope={scope}
|
|
452
|
+
open={open}
|
|
453
|
+
placement={placement}
|
|
440
454
|
allowFlip={allowFlip}
|
|
441
455
|
stayInFrame={stayInFrame}
|
|
456
|
+
resize={resize}
|
|
457
|
+
offset={offset}
|
|
442
458
|
{...rest}
|
|
443
459
|
>
|
|
444
460
|
<MenuProvider
|
|
@@ -556,7 +572,7 @@ export function createBaseMenu({
|
|
|
556
572
|
|
|
557
573
|
return (
|
|
558
574
|
<Animate type="presence" present={isPresent}>
|
|
559
|
-
<PortalPrimitive>
|
|
575
|
+
<PortalPrimitive stackZIndex>
|
|
560
576
|
<>
|
|
561
577
|
<PortalProvider scope={scope} forceMount={forceMount}>
|
|
562
578
|
<View zIndex={zIndex || 100} inset={0} position="absolute">
|
|
@@ -587,7 +603,11 @@ export function createBaseMenu({
|
|
|
587
603
|
const { Provider: MenuContentProvider, useStyledContext: useMenuContentContext } =
|
|
588
604
|
createStyledContext<MenuContentContextValue>()
|
|
589
605
|
|
|
590
|
-
const
|
|
606
|
+
const MenuContentFrame = styled(PopperPrimitive.PopperContentFrame, {
|
|
607
|
+
name: CONTENT_NAME,
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
const MenuContent = MenuContentFrame.styleable<ScopedProps<MenuContentProps>>(
|
|
591
611
|
(props, forwardedRef) => {
|
|
592
612
|
const scope = props.scope || MENU_CONTEXT
|
|
593
613
|
const portalContext = usePortalContext(scope)
|
|
@@ -694,9 +714,13 @@ export function createBaseMenu({
|
|
|
694
714
|
|
|
695
715
|
const context = useMenuContext(scope)
|
|
696
716
|
const rootContext = useMenuRootContext(scope)
|
|
717
|
+
const popperContext = PopperPrimitive.usePopperContext(scope)
|
|
697
718
|
const getItems = useCollection(scope)
|
|
698
719
|
const [currentItemId, setCurrentItemId] = React.useState<string | null>(null)
|
|
699
720
|
const contentRef = React.useRef<TamaguiElement>(null)
|
|
721
|
+
// the actual focusable content element (PopperContentFrame) differs from contentRef
|
|
722
|
+
// due to PopperContent's wrapper structure, so we capture it on mount
|
|
723
|
+
const focusableContentRef = React.useRef<HTMLElement | null>(null)
|
|
700
724
|
const composedRefs = useComposedRefs(
|
|
701
725
|
forwardedRef,
|
|
702
726
|
contentRef,
|
|
@@ -740,17 +764,54 @@ export function createBaseMenu({
|
|
|
740
764
|
return () => clearTimeout(timerRef.current)
|
|
741
765
|
}, [])
|
|
742
766
|
|
|
743
|
-
//
|
|
767
|
+
// capture the actual focusable content element on mount
|
|
768
|
+
// PopperContent has a wrapper structure where the ref points to the outer
|
|
769
|
+
// TamaguiView but props like tabIndex go to the inner PopperContentFrame
|
|
770
|
+
React.useEffect(() => {
|
|
771
|
+
if (!isWeb || !context.open) return
|
|
772
|
+
// use requestAnimationFrame to ensure DOM is ready
|
|
773
|
+
const frame = requestAnimationFrame(() => {
|
|
774
|
+
// scope the query to within this menu's content to avoid grabbing a submenu's element
|
|
775
|
+
const container = contentRef.current as unknown as HTMLElement
|
|
776
|
+
const el = container?.querySelector('[data-tamagui-menu-content]') as HTMLElement
|
|
777
|
+
if (el) focusableContentRef.current = el
|
|
778
|
+
})
|
|
779
|
+
return () => cancelAnimationFrame(frame)
|
|
780
|
+
}, [context.open])
|
|
781
|
+
|
|
782
|
+
// dismiss on scroll (web only) - only when the scroll actually moves the
|
|
783
|
+
// menu's anchor. a scroll in an unrelated subtree leaves the menu's
|
|
784
|
+
// position unchanged, so it should not dismiss.
|
|
744
785
|
React.useEffect(() => {
|
|
745
786
|
if (!isWeb || disableDismissOnScroll || !context.open) return
|
|
746
|
-
const handleScroll = () => {
|
|
787
|
+
const handleScroll = (event: Event) => {
|
|
788
|
+
const scrolled = event.target as Node | null
|
|
789
|
+
if (!scrolled) return
|
|
790
|
+
// never dismiss when scrolling within the menu's own content
|
|
791
|
+
const content = contentRef.current as unknown as HTMLElement | null
|
|
792
|
+
if (content?.contains(scrolled)) return
|
|
793
|
+
// resolve the menu's anchor element (the trigger). a virtual reference
|
|
794
|
+
// (e.g. a context menu pinned to a point) may expose its DOM origin
|
|
795
|
+
// via `contextElement`.
|
|
796
|
+
const reference = popperContext.refs?.reference?.current as
|
|
797
|
+
| Element
|
|
798
|
+
| { contextElement?: Element | null }
|
|
799
|
+
| null
|
|
800
|
+
| undefined
|
|
801
|
+
const anchor =
|
|
802
|
+
reference instanceof Element ? reference : (reference?.contextElement ?? null)
|
|
803
|
+
// when we can see the anchor element, only dismiss if the scrolled
|
|
804
|
+
// container actually contains it - i.e. this scroll moved the menu
|
|
805
|
+
// relative to the viewport. window/document scrolls have `document`
|
|
806
|
+
// as the target, which contains the anchor, so those still dismiss.
|
|
807
|
+
if (anchor && !scrolled.contains(anchor)) return
|
|
747
808
|
onDismiss?.()
|
|
748
809
|
}
|
|
749
810
|
window.addEventListener('scroll', handleScroll, { capture: true, passive: true })
|
|
750
811
|
return () => {
|
|
751
812
|
window.removeEventListener('scroll', handleScroll, { capture: true })
|
|
752
813
|
}
|
|
753
|
-
}, [disableDismissOnScroll, context.open, onDismiss])
|
|
814
|
+
}, [disableDismissOnScroll, context.open, onDismiss, popperContext.refs])
|
|
754
815
|
|
|
755
816
|
// Make sure the whole tree has focus guards as our `MenuContent` may be
|
|
756
817
|
// the last element in the DOM (beacuse of the `Portal`)
|
|
@@ -768,8 +829,11 @@ export function createBaseMenu({
|
|
|
768
829
|
const content = (
|
|
769
830
|
<PopperPrimitive.PopperContent
|
|
770
831
|
role="menu"
|
|
832
|
+
// tabIndex allows the content to be focusable so that onItemLeave can
|
|
833
|
+
// focus the content frame and properly blur the previously focused item
|
|
834
|
+
tabIndex={-1}
|
|
835
|
+
unstyled={unstyled}
|
|
771
836
|
{...(!unstyled && {
|
|
772
|
-
padding: 4,
|
|
773
837
|
backgroundColor: '$background',
|
|
774
838
|
borderWidth: 1,
|
|
775
839
|
borderColor: '$borderColor',
|
|
@@ -858,7 +922,7 @@ export function createBaseMenu({
|
|
|
858
922
|
onItemLeave={React.useCallback(
|
|
859
923
|
(event) => {
|
|
860
924
|
if (isPointerMovingToSubmenu(event)) return
|
|
861
|
-
|
|
925
|
+
focusableContentRef.current?.focus()
|
|
862
926
|
setCurrentItemId(null)
|
|
863
927
|
},
|
|
864
928
|
[isPointerMovingToSubmenu]
|
|
@@ -888,7 +952,7 @@ export function createBaseMenu({
|
|
|
888
952
|
const content = document.querySelector(
|
|
889
953
|
'[data-tamagui-menu-content]'
|
|
890
954
|
) as HTMLElement | null
|
|
891
|
-
content?.focus()
|
|
955
|
+
content?.focus({ preventScroll: true })
|
|
892
956
|
})}
|
|
893
957
|
onUnmountAutoFocus={onCloseAutoFocus}
|
|
894
958
|
>
|
|
@@ -935,106 +999,109 @@ export function createBaseMenu({
|
|
|
935
999
|
const ITEM_NAME = 'MenuItem'
|
|
936
1000
|
const ITEM_SELECT = 'menu.itemSelect'
|
|
937
1001
|
|
|
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
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
rootContext.onClose()
|
|
979
|
-
}
|
|
980
|
-
} else {
|
|
981
|
-
// TODO: find a better way to handle this on native
|
|
982
|
-
onSelect?.({ target: menuItem } as unknown as Event)
|
|
1002
|
+
// use styleable so styled(Menu.Item, { focusStyle }) passes pseudo styles through correctly
|
|
1003
|
+
const MenuItem = _Item.styleable<ScopedProps<MenuItemProps>>((props, forwardedRef) => {
|
|
1004
|
+
const {
|
|
1005
|
+
disabled = false,
|
|
1006
|
+
onSelect,
|
|
1007
|
+
preventCloseOnSelect,
|
|
1008
|
+
children,
|
|
1009
|
+
scope = MENU_CONTEXT,
|
|
1010
|
+
// filter out native-only props that shouldn't reach the DOM
|
|
1011
|
+
// @ts-ignore
|
|
1012
|
+
destructive,
|
|
1013
|
+
// @ts-ignore
|
|
1014
|
+
hidden,
|
|
1015
|
+
// @ts-ignore
|
|
1016
|
+
androidIconName,
|
|
1017
|
+
// @ts-ignore
|
|
1018
|
+
iosIconName,
|
|
1019
|
+
...itemProps
|
|
1020
|
+
} = props
|
|
1021
|
+
const ref = React.useRef<TamaguiElement>(null)
|
|
1022
|
+
const rootContext = useMenuRootContext(scope)
|
|
1023
|
+
const contentContext = useMenuContentContext(scope)
|
|
1024
|
+
const composedRefs = useComposedRefs(forwardedRef, ref)
|
|
1025
|
+
const isPointerDownRef = React.useRef(false)
|
|
1026
|
+
|
|
1027
|
+
const handleSelect = () => {
|
|
1028
|
+
const menuItem = ref.current
|
|
1029
|
+
if (!disabled && menuItem) {
|
|
1030
|
+
if (isWeb) {
|
|
1031
|
+
const menuItemEl = menuItem as HTMLElement
|
|
1032
|
+
const itemSelectEvent = new CustomEvent(ITEM_SELECT, {
|
|
1033
|
+
bubbles: true,
|
|
1034
|
+
cancelable: true,
|
|
1035
|
+
})
|
|
1036
|
+
menuItemEl.addEventListener(ITEM_SELECT, (event) => onSelect?.(event), {
|
|
1037
|
+
once: true,
|
|
1038
|
+
})
|
|
1039
|
+
dispatchDiscreteCustomEvent(menuItemEl, itemSelectEvent)
|
|
1040
|
+
// close the menu unless preventCloseOnSelect is set or event.preventDefault() was called
|
|
1041
|
+
if (itemSelectEvent.defaultPrevented || preventCloseOnSelect) {
|
|
983
1042
|
isPointerDownRef.current = false
|
|
1043
|
+
} else {
|
|
1044
|
+
rootContext.onClose()
|
|
1045
|
+
}
|
|
1046
|
+
} else {
|
|
1047
|
+
// TODO: find a better way to handle this on native
|
|
1048
|
+
onSelect?.({ target: menuItem } as unknown as Event)
|
|
1049
|
+
isPointerDownRef.current = false
|
|
1050
|
+
if (!preventCloseOnSelect) {
|
|
984
1051
|
rootContext.onClose()
|
|
985
1052
|
}
|
|
986
1053
|
}
|
|
987
1054
|
}
|
|
1055
|
+
}
|
|
988
1056
|
|
|
989
|
-
|
|
1057
|
+
const content = typeof children === 'string' ? <Text>{children}</Text> : children
|
|
990
1058
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1059
|
+
return (
|
|
1060
|
+
<MenuItemImpl
|
|
1061
|
+
outlineStyle="none"
|
|
1062
|
+
{...itemProps}
|
|
1063
|
+
scope={scope}
|
|
1064
|
+
// @ts-ignore
|
|
1065
|
+
ref={composedRefs}
|
|
1066
|
+
disabled={disabled}
|
|
1067
|
+
onPress={composeEventHandlers(props.onPress, handleSelect)}
|
|
1068
|
+
onPointerDown={(event) => {
|
|
1069
|
+
props.onPointerDown?.(event)
|
|
1070
|
+
isPointerDownRef.current = true
|
|
1071
|
+
}}
|
|
1072
|
+
onPointerUp={composeEventHandlers(props.onPointerUp, (event) => {
|
|
1073
|
+
// Pointer down can move to a different menu item which should activate it on pointer up.
|
|
1074
|
+
// We dispatch a click for selection to allow composition with click based triggers and to
|
|
1075
|
+
// prevent Firefox from getting stuck in text selection mode when the menu closes.
|
|
1076
|
+
if (isWeb) {
|
|
1077
|
+
// @ts-ignore
|
|
1078
|
+
if (!isPointerDownRef.current) event.currentTarget?.click()
|
|
1079
|
+
}
|
|
1080
|
+
})}
|
|
1081
|
+
{...(isWeb
|
|
1082
|
+
? {
|
|
1083
|
+
onKeyDown: composeEventHandlers(props.onKeyDown, (event) => {
|
|
1084
|
+
const isTypingAhead = contentContext.searchRef.current !== ''
|
|
1085
|
+
if (disabled || (isTypingAhead && event.key === ' ')) return
|
|
1086
|
+
if (SELECTION_KEYS.includes(event.key)) {
|
|
1087
|
+
// @ts-ignore
|
|
1088
|
+
event.currentTarget?.click()
|
|
1089
|
+
/**
|
|
1090
|
+
* We prevent default browser behaviour for selection keys as they should trigger
|
|
1091
|
+
* a selection only:
|
|
1092
|
+
* - prevents space from scrolling the page.
|
|
1093
|
+
* - if keydown causes focus to move, prevents keydown from firing on the new target.
|
|
1094
|
+
*/
|
|
1095
|
+
event.preventDefault()
|
|
1096
|
+
}
|
|
1097
|
+
}),
|
|
1011
1098
|
}
|
|
1012
|
-
})}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
if (SELECTION_KEYS.includes(event.key)) {
|
|
1019
|
-
// @ts-ignore
|
|
1020
|
-
event.currentTarget?.click()
|
|
1021
|
-
/**
|
|
1022
|
-
* We prevent default browser behaviour for selection keys as they should trigger
|
|
1023
|
-
* a selection only:
|
|
1024
|
-
* - prevents space from scrolling the page.
|
|
1025
|
-
* - if keydown causes focus to move, prevents keydown from firing on the new target.
|
|
1026
|
-
*/
|
|
1027
|
-
event.preventDefault()
|
|
1028
|
-
}
|
|
1029
|
-
}),
|
|
1030
|
-
}
|
|
1031
|
-
: {})}
|
|
1032
|
-
>
|
|
1033
|
-
{content}
|
|
1034
|
-
</MenuItemImpl>
|
|
1035
|
-
)
|
|
1036
|
-
}
|
|
1037
|
-
)
|
|
1099
|
+
: {})}
|
|
1100
|
+
>
|
|
1101
|
+
{content}
|
|
1102
|
+
</MenuItemImpl>
|
|
1103
|
+
)
|
|
1104
|
+
})
|
|
1038
1105
|
|
|
1039
1106
|
const MenuItemImpl = React.forwardRef<
|
|
1040
1107
|
MenuItemImplElement,
|
|
@@ -1074,13 +1141,9 @@ export function createBaseMenu({
|
|
|
1074
1141
|
asChild
|
|
1075
1142
|
__scopeRovingFocusGroup={scope}
|
|
1076
1143
|
focusable={!disabled}
|
|
1077
|
-
{...(!unstyled && {
|
|
1078
|
-
flexDirection: 'row',
|
|
1079
|
-
alignItems: 'center',
|
|
1080
|
-
})}
|
|
1081
|
-
{...itemProps}
|
|
1082
1144
|
>
|
|
1083
1145
|
<_Item
|
|
1146
|
+
unstyled={unstyled}
|
|
1084
1147
|
componentName={ITEM_NAME}
|
|
1085
1148
|
role="menuitem"
|
|
1086
1149
|
data-highlighted={isFocused ? '' : undefined}
|
|
@@ -1206,40 +1269,39 @@ export function createBaseMenu({
|
|
|
1206
1269
|
|
|
1207
1270
|
const CHECKBOX_ITEM_NAME = 'MenuCheckboxItem'
|
|
1208
1271
|
|
|
1209
|
-
const MenuCheckboxItem =
|
|
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
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
})
|
|
1272
|
+
const MenuCheckboxItem = _Item.styleable<ScopedProps<MenuCheckboxItemProps>>(
|
|
1273
|
+
(props, forwardedRef) => {
|
|
1274
|
+
const {
|
|
1275
|
+
checked = false,
|
|
1276
|
+
onCheckedChange,
|
|
1277
|
+
scope = MENU_CONTEXT,
|
|
1278
|
+
// filter out native-only props
|
|
1279
|
+
// @ts-ignore - native menu value state
|
|
1280
|
+
value,
|
|
1281
|
+
// @ts-ignore - native menu value change handler
|
|
1282
|
+
onValueChange,
|
|
1283
|
+
...checkboxItemProps
|
|
1284
|
+
} = props
|
|
1285
|
+
return (
|
|
1286
|
+
<ItemIndicatorProvider scope={scope} checked={checked}>
|
|
1287
|
+
<MenuItem
|
|
1288
|
+
componentName={CHECKBOX_ITEM_NAME}
|
|
1289
|
+
role={(isWeb ? 'menuitemcheckbox' : 'menuitem') as 'menuitem'}
|
|
1290
|
+
aria-checked={isIndeterminate(checked) ? 'mixed' : checked}
|
|
1291
|
+
{...checkboxItemProps}
|
|
1292
|
+
scope={scope}
|
|
1293
|
+
ref={forwardedRef}
|
|
1294
|
+
data-state={getCheckedState(checked)}
|
|
1295
|
+
onSelect={composeEventHandlers(
|
|
1296
|
+
checkboxItemProps.onSelect,
|
|
1297
|
+
() => onCheckedChange?.(isIndeterminate(checked) ? true : !checked),
|
|
1298
|
+
{ checkDefaultPrevented: false }
|
|
1299
|
+
)}
|
|
1300
|
+
/>
|
|
1301
|
+
</ItemIndicatorProvider>
|
|
1302
|
+
)
|
|
1303
|
+
}
|
|
1304
|
+
)
|
|
1243
1305
|
|
|
1244
1306
|
MenuCheckboxItem.displayName = CHECKBOX_ITEM_NAME
|
|
1245
1307
|
/* -------------------------------------------------------------------------------------------------
|
|
@@ -1274,7 +1336,7 @@ export function createBaseMenu({
|
|
|
1274
1336
|
|
|
1275
1337
|
const RADIO_ITEM_NAME = 'MenuRadioItem'
|
|
1276
1338
|
|
|
1277
|
-
const MenuRadioItem =
|
|
1339
|
+
const MenuRadioItem = _Item.styleable<ScopedProps<MenuRadioItemProps>>(
|
|
1278
1340
|
(props, forwardedRef) => {
|
|
1279
1341
|
const { value, scope = MENU_CONTEXT, ...radioItemProps } = props
|
|
1280
1342
|
const context = useRadioGroupContext(scope)
|
|
@@ -1351,6 +1413,7 @@ export function createBaseMenu({
|
|
|
1351
1413
|
<PopperPrimitive.PopperArrow
|
|
1352
1414
|
scope={scope}
|
|
1353
1415
|
componentName="PopperArrow"
|
|
1416
|
+
unstyled={unstyled}
|
|
1354
1417
|
{...(!unstyled && {
|
|
1355
1418
|
backgroundColor: '$background',
|
|
1356
1419
|
})}
|
|
@@ -1371,15 +1434,58 @@ export function createBaseMenu({
|
|
|
1371
1434
|
createStyledContext<MenuSubContextValue>()
|
|
1372
1435
|
|
|
1373
1436
|
const MenuSub: React.FC<ScopedProps<MenuSubProps>> = (props) => {
|
|
1437
|
+
const isTouchDevice = useIsTouchDevice()
|
|
1438
|
+
const { scope = MENU_CONTEXT } = props
|
|
1439
|
+
const rootContext = useMenuRootContext(scope)
|
|
1440
|
+
|
|
1441
|
+
// detect if this sub is nested inside another sub that opened to a side
|
|
1442
|
+
const parentPopperContext = PopperPrimitive.usePopperContext(scope)
|
|
1443
|
+
const parentSide = parentPopperContext.placement?.split('-')[0]
|
|
1444
|
+
const isNestedSubmenu = parentSide === 'left' || parentSide === 'right'
|
|
1445
|
+
|
|
1446
|
+
// nested submenus inherit the parent's direction so they cascade
|
|
1447
|
+
// rather than flipping back on top of the grandparent
|
|
1448
|
+
const defaultPlacement = isTouchDevice
|
|
1449
|
+
? 'bottom'
|
|
1450
|
+
: isNestedSubmenu
|
|
1451
|
+
? (`${parentSide}-start` as any)
|
|
1452
|
+
: rootContext.dir === 'rtl'
|
|
1453
|
+
? 'left-start'
|
|
1454
|
+
: 'right-start'
|
|
1374
1455
|
const {
|
|
1375
|
-
scope = MENU_CONTEXT,
|
|
1376
1456
|
children,
|
|
1377
1457
|
open = false,
|
|
1378
1458
|
onOpenChange,
|
|
1379
|
-
allowFlip = { padding: 10 },
|
|
1459
|
+
allowFlip: allowFlipProp = { padding: 10 },
|
|
1380
1460
|
stayInFrame = { padding: 10 },
|
|
1461
|
+
placement = defaultPlacement,
|
|
1381
1462
|
...rest
|
|
1382
1463
|
} = props
|
|
1464
|
+
|
|
1465
|
+
// for nested submenus, flip to opposite side (never top/bottom which overlap parent)
|
|
1466
|
+
const allowFlip = React.useMemo(() => {
|
|
1467
|
+
if (!isNestedSubmenu || typeof allowFlipProp === 'boolean') return allowFlipProp
|
|
1468
|
+
if ((allowFlipProp as any).fallbackPlacements) return allowFlipProp
|
|
1469
|
+
|
|
1470
|
+
const side = placement.split('-')[0]
|
|
1471
|
+
const align = placement.split('-')[1] || 'start'
|
|
1472
|
+
const otherAlign = align === 'start' ? 'end' : 'start'
|
|
1473
|
+
|
|
1474
|
+
if (side === 'left' || side === 'right') {
|
|
1475
|
+
const oppositeSide = side === 'right' ? 'left' : 'right'
|
|
1476
|
+
return {
|
|
1477
|
+
...(typeof allowFlipProp === 'object' ? allowFlipProp : {}),
|
|
1478
|
+
fallbackPlacements: [
|
|
1479
|
+
`${side}-${otherAlign}`,
|
|
1480
|
+
`${oppositeSide}-${align}`,
|
|
1481
|
+
`${oppositeSide}-${otherAlign}`,
|
|
1482
|
+
] as any,
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
return allowFlipProp
|
|
1487
|
+
}, [isNestedSubmenu, allowFlipProp, placement])
|
|
1488
|
+
|
|
1383
1489
|
const parentMenuContext = useMenuContext(scope)
|
|
1384
1490
|
const [trigger, setTrigger] = React.useState<MenuSubTriggerElement | null>(null)
|
|
1385
1491
|
const [content, setContent] = React.useState<MenuContentElement | null>(null)
|
|
@@ -1393,6 +1499,8 @@ export function createBaseMenu({
|
|
|
1393
1499
|
|
|
1394
1500
|
return (
|
|
1395
1501
|
<PopperPrimitive.Popper
|
|
1502
|
+
open={open}
|
|
1503
|
+
placement={placement}
|
|
1396
1504
|
allowFlip={allowFlip}
|
|
1397
1505
|
stayInFrame={stayInFrame}
|
|
1398
1506
|
{...rest}
|
|
@@ -1439,15 +1547,9 @@ export function createBaseMenu({
|
|
|
1439
1547
|
const openTimerRef = React.useRef<number | null>(null)
|
|
1440
1548
|
const { pointerGraceTimerRef, onPointerGraceIntentChange } = contentContext
|
|
1441
1549
|
|
|
1442
|
-
//
|
|
1443
|
-
//
|
|
1444
|
-
const
|
|
1445
|
-
const effectiveDir: Direction =
|
|
1446
|
-
placementSide === 'left'
|
|
1447
|
-
? 'rtl'
|
|
1448
|
-
: placementSide === 'right'
|
|
1449
|
-
? 'ltr'
|
|
1450
|
-
: rootContext.dir
|
|
1550
|
+
// keyboard navigation follows text direction, not placement side
|
|
1551
|
+
// ArrowRight always opens, ArrowLeft always closes (flipped only for RTL)
|
|
1552
|
+
const effectiveDir: Direction = rootContext.dir
|
|
1451
1553
|
|
|
1452
1554
|
const clearOpenTimer = React.useCallback(() => {
|
|
1453
1555
|
if (openTimerRef.current) window.clearTimeout(openTimerRef.current)
|
|
@@ -1673,114 +1775,113 @@ export function createBaseMenu({
|
|
|
1673
1775
|
|
|
1674
1776
|
const SUB_CONTENT_NAME = 'MenuSubContent'
|
|
1675
1777
|
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
>((props, forwardedRef) => {
|
|
1680
|
-
const scope = props.scope || MENU_CONTEXT
|
|
1681
|
-
const portalContext = usePortalContext(scope)
|
|
1682
|
-
const { forceMount = portalContext.forceMount, ...subContentProps } = props
|
|
1683
|
-
const context = useMenuContext(scope)
|
|
1684
|
-
const rootContext = useMenuRootContext(scope)
|
|
1685
|
-
const subContext = useMenuSubContext(scope)
|
|
1686
|
-
const popperContext = PopperPrimitive.usePopperContext(scope)
|
|
1687
|
-
const ref = React.useRef<MenuSubContentElement>(null)
|
|
1688
|
-
const composedRefs = useComposedRefs(forwardedRef, ref)
|
|
1778
|
+
const MenuSubContentFrame = styled(PopperPrimitive.PopperContentFrame, {
|
|
1779
|
+
name: SUB_CONTENT_NAME,
|
|
1780
|
+
})
|
|
1689
1781
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
placementSide === 'left' || placementSide === 'right'
|
|
1702
|
-
? placementSide
|
|
1703
|
-
: rootContext.dir === 'rtl'
|
|
1704
|
-
? 'left'
|
|
1705
|
-
: 'right'
|
|
1782
|
+
const MenuSubContent = MenuSubContentFrame.styleable<ScopedProps<MenuSubContentProps>>(
|
|
1783
|
+
(props, forwardedRef) => {
|
|
1784
|
+
const scope = props.scope || MENU_CONTEXT
|
|
1785
|
+
const portalContext = usePortalContext(scope)
|
|
1786
|
+
const { forceMount = portalContext.forceMount, ...subContentProps } = props
|
|
1787
|
+
const context = useMenuContext(scope)
|
|
1788
|
+
const rootContext = useMenuRootContext(scope)
|
|
1789
|
+
const subContext = useMenuSubContext(scope)
|
|
1790
|
+
const popperContext = PopperPrimitive.usePopperContext(scope)
|
|
1791
|
+
const ref = React.useRef<MenuSubContentElement>(null)
|
|
1792
|
+
const composedRefs = useComposedRefs(forwardedRef, ref)
|
|
1706
1793
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
placementSide
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1794
|
+
// determine side from actual placement, not just RTL direction
|
|
1795
|
+
// placement like "left-start" or "right-end" - extract the side
|
|
1796
|
+
const placementSide = popperContext.placement?.split('-')[0] as
|
|
1797
|
+
| 'left'
|
|
1798
|
+
| 'right'
|
|
1799
|
+
| 'top'
|
|
1800
|
+
| 'bottom'
|
|
1801
|
+
| undefined
|
|
1802
|
+
// for submenus, we care about horizontal placement (left/right)
|
|
1803
|
+
// default to 'right' for LTR, 'left' for RTL
|
|
1804
|
+
const dataSide: Side =
|
|
1805
|
+
placementSide === 'left' || placementSide === 'right'
|
|
1806
|
+
? placementSide
|
|
1807
|
+
: rootContext.dir === 'rtl'
|
|
1808
|
+
? 'left'
|
|
1809
|
+
: 'right'
|
|
1810
|
+
|
|
1811
|
+
// keyboard navigation follows text direction, not placement side
|
|
1812
|
+
const effectiveDir: Direction = rootContext.dir
|
|
1714
1813
|
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
event.preventDefault()
|
|
1738
|
-
}}
|
|
1739
|
-
// The menu might close because of focusing another menu item in the parent menu. We
|
|
1740
|
-
// don't want it to refocus the trigger in that case so we handle trigger focus ourselves.
|
|
1741
|
-
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
1742
|
-
onFocusOutside={composeEventHandlers(props.onFocusOutside, (event) => {
|
|
1743
|
-
// We prevent closing when the trigger is focused to avoid triggering a re-open animation
|
|
1744
|
-
// on pointer interaction.
|
|
1745
|
-
if (event.target !== subContext.trigger) context.onOpenChange(false)
|
|
1746
|
-
})}
|
|
1747
|
-
onEscapeKeyDown={composeEventHandlers(props.onEscapeKeyDown, (event) => {
|
|
1748
|
-
// close only this submenu, not the root menu
|
|
1749
|
-
context.onOpenChange(false)
|
|
1750
|
-
// return focus to the submenu trigger with focusVisible since this is keyboard navigation
|
|
1751
|
-
// @ts-ignore focusVisible is a newer API
|
|
1752
|
-
subContext.trigger?.focus({ focusVisible: true })
|
|
1753
|
-
// ensure pressing escape in submenu doesn't escape full screen mode
|
|
1754
|
-
event.preventDefault()
|
|
1755
|
-
})}
|
|
1756
|
-
{...(isWeb
|
|
1757
|
-
? {
|
|
1758
|
-
onKeyDown: composeEventHandlers(props.onKeyDown, (event) => {
|
|
1759
|
-
// Submenu key events bubble through portals. We only care about keys in this menu.
|
|
1760
|
-
// @ts-ignore
|
|
1761
|
-
const isKeyDownInside = event.currentTarget.contains(
|
|
1762
|
-
event.target as HTMLElement
|
|
1763
|
-
)
|
|
1764
|
-
// use effectiveDir so arrow keys match the submenu's actual position
|
|
1765
|
-
// (e.g., ArrowRight closes a left-side submenu)
|
|
1766
|
-
const isCloseKey = SUB_CLOSE_KEYS[effectiveDir].includes(event.key)
|
|
1767
|
-
if (isKeyDownInside && isCloseKey) {
|
|
1768
|
-
context.onOpenChange(false)
|
|
1769
|
-
// We focus manually because we prevented it in `onCloseAutoFocus`
|
|
1770
|
-
// use focusVisible: true since this is keyboard navigation
|
|
1771
|
-
// @ts-ignore focusVisible is a newer API
|
|
1772
|
-
subContext.trigger?.focus({ focusVisible: true })
|
|
1773
|
-
// prevent window from scrolling
|
|
1774
|
-
event.preventDefault()
|
|
1775
|
-
}
|
|
1776
|
-
}),
|
|
1814
|
+
return (
|
|
1815
|
+
<Collection.Provider scope={scope}>
|
|
1816
|
+
<Collection.Slot scope={scope}>
|
|
1817
|
+
<MenuContentImpl
|
|
1818
|
+
id={subContext.contentId}
|
|
1819
|
+
aria-labelledby={subContext.triggerId}
|
|
1820
|
+
{...subContentProps}
|
|
1821
|
+
ref={composedRefs}
|
|
1822
|
+
data-side={dataSide}
|
|
1823
|
+
disableOutsidePointerEvents={false}
|
|
1824
|
+
disableOutsideScroll={false}
|
|
1825
|
+
trapFocus={false}
|
|
1826
|
+
onOpenAutoFocus={(event) => {
|
|
1827
|
+
// when opening a submenu, focus content for keyboard users only
|
|
1828
|
+
if (rootContext.isUsingKeyboardRef.current) {
|
|
1829
|
+
// scope query to this submenu's subtree so nested submenus
|
|
1830
|
+
// don't accidentally focus a sibling/parent submenu content
|
|
1831
|
+
const root = ref.current as unknown as HTMLElement
|
|
1832
|
+
const content = root?.querySelector?.(
|
|
1833
|
+
'[data-tamagui-menu-content]'
|
|
1834
|
+
) as HTMLElement | null
|
|
1835
|
+
;(content || root)?.focus({ preventScroll: true })
|
|
1777
1836
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1837
|
+
event.preventDefault()
|
|
1838
|
+
}}
|
|
1839
|
+
// The menu might close because of focusing another menu item in the parent menu. We
|
|
1840
|
+
// don't want it to refocus the trigger in that case so we handle trigger focus ourselves.
|
|
1841
|
+
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
1842
|
+
onFocusOutside={composeEventHandlers(props.onFocusOutside, (event) => {
|
|
1843
|
+
// We prevent closing when the trigger is focused to avoid triggering a re-open animation
|
|
1844
|
+
// on pointer interaction.
|
|
1845
|
+
if (event.target !== subContext.trigger) context.onOpenChange(false)
|
|
1846
|
+
})}
|
|
1847
|
+
onEscapeKeyDown={composeEventHandlers(props.onEscapeKeyDown, (event) => {
|
|
1848
|
+
// close only this submenu, not the root menu
|
|
1849
|
+
context.onOpenChange(false)
|
|
1850
|
+
// return focus to the submenu trigger with focusVisible since this is keyboard navigation
|
|
1851
|
+
// @ts-ignore focusVisible is a newer API
|
|
1852
|
+
subContext.trigger?.focus({ focusVisible: true })
|
|
1853
|
+
// ensure pressing escape in submenu doesn't escape full screen mode
|
|
1854
|
+
event.preventDefault()
|
|
1855
|
+
})}
|
|
1856
|
+
{...(isWeb
|
|
1857
|
+
? {
|
|
1858
|
+
onKeyDown: composeEventHandlers(props.onKeyDown, (event) => {
|
|
1859
|
+
// Submenu key events bubble through portals. We only care about keys in this menu.
|
|
1860
|
+
// @ts-ignore
|
|
1861
|
+
const isKeyDownInside = event.currentTarget.contains(
|
|
1862
|
+
event.target as HTMLElement
|
|
1863
|
+
)
|
|
1864
|
+
// use effectiveDir so arrow keys match the submenu's actual position
|
|
1865
|
+
// (e.g., ArrowRight closes a left-side submenu)
|
|
1866
|
+
const isCloseKey = SUB_CLOSE_KEYS[effectiveDir].includes(event.key)
|
|
1867
|
+
if (isKeyDownInside && isCloseKey) {
|
|
1868
|
+
context.onOpenChange(false)
|
|
1869
|
+
// We focus manually because we prevented it in `onCloseAutoFocus`
|
|
1870
|
+
// use focusVisible: true since this is keyboard navigation
|
|
1871
|
+
// @ts-ignore focusVisible is a newer API
|
|
1872
|
+
subContext.trigger?.focus({ focusVisible: true })
|
|
1873
|
+
// prevent window from scrolling
|
|
1874
|
+
event.preventDefault()
|
|
1875
|
+
}
|
|
1876
|
+
}),
|
|
1877
|
+
}
|
|
1878
|
+
: null)}
|
|
1879
|
+
/>
|
|
1880
|
+
</Collection.Slot>
|
|
1881
|
+
</Collection.Provider>
|
|
1882
|
+
)
|
|
1883
|
+
}
|
|
1884
|
+
)
|
|
1784
1885
|
|
|
1785
1886
|
MenuSubContent.displayName = SUB_CONTENT_NAME
|
|
1786
1887
|
|
|
@@ -1860,7 +1961,7 @@ function focusFirst(candidates: HTMLElement[], options?: { focusVisible?: boolea
|
|
|
1860
1961
|
// if focus is already where we want to go, we don't want to keep going through the candidates
|
|
1861
1962
|
if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return
|
|
1862
1963
|
// @ts-ignore focusVisible is a newer API not yet in all TS libs
|
|
1863
|
-
candidate.focus({ focusVisible: options?.focusVisible })
|
|
1964
|
+
candidate.focus({ preventScroll: true, focusVisible: options?.focusVisible })
|
|
1864
1965
|
if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return
|
|
1865
1966
|
}
|
|
1866
1967
|
}
|