@multiplatform.one/forms 7.48.0 → 7.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/cjs/AdaptivePopup/index.cjs +13 -7
  2. package/dist/cjs/AdaptivePopup/index.native.js +13 -7
  3. package/dist/cjs/AdaptivePopup/index.native.js.map +1 -1
  4. package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.cjs +3 -1
  5. package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js +3 -1
  6. package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
  7. package/dist/cjs/FloatingPanel/index.cjs +21 -10
  8. package/dist/cjs/FloatingPanel/index.native.js +116 -110
  9. package/dist/cjs/FloatingPanel/index.native.js.map +1 -1
  10. package/dist/cjs/fields/Combobox/index.cjs +3 -2
  11. package/dist/cjs/fields/Combobox/index.native.js +3 -2
  12. package/dist/cjs/fields/Combobox/index.native.js.map +1 -1
  13. package/dist/cjs/fields/Geolocation/index.cjs +3 -2
  14. package/dist/cjs/fields/Geolocation/index.native.js +3 -2
  15. package/dist/cjs/fields/Geolocation/index.native.js.map +1 -1
  16. package/dist/cjs/fields/MentionInput/index.cjs +3 -2
  17. package/dist/cjs/fields/MentionInput/index.native.js +3 -2
  18. package/dist/cjs/fields/MentionInput/index.native.js.map +1 -1
  19. package/dist/cjs/fields/Select/index.cjs +3 -2
  20. package/dist/cjs/fields/Select/index.native.js +3 -2
  21. package/dist/cjs/fields/Select/index.native.js.map +1 -1
  22. package/dist/cjs/index.cjs +9 -0
  23. package/dist/cjs/index.native.js +9 -0
  24. package/dist/cjs/index.native.js.map +1 -1
  25. package/dist/cjs/shared/OverlayWindow.cjs +38 -0
  26. package/dist/cjs/shared/OverlayWindow.native.js +54 -0
  27. package/dist/cjs/shared/OverlayWindow.native.js.map +1 -0
  28. package/dist/cjs/shared/overlayStack.cjs +92 -0
  29. package/dist/cjs/shared/overlayStack.native.js +97 -0
  30. package/dist/cjs/shared/overlayStack.native.js.map +1 -0
  31. package/dist/esm/AdaptivePopup/index.mjs +13 -7
  32. package/dist/esm/AdaptivePopup/index.mjs.map +1 -1
  33. package/dist/esm/AdaptivePopup/index.native.js +13 -7
  34. package/dist/esm/AdaptivePopup/index.native.js.map +1 -1
  35. package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.mjs +3 -1
  36. package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.mjs.map +1 -1
  37. package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js +3 -1
  38. package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
  39. package/dist/esm/FloatingPanel/index.mjs +21 -10
  40. package/dist/esm/FloatingPanel/index.mjs.map +1 -1
  41. package/dist/esm/FloatingPanel/index.native.js +116 -110
  42. package/dist/esm/FloatingPanel/index.native.js.map +1 -1
  43. package/dist/esm/fields/Combobox/index.mjs +3 -2
  44. package/dist/esm/fields/Combobox/index.mjs.map +1 -1
  45. package/dist/esm/fields/Combobox/index.native.js +3 -2
  46. package/dist/esm/fields/Combobox/index.native.js.map +1 -1
  47. package/dist/esm/fields/Geolocation/index.mjs +3 -2
  48. package/dist/esm/fields/Geolocation/index.mjs.map +1 -1
  49. package/dist/esm/fields/Geolocation/index.native.js +3 -2
  50. package/dist/esm/fields/Geolocation/index.native.js.map +1 -1
  51. package/dist/esm/fields/MentionInput/index.mjs +3 -2
  52. package/dist/esm/fields/MentionInput/index.mjs.map +1 -1
  53. package/dist/esm/fields/MentionInput/index.native.js +3 -2
  54. package/dist/esm/fields/MentionInput/index.native.js.map +1 -1
  55. package/dist/esm/fields/Select/index.mjs +3 -2
  56. package/dist/esm/fields/Select/index.mjs.map +1 -1
  57. package/dist/esm/fields/Select/index.native.js +3 -2
  58. package/dist/esm/fields/Select/index.native.js.map +1 -1
  59. package/dist/esm/index.mjs +3 -1
  60. package/dist/esm/index.mjs.map +1 -1
  61. package/dist/esm/index.native.js +3 -1
  62. package/dist/esm/index.native.js.map +1 -1
  63. package/dist/esm/shared/OverlayWindow.mjs +13 -0
  64. package/dist/esm/shared/OverlayWindow.mjs.map +1 -0
  65. package/dist/esm/shared/OverlayWindow.native.js +26 -0
  66. package/dist/esm/shared/OverlayWindow.native.js.map +1 -0
  67. package/dist/esm/shared/overlayStack.mjs +62 -0
  68. package/dist/esm/shared/overlayStack.mjs.map +1 -0
  69. package/dist/esm/shared/overlayStack.native.js +64 -0
  70. package/dist/esm/shared/overlayStack.native.js.map +1 -0
  71. package/dist/jsx/AdaptivePopup/index.mjs +13 -7
  72. package/dist/jsx/AdaptivePopup/index.mjs.map +1 -1
  73. package/dist/jsx/AdaptivePopup/index.native.js +13 -7
  74. package/dist/jsx/AdaptivePopup/index.native.js.map +1 -1
  75. package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.mjs +3 -1
  76. package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.mjs.map +1 -1
  77. package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js +3 -1
  78. package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
  79. package/dist/jsx/FloatingPanel/index.mjs +21 -10
  80. package/dist/jsx/FloatingPanel/index.mjs.map +1 -1
  81. package/dist/jsx/FloatingPanel/index.native.js +116 -110
  82. package/dist/jsx/FloatingPanel/index.native.js.map +1 -1
  83. package/dist/jsx/fields/Combobox/index.mjs +3 -2
  84. package/dist/jsx/fields/Combobox/index.mjs.map +1 -1
  85. package/dist/jsx/fields/Combobox/index.native.js +3 -2
  86. package/dist/jsx/fields/Combobox/index.native.js.map +1 -1
  87. package/dist/jsx/fields/Geolocation/index.mjs +3 -2
  88. package/dist/jsx/fields/Geolocation/index.mjs.map +1 -1
  89. package/dist/jsx/fields/Geolocation/index.native.js +3 -2
  90. package/dist/jsx/fields/Geolocation/index.native.js.map +1 -1
  91. package/dist/jsx/fields/MentionInput/index.mjs +3 -2
  92. package/dist/jsx/fields/MentionInput/index.mjs.map +1 -1
  93. package/dist/jsx/fields/MentionInput/index.native.js +3 -2
  94. package/dist/jsx/fields/MentionInput/index.native.js.map +1 -1
  95. package/dist/jsx/fields/Select/index.mjs +3 -2
  96. package/dist/jsx/fields/Select/index.mjs.map +1 -1
  97. package/dist/jsx/fields/Select/index.native.js +3 -2
  98. package/dist/jsx/fields/Select/index.native.js.map +1 -1
  99. package/dist/jsx/index.js +3 -1
  100. package/dist/jsx/index.js.map +1 -1
  101. package/dist/jsx/index.mjs +3 -1
  102. package/dist/jsx/index.mjs.map +1 -1
  103. package/dist/jsx/index.native.js +9 -0
  104. package/dist/jsx/index.native.js.map +1 -1
  105. package/dist/jsx/shared/OverlayWindow.mjs +13 -0
  106. package/dist/jsx/shared/OverlayWindow.mjs.map +1 -0
  107. package/dist/jsx/shared/OverlayWindow.native.js +54 -0
  108. package/dist/jsx/shared/OverlayWindow.native.js.map +1 -0
  109. package/dist/jsx/shared/overlayStack.mjs +62 -0
  110. package/dist/jsx/shared/overlayStack.mjs.map +1 -0
  111. package/dist/jsx/shared/overlayStack.native.js +97 -0
  112. package/dist/jsx/shared/overlayStack.native.js.map +1 -0
  113. package/package.json +8 -8
  114. package/src/AdaptivePopup/index.tsx +11 -7
  115. package/src/ContextualSaveBar/UnsavedChangesDialog.tsx +5 -2
  116. package/src/FloatingPanel/index.native.tsx +102 -97
  117. package/src/FloatingPanel/index.tsx +17 -12
  118. package/src/fields/Combobox/index.tsx +3 -2
  119. package/src/fields/Geolocation/index.tsx +3 -2
  120. package/src/fields/MentionInput/index.tsx +3 -2
  121. package/src/fields/Select/index.tsx +3 -2
  122. package/src/index.ts +11 -0
  123. package/src/shared/OverlayWindow.native.tsx +28 -0
  124. package/src/shared/OverlayWindow.tsx +17 -0
  125. package/src/shared/overlayStack.spec.tsx +59 -0
  126. package/src/shared/overlayStack.tsx +102 -0
  127. package/types/AdaptivePopup/index.d.ts.map +1 -1
  128. package/types/ContextualSaveBar/UnsavedChangesDialog.d.ts.map +1 -1
  129. package/types/FloatingPanel/index.native.d.ts.map +1 -1
  130. package/types/fields/Combobox/index.d.ts.map +1 -1
  131. package/types/fields/Geolocation/index.d.ts.map +1 -1
  132. package/types/fields/MentionInput/index.d.ts.map +1 -1
  133. package/types/fields/Select/index.d.ts.map +1 -1
  134. package/types/index.d.ts +4 -0
  135. package/types/index.d.ts.map +1 -1
  136. package/types/shared/OverlayWindow.d.ts +13 -0
  137. package/types/shared/OverlayWindow.d.ts.map +1 -0
  138. package/types/shared/OverlayWindow.native.d.ts +15 -0
  139. package/types/shared/OverlayWindow.native.d.ts.map +1 -0
  140. package/types/shared/overlayStack.d.ts +42 -0
  141. package/types/shared/overlayStack.d.ts.map +1 -0
@@ -83,6 +83,8 @@ import {
83
83
  motionDurationMs,
84
84
  } from "@multiplatform.one/theme";
85
85
  import { View, YStack } from "tamagui";
86
+ import { useOverlayZIndex } from "../shared/overlayStack";
87
+ import { OverlayWindow } from "../shared/OverlayWindow";
86
88
  import { t } from "../shared/t";
87
89
  import { useKeyboardInset } from "../shared/useKeyboardInset";
88
90
  import type { FloatingPanelProps } from "./index";
@@ -143,6 +145,7 @@ export function FloatingPanel({
143
145
  }: FloatingPanelProps) {
144
146
  // The panel floats over the page: it never inherits a painted ancestor.
145
147
  const { knobProps } = useResolvedKnobs({ painted: false });
148
+ const windowZIndex = useOverlayZIndex("sheet");
146
149
  const children = <Surface painted={false}>{panelChildren}</Surface>;
147
150
  const slideTransition = knobProps.motion.slide.transition;
148
151
  const enterDuration = motionDurationMs(slideTransition);
@@ -522,118 +525,120 @@ export function FloatingPanel({
522
525
  onNativeShow?.();
523
526
  }}
524
527
  >
525
- {atPoint && pressPoint ? (
526
- <PointPanel
527
- key={`${pressPoint.x},${pressPoint.y}`}
528
- point={pressPoint}
529
- bounds={{
530
- top: topInset + panelViewportPadding,
531
- left: (safeArea?.left ?? 0) + panelViewportPadding,
532
- right: windowWidth - (safeArea?.right ?? 0) - panelViewportPadding,
533
- bottom:
534
- windowHeight -
535
- Math.max(safeArea?.bottom ?? 0, keyboardInset) -
536
- panelViewportPadding,
537
- }}
538
- framePadding={framePadding}
539
- frameGap={frameGap}
540
- onBackdropPress={handleBackdropPress}
541
- onEscape={close}
542
- >
543
- {header}
544
- {children}
545
- </PointPanel>
546
- ) : (
547
- /*
528
+ <OverlayWindow zIndex={windowZIndex}>
529
+ {atPoint && pressPoint ? (
530
+ <PointPanel
531
+ key={`${pressPoint.x},${pressPoint.y}`}
532
+ point={pressPoint}
533
+ bounds={{
534
+ top: topInset + panelViewportPadding,
535
+ left: (safeArea?.left ?? 0) + panelViewportPadding,
536
+ right: windowWidth - (safeArea?.right ?? 0) - panelViewportPadding,
537
+ bottom:
538
+ windowHeight -
539
+ Math.max(safeArea?.bottom ?? 0, keyboardInset) -
540
+ panelViewportPadding,
541
+ }}
542
+ framePadding={framePadding}
543
+ frameGap={frameGap}
544
+ onBackdropPress={handleBackdropPress}
545
+ onEscape={close}
546
+ >
547
+ {header}
548
+ {children}
549
+ </PointPanel>
550
+ ) : (
551
+ /*
548
552
  A plain padded column, not KeyboardAvoidingView: inside a Modal the
549
553
  latter pads the container while the sheet keeps its own fixed height,
550
554
  so the sheet overflows off the TOP instead of shrinking (MPO-64).
551
555
  */
552
- <RNView style={{ flex: 1, justifyContent: "flex-end", paddingBottom: keyboardInset }}>
553
- <Pressable
554
- style={StyleSheet.absoluteFill}
555
- onPress={handleBackdropPress}
556
- accessibilityRole="button"
557
- accessibilityLabel={t("Close")}
558
- >
559
- <View flex={1} backgroundColor="$shadow6" />
560
- </Pressable>
561
- {/*
556
+ <RNView style={{ flex: 1, justifyContent: "flex-end", paddingBottom: keyboardInset }}>
557
+ <Pressable
558
+ style={StyleSheet.absoluteFill}
559
+ onPress={handleBackdropPress}
560
+ accessibilityRole="button"
561
+ accessibilityLabel={t("Close")}
562
+ >
563
+ <View flex={1} backgroundColor="$shadow6" />
564
+ </Pressable>
565
+ {/*
562
566
  Sheet sits above the absoluteFill backdrop in z-order (later sibling).
563
567
  Do NOT claim the responder on a wrapper here — that steals presses
564
568
  from option rows / calendar days and blocks commit.
565
569
  */}
566
- <Animated.View
567
- testID="floating-panel-sheet"
568
- onLayout={(e) => {
569
- // Travel distance for a dismiss, and the basis for the detents.
570
- // A content-sized picker is shorter than maxHeight, and sliding
571
- // it by maxHeight would overshoot into a visible lag.
572
- if (restHeightRef.current !== null) return;
573
- const h = Math.round(e.nativeEvent.layout.height);
574
- if (h <= 0) return;
575
- naturalRef.current = h;
576
- filledRef.current = h >= maxHeightRef.current - 1;
577
- if (h !== measuredHeight) setMeasuredHeight(h);
578
- }}
579
- style={{
580
- // MPO-64: a flex child defaults to flexShrink 0, so without this
581
- // the sheet cannot give way to the keyboard at all.
582
- flexShrink: 1,
583
- minHeight: 0,
584
- maxHeight: frameCap,
585
- transform: [{ translateY }],
586
- }}
587
- >
588
- <YStack
589
- {...knobProps.elevatedSurface}
590
- borderBottomLeftRadius={0}
591
- borderBottomRightRadius={0}
592
- maxHeight={maxHeight}
593
- flexShrink={1}
594
- minHeight={0}
595
- // sheetFill opens at full sheet height (flexShrink keeps the
596
- // keyboard from pushing the top of the sheet off-screen).
597
- {...(sheetFill ? { height: maxHeight } : undefined)}
598
- paddingTop="$2"
599
- paddingBottom={sheetBottomPadding}
600
- // VoiceOver escape gesture (two-finger Z) dismisses the sheet.
601
- onAccessibilityEscape={close}
570
+ <Animated.View
571
+ testID="floating-panel-sheet"
572
+ onLayout={(e) => {
573
+ // Travel distance for a dismiss, and the basis for the detents.
574
+ // A content-sized picker is shorter than maxHeight, and sliding
575
+ // it by maxHeight would overshoot into a visible lag.
576
+ if (restHeightRef.current !== null) return;
577
+ const h = Math.round(e.nativeEvent.layout.height);
578
+ if (h <= 0) return;
579
+ naturalRef.current = h;
580
+ filledRef.current = h >= maxHeightRef.current - 1;
581
+ if (h !== measuredHeight) setMeasuredHeight(h);
582
+ }}
583
+ style={{
584
+ // MPO-64: a flex child defaults to flexShrink 0, so without this
585
+ // the sheet cannot give way to the keyboard at all.
586
+ flexShrink: 1,
587
+ minHeight: 0,
588
+ maxHeight: frameCap,
589
+ transform: [{ translateY }],
590
+ }}
602
591
  >
603
- {/*
592
+ <YStack
593
+ {...knobProps.elevatedSurface}
594
+ borderBottomLeftRadius={0}
595
+ borderBottomRightRadius={0}
596
+ maxHeight={maxHeight}
597
+ flexShrink={1}
598
+ minHeight={0}
599
+ // sheetFill opens at full sheet height (flexShrink keeps the
600
+ // keyboard from pushing the top of the sheet off-screen).
601
+ {...(sheetFill ? { height: maxHeight } : undefined)}
602
+ paddingTop="$2"
603
+ paddingBottom={sheetBottomPadding}
604
+ // VoiceOver escape gesture (two-finger Z) dismisses the sheet.
605
+ onAccessibilityEscape={close}
606
+ >
607
+ {/*
604
608
  The grab ZONE, not just the 4pt pill: a 4pt tall target is
605
609
  unhittable. The gesture lives here rather than on the whole
606
610
  sheet so it never fights the ScrollView inside it, which is the
607
611
  same division iOS uses.
608
612
  */}
609
- <RNView
610
- {...panResponder.panHandlers}
611
- accessibilityRole="adjustable"
612
- accessibilityLabel={t("Drag to resize")}
613
- style={{
614
- alignSelf: "stretch",
615
- alignItems: "center",
616
- paddingTop: 4,
617
- paddingBottom: 12,
618
- }}
619
- >
620
- <View width={40} height={4} borderRadius={2} backgroundColor="$color8" />
621
- </RNView>
622
- <YStack
623
- padding={framePadding}
624
- gap={frameGap}
625
- flexShrink={1}
626
- {...(sheetFill ? { flex: 1, minHeight: 0 } : undefined)}
627
- >
628
- <Surface size="$true">
629
- {header}
630
- {content}
631
- </Surface>
613
+ <RNView
614
+ {...panResponder.panHandlers}
615
+ accessibilityRole="adjustable"
616
+ accessibilityLabel={t("Drag to resize")}
617
+ style={{
618
+ alignSelf: "stretch",
619
+ alignItems: "center",
620
+ paddingTop: 4,
621
+ paddingBottom: 12,
622
+ }}
623
+ >
624
+ <View width={40} height={4} borderRadius={2} backgroundColor="$color8" />
625
+ </RNView>
626
+ <YStack
627
+ padding={framePadding}
628
+ gap={frameGap}
629
+ flexShrink={1}
630
+ {...(sheetFill ? { flex: 1, minHeight: 0 } : undefined)}
631
+ >
632
+ <Surface size="$true">
633
+ {header}
634
+ {content}
635
+ </Surface>
636
+ </YStack>
632
637
  </YStack>
633
- </YStack>
634
- </Animated.View>
635
- </RNView>
636
- )}
638
+ </Animated.View>
639
+ </RNView>
640
+ )}
641
+ </OverlayWindow>
637
642
  </Modal>
638
643
  </>
639
644
  );
@@ -28,7 +28,7 @@ import { useReturnFocusOnClose, useSheetModalFocus } from "../shared/focusManage
28
28
  import { useSheetEscapeDismiss } from "../shared/useSheetEscapeDismiss";
29
29
  import { useFloatingPanel, panelViewportPadding } from "./useFloatingPanel";
30
30
  import type { PanelWidthMode } from "./useFloatingPanel";
31
- import { zIndex } from "../shared/zIndex";
31
+ import { OverlayStackProvider, useOverlayZIndex } from "../shared/overlayStack";
32
32
  import { isNestedPanelAction, type PanelActionEvent } from "./nestedAction";
33
33
  import { PanelPortal } from "./PanelPortal";
34
34
  import {
@@ -490,6 +490,7 @@ function FloatingPanelSheet({
490
490
  escapeTarget = "window",
491
491
  }: FloatingPanelProps & { escapeTarget?: "window" | "document" }) {
492
492
  const { knobProps } = useResolvedKnobs({ painted: false });
493
+ const sheetZIndex = useOverlayZIndex("sheet");
493
494
  // LC-24 / A-ENTEREXIT: honor animation=none (undefined) — do not hardcode "medium".
494
495
  const slideMotion = {
495
496
  ...knobProps.motion.slide,
@@ -585,7 +586,8 @@ function FloatingPanelSheet({
585
586
  dismissOnSnapToBottom
586
587
  position={position}
587
588
  onPositionChange={setPosition}
588
- zIndex={zIndex.sheet}
589
+ zIndex={sheetZIndex}
590
+ portalProps={{ zIndex: sheetZIndex }}
589
591
  {...transitionProps(slideMotion)}
590
592
  >
591
593
  <Sheet.Overlay
@@ -608,14 +610,16 @@ function FloatingPanelSheet({
608
610
  padding={framePadding}
609
611
  gap={frameGap}
610
612
  >
611
- {header}
612
- {scrollable ? (
613
- <Sheet.ScrollView flex={header ? 1 : undefined} {...scrollViewProps}>
614
- {children}
615
- </Sheet.ScrollView>
616
- ) : (
617
- children
618
- )}
613
+ <OverlayStackProvider zIndex={sheetZIndex}>
614
+ {header}
615
+ {scrollable ? (
616
+ <Sheet.ScrollView flex={header ? 1 : undefined} {...scrollViewProps}>
617
+ {children}
618
+ </Sheet.ScrollView>
619
+ ) : (
620
+ children
621
+ )}
622
+ </OverlayStackProvider>
619
623
  </Sheet.Frame>
620
624
  </Sheet>
621
625
  </>
@@ -642,6 +646,7 @@ function FloatingPanelWeb({
642
646
  anchorRect,
643
647
  ...sheetProps
644
648
  }: FloatingPanelProps) {
649
+ const panelZIndex = useOverlayZIndex("dropdown");
645
650
  const [collisionSheet, setCollisionSheet] = useState(false);
646
651
  const isFill = sizing === "fill";
647
652
  const isContextMenu = openOn === "contextmenu";
@@ -1193,7 +1198,7 @@ function FloatingPanelWeb({
1193
1198
  top: overlayStyleRef.current?.top ?? panel.y ?? 0,
1194
1199
  left: overlayStyleRef.current?.left ?? panel.x ?? 0,
1195
1200
  scrollbarWidth: "none" as any,
1196
- zIndex: zIndex.dropdown,
1201
+ zIndex: panelZIndex,
1197
1202
  overflow: "auto",
1198
1203
  ...(cover && !isContextMenu ? { overflowX: "hidden" as const } : {}),
1199
1204
  ...panel.floatingStyleRef.current,
@@ -1261,7 +1266,7 @@ function FloatingPanelWeb({
1261
1266
  {...(contentPadding !== "none" ? panel.knobProps.panelPadding : undefined)}
1262
1267
  {...(contentPadding !== "none" ? panel.knobProps.gapLg : undefined)}
1263
1268
  >
1264
- {children}
1269
+ <OverlayStackProvider zIndex={panelZIndex}>{children}</OverlayStackProvider>
1265
1270
  </View>
1266
1271
  <ScrollArrow
1267
1272
  direction="down"
@@ -67,7 +67,7 @@ import {
67
67
  useViewportGtSm,
68
68
  } from "../../FloatingPanel";
69
69
  import { PanelPortal } from "../../FloatingPanel/PanelPortal";
70
- import { zIndex } from "../../shared/zIndex";
70
+ import { useOverlayZIndex } from "../../shared/overlayStack";
71
71
  import { useDragScroll } from "../../shared/useDragScroll";
72
72
  import { useIsInTableCell } from "../../shared/tableCellContext";
73
73
  import { orderOptionsBySelected, type SelectedOrder } from "../selectedOrder";
@@ -518,6 +518,7 @@ function FloatingComboboxCore({
518
518
  open: openProp,
519
519
  onOpenChange,
520
520
  }: FloatingComboboxCoreProps) {
521
+ const overlayZIndex = useOverlayZIndex("dropdown");
521
522
  const { style: elevatedSurfaceStyle, ...elevatedSurfaceProps } = knobProps.elevatedSurface;
522
523
  const hydrationTouch = useTouchSurface();
523
524
  if (isWeb) ensureKeyboardModalityTracking();
@@ -1498,7 +1499,7 @@ function FloatingComboboxCore({
1498
1499
  position: "fixed",
1499
1500
  top: 0,
1500
1501
  left: 0,
1501
- zIndex: zIndex.dropdown,
1502
+ zIndex: overlayZIndex,
1502
1503
  overflow: "hidden",
1503
1504
  display: "flex",
1504
1505
  flexDirection: "column",
@@ -32,7 +32,7 @@ import { formCommonColors, formInputColors } from "../../shared/colorRamps";
32
32
  import { t } from "../../shared/t";
33
33
  import { pressSlopProps } from "../../shared/pressSlopProps";
34
34
  import { useIsInTableCell } from "../../shared/tableCellContext";
35
- import { zIndex } from "../../shared/zIndex";
35
+ import { useOverlayZIndex } from "../../shared/overlayStack";
36
36
  import {
37
37
  clampRadiusForLargeComponent,
38
38
  getFieldError,
@@ -878,6 +878,7 @@ export function Geolocation({
878
878
  reverseGeocode: reverseGeocodeProp,
879
879
  geocoder,
880
880
  }: GeolocationProps) {
881
+ const overlayZIndex = useOverlayZIndex("dropdown");
881
882
  const hydrationTouch = useTouchSurface();
882
883
  const { resolvedForm, knobProps, id } = useFormField({ form: formProp, id: idProp });
883
884
  const resolvedValidators = useResolvedValidators(required, validators, label, name);
@@ -1432,7 +1433,7 @@ export function Geolocation({
1432
1433
  top="100%"
1433
1434
  left={0}
1434
1435
  right={0}
1435
- zIndex={zIndex.dropdown}
1436
+ zIndex={overlayZIndex}
1436
1437
  {...knobProps.elevatedSurface}
1437
1438
  overflow="hidden"
1438
1439
  >
@@ -23,7 +23,7 @@ import {
23
23
  import { formCommonColors, formInputColors } from "../../shared/colorRamps";
24
24
  import { ScrollArrow } from "../../shared/floatingList";
25
25
  import { Skeleton } from "../../Skeleton";
26
- import { zIndex } from "../../shared/zIndex";
26
+ import { useOverlayZIndex } from "../../shared/overlayStack";
27
27
  import { useFloatingPanel } from "../../FloatingPanel";
28
28
  import { PanelPortal } from "../../FloatingPanel/PanelPortal";
29
29
  import { Field, FieldLayout } from "../../fieldLayout";
@@ -240,6 +240,7 @@ export function MentionInput<
240
240
  TFormValidator extends Validator<TParentData, unknown> | undefined = undefined,
241
241
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
242
242
  >(props: MentionInputProps<TParentData, TName, TFieldValidator, TFormValidator, TData>) {
243
+ const overlayZIndex = useOverlayZIndex("dropdown");
243
244
  const hydrationTouch = useTouchSurface();
244
245
  const {
245
246
  defaultValue,
@@ -807,7 +808,7 @@ export function MentionInput<
807
808
  top: panel.y ?? 0,
808
809
  left: panel.x ?? 0,
809
810
  scrollbarWidth: "none" as any,
810
- zIndex: zIndex.dropdown,
811
+ zIndex: overlayZIndex,
811
812
  overflow: "auto",
812
813
  opacity: panel.visible ? 1 : 0,
813
814
  transform: panel.visible ? "scale(1)" : "scale(0.96)",
@@ -55,7 +55,7 @@ import {
55
55
  useViewportGtSm,
56
56
  } from "../../FloatingPanel";
57
57
  import { PanelPortal } from "../../FloatingPanel/PanelPortal";
58
- import { zIndex } from "../../shared/zIndex";
58
+ import { useOverlayZIndex } from "../../shared/overlayStack";
59
59
  import { useDragScroll } from "../../shared/useDragScroll";
60
60
  import { useIsInTableCell } from "../../shared/tableCellContext";
61
61
  import { t } from "../../shared/t";
@@ -512,6 +512,7 @@ function FloatingSelectCore({
512
512
  selectedOrder,
513
513
  dismissible,
514
514
  }: FloatingSelectCoreProps) {
515
+ const overlayZIndex = useOverlayZIndex("dropdown");
515
516
  const { style: elevatedSurfaceStyle, ...elevatedSurfaceProps } = knobProps.elevatedSurface;
516
517
  const hydrationTouch = useTouchSurface();
517
518
  const inTableCell = useIsInTableCell();
@@ -1113,7 +1114,7 @@ function FloatingSelectCore({
1113
1114
  top: overlayStyle.current?.top ?? panel.y ?? 0,
1114
1115
  left: panel.x ?? 0,
1115
1116
  scrollbarWidth: "none" as any,
1116
- zIndex: zIndex.dropdown,
1117
+ zIndex: overlayZIndex,
1117
1118
  overflow: "auto",
1118
1119
  ...panel.floatingStyleRef.current,
1119
1120
  ...(overlayStyle.current ? { maxHeight: overlayStyle.current.maxHeight } : {}),
package/src/index.ts CHANGED
@@ -416,6 +416,17 @@ export type {
416
416
  // ── Z-Index Scale ─────────────────────────────────────────────
417
417
  export { zIndex } from "./shared/zIndex";
418
418
  export type { ZIndexKey } from "./shared/zIndex";
419
+ export {
420
+ OverlayStackProvider,
421
+ overlayStackStep,
422
+ stackedOverlayZIndex,
423
+ useOverlayParentZIndex,
424
+ useOverlayZIndex,
425
+ withOverlayStack,
426
+ } from "./shared/overlayStack";
427
+ export type { OverlayTier } from "./shared/overlayStack";
428
+ export { OverlayWindow } from "./shared/OverlayWindow";
429
+ export type { OverlayWindowProps } from "./shared/OverlayWindow";
419
430
 
420
431
  // ── TanStack Form re-exports ─────────────────────────────────
421
432
  export {
@@ -0,0 +1,28 @@
1
+ import { StyleSheet, View as RNView } from "react-native";
2
+ import { PortalHost, PortalProvider } from "tamagui";
3
+ import { OverlayStackProvider } from "./overlayStack";
4
+ import type { OverlayWindowProps } from "./OverlayWindow";
5
+
6
+ export type { OverlayWindowProps } from "./OverlayWindow";
7
+
8
+ /**
9
+ * The content of an overlay that is its own native window (a core RN
10
+ * `Modal`: native SheetModal and FloatingPanel).
11
+ *
12
+ * A tamagui Dialog, modal Sheet or Popover opened in here portals to the
13
+ * `root` host, which tamagui mounts in the app's window, under this one, so
14
+ * it opened invisibly (FZ-7869). The nearest PortalProvider owns `root`, so
15
+ * this window gets its own provider and its own `root` host, laid over its
16
+ * content, and those overlays open inside it. A PortalItem in here aimed at
17
+ * any other app-level host finds no host, since this provider owns none.
18
+ */
19
+ export function OverlayWindow({ zIndex, children }: OverlayWindowProps) {
20
+ return (
21
+ <PortalProvider shouldAddRootHost={false}>
22
+ <OverlayStackProvider zIndex={zIndex}>{children}</OverlayStackProvider>
23
+ <RNView pointerEvents="box-none" style={StyleSheet.absoluteFill} testID="overlay-window-host">
24
+ <PortalHost name="root" />
25
+ </RNView>
26
+ </PortalProvider>
27
+ );
28
+ }
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from "react";
2
+ import { OverlayStackProvider } from "./overlayStack";
3
+
4
+ export interface OverlayWindowProps {
5
+ /** The z the window's own overlay binds; nested overlays stack above it. */
6
+ zIndex: number;
7
+ children?: ReactNode;
8
+ }
9
+
10
+ /**
11
+ * The content of an overlay that is its own native window (a core RN
12
+ * `Modal`). On web there is no such window: the overlay's subtree just
13
+ * carries its z for the overlays it opens.
14
+ */
15
+ export function OverlayWindow({ zIndex, children }: OverlayWindowProps) {
16
+ return <OverlayStackProvider zIndex={zIndex}>{children}</OverlayStackProvider>;
17
+ }
@@ -0,0 +1,59 @@
1
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
2
+ import { cleanup, screen } from "@testing-library/react";
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import {
5
+ OverlayStackProvider,
6
+ overlayStackStep,
7
+ stackedOverlayZIndex,
8
+ useOverlayZIndex,
9
+ type OverlayTier,
10
+ } from "./overlayStack";
11
+ import { zIndex } from "./zIndex";
12
+
13
+ afterEach(cleanup);
14
+
15
+ function Probe({ tier, id }: { tier: OverlayTier; id: string }) {
16
+ return <span data-testid={id}>{useOverlayZIndex(tier)}</span>;
17
+ }
18
+
19
+ const z = (id: string) => Number(screen.getByTestId(id).textContent);
20
+
21
+ describe("overlay stack (FZ-7869)", () => {
22
+ it("steps by the theme's $zIndex $1 token", () => {
23
+ renderWithProviders(<span />);
24
+ expect(overlayStackStep()).toBe(100);
25
+ });
26
+
27
+ it("keeps the ladder tier at page level, so a dialog covers any sheet", () => {
28
+ renderWithProviders(
29
+ <>
30
+ <Probe tier="sheet" id="sheet" />
31
+ <Probe tier="dropdown" id="dialog" />
32
+ </>,
33
+ );
34
+ expect(z("sheet")).toBe(zIndex.sheet);
35
+ expect(z("dialog")).toBe(zIndex.dropdown);
36
+ expect(z("dialog")).toBeGreaterThan(z("sheet"));
37
+ });
38
+
39
+ it("puts an overlay opened from another one step above its opener, never below its tier", () => {
40
+ const step = 100;
41
+ expect(stackedOverlayZIndex("dropdown", zIndex.sheet, step)).toBe(zIndex.dropdown);
42
+ expect(stackedOverlayZIndex("dropdown", zIndex.dropdown, step)).toBe(zIndex.dropdown + step);
43
+ expect(stackedOverlayZIndex("sheet", zIndex.dropdown, step)).toBe(zIndex.dropdown + step);
44
+ expect(stackedOverlayZIndex("sheet", zIndex.sheet, step)).toBe(zIndex.sheet + step);
45
+ });
46
+
47
+ it("reads the nearest opener through nested providers", () => {
48
+ renderWithProviders(
49
+ <OverlayStackProvider zIndex={zIndex.dropdown}>
50
+ <Probe tier="sheet" id="sheet-in-dialog" />
51
+ <OverlayStackProvider zIndex={zIndex.dropdown + 100}>
52
+ <Probe tier="dropdown" id="menu-in-sheet-in-dialog" />
53
+ </OverlayStackProvider>
54
+ </OverlayStackProvider>,
55
+ );
56
+ expect(z("sheet-in-dialog")).toBe(zIndex.dropdown + 100);
57
+ expect(z("menu-in-sheet-in-dialog")).toBe(zIndex.dropdown + 200);
58
+ });
59
+ });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Overlay stack: an overlay opened from inside another overlay stacks above
3
+ * its opener (Axiom 14, FZ-7869).
4
+ *
5
+ * Every house overlay reads the z of the overlay it was opened from and binds
6
+ * `max(own tier, parent + one $zIndex step)`, then hands its own z to what it
7
+ * contains. A top-level overlay keeps its ladder tier (`zIndex.sheet`,
8
+ * `zIndex.dropdown`), so a dialog still covers any sheet, and a dialog opened
9
+ * from a dialog, or a sheet from a dialog, lands one step above it.
10
+ *
11
+ * Tamagui 2.7.6 cannot do this on its own on native: a modal Sheet teleports
12
+ * through `Portal` with a stacked zIndex, while `Dialog.Portal` goes through
13
+ * `PortalItem` to the same root host with none, so a dialog opened in a sheet
14
+ * mounts under it. Both accept an explicit zIndex (`Dialog.Portal zIndex`,
15
+ * `Sheet portalProps.zIndex`), which is what the house overlays pass.
16
+ *
17
+ * The provider has to sit INSIDE the portaled subtree. The gorhom portal host
18
+ * renders portaled nodes at the root, under the host's context, not the
19
+ * opener's, so a provider wrapped around a portal is lost on native.
20
+ */
21
+ import { createContext, useContext, type ComponentType, type ReactNode } from "react";
22
+ import { getTokenValue } from "tamagui";
23
+ import { zIndex } from "./zIndex";
24
+
25
+ export type OverlayTier = "sheet" | "dropdown";
26
+
27
+ const OverlayStackContext = createContext<number | undefined>(undefined);
28
+
29
+ /** One rung of the theme's `$zIndex` scale: how far an overlay sits above its opener. */
30
+ export function overlayStackStep(): number {
31
+ let step: unknown;
32
+ try {
33
+ step = getTokenValue("$1" as never, "zIndex");
34
+ } catch {
35
+ step = undefined;
36
+ }
37
+ return typeof step === "number" && step > 0 ? step : 1;
38
+ }
39
+
40
+ export function stackedOverlayZIndex(
41
+ tier: OverlayTier,
42
+ parent: number | undefined,
43
+ step: number,
44
+ ): number {
45
+ const base = zIndex[tier];
46
+ return parent === undefined ? base : Math.max(base, parent + step);
47
+ }
48
+
49
+ /** The z of the overlay this subtree was opened from, or undefined at page level. */
50
+ export function useOverlayParentZIndex(): number | undefined {
51
+ return useContext(OverlayStackContext);
52
+ }
53
+
54
+ /** The z an overlay of `tier` binds here: its tier, or one step above the overlay that opened it. */
55
+ export function useOverlayZIndex(tier: OverlayTier): number {
56
+ return stackedOverlayZIndex(tier, useContext(OverlayStackContext), overlayStackStep());
57
+ }
58
+
59
+ /** Marks everything inside as opened from an overlay at `zIndex`. */
60
+ export function OverlayStackProvider({
61
+ zIndex: value,
62
+ children,
63
+ }: {
64
+ zIndex: number;
65
+ children?: ReactNode;
66
+ }) {
67
+ return <OverlayStackContext.Provider value={value}>{children}</OverlayStackContext.Provider>;
68
+ }
69
+
70
+ function resolveZIndex(value: unknown): number | undefined {
71
+ if (typeof value === "number") return value;
72
+ if (typeof value !== "string") return undefined;
73
+ const token = getTokenValue(value as never, "zIndex");
74
+ return typeof token === "number" ? token : undefined;
75
+ }
76
+
77
+ /**
78
+ * A portal part that takes `zIndex` (Dialog.Portal, AlertDialog.Portal,
79
+ * Popover.Content), bound to the stack: it renders at the tier's stacked z
80
+ * unless the caller passes its own, and its content opens further overlays
81
+ * above it.
82
+ */
83
+ export function withOverlayStack<P extends ComponentType<any>>(
84
+ Part: P,
85
+ tier: OverlayTier,
86
+ displayName: string,
87
+ ): P {
88
+ const Raw = Part as ComponentType<any>;
89
+ function Stacked(props: { zIndex?: unknown; children?: ReactNode }) {
90
+ const stacked = useOverlayZIndex(tier);
91
+ const { children, zIndex: own, ...rest } = props;
92
+ return (
93
+ <Raw {...rest} zIndex={own ?? stacked}>
94
+ <OverlayStackProvider zIndex={resolveZIndex(own) ?? stacked}>
95
+ {children}
96
+ </OverlayStackProvider>
97
+ </Raw>
98
+ );
99
+ }
100
+ Stacked.displayName = displayName;
101
+ return Stacked as unknown as P;
102
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/AdaptivePopup/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAyB5D,MAAM,WAAW,kBAAkB;IACjC,4BAA4B;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,uCAAuC;IACvC,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,gFAAgF;IAChF,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,gCAAgC;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,wCAAwC;IACxC,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,mBAAmB;IACnB,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,YAAY,CAAC;IAChD;;;;;OAKG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC3B,sDAAsD;IACtD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,kDAAkD;IAClD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAkBD,MAAM,MAAM,yBAAyB,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEtE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAC7C,IAAI,CAAC,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,CAAC,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAClC,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,yBAAyB,CAK5B;AA6ED,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,YAAY,EACZ,OAAO,EACP,KAAK,EACL,WAAW,EACX,QAAQ,EACR,IAAW,EACX,IAAe,EACf,eAAsB,EACtB,MAAM,EACN,QAAQ,GACT,EAAE,kBAAkB,2CAmRpB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/AdaptivePopup/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAyB5D,MAAM,WAAW,kBAAkB;IACjC,4BAA4B;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,uCAAuC;IACvC,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,gFAAgF;IAChF,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,gCAAgC;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,wCAAwC;IACxC,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,mBAAmB;IACnB,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,YAAY,CAAC;IAChD;;;;;OAKG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC3B,sDAAsD;IACtD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,kDAAkD;IAClD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAoBD,MAAM,MAAM,yBAAyB,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEtE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAC7C,IAAI,CAAC,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,CAAC,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAClC,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,yBAAyB,CAK5B;AA6ED,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,YAAY,EACZ,OAAO,EACP,KAAK,EACL,WAAW,EACX,QAAQ,EACR,IAAW,EACX,IAAe,EACf,eAAsB,EACtB,MAAM,EACN,QAAQ,GACT,EAAE,kBAAkB,2CAqRpB"}