@multiplatform.one/forms 7.80.0 → 7.81.1

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 (221) hide show
  1. package/dist/cjs/AdaptivePopup/index.cjs +2 -1
  2. package/dist/cjs/AdaptivePopup/index.native.js +2 -1
  3. package/dist/cjs/AdaptivePopup/index.native.js.map +1 -1
  4. package/dist/cjs/Button/index.cjs +12 -5
  5. package/dist/cjs/Button/index.native.js +14 -6
  6. package/dist/cjs/Button/index.native.js.map +1 -1
  7. package/dist/cjs/FloatingPanel/PanelHeader.cjs +3 -8
  8. package/dist/cjs/FloatingPanel/PanelHeader.native.js +3 -8
  9. package/dist/cjs/FloatingPanel/PanelHeader.native.js.map +1 -1
  10. package/dist/cjs/FloatingPanel/coverRect.cjs +0 -8
  11. package/dist/cjs/FloatingPanel/coverRect.native.js +0 -8
  12. package/dist/cjs/FloatingPanel/coverRect.native.js.map +1 -1
  13. package/dist/cjs/FloatingPanel/echoContext.cjs +5 -27
  14. package/dist/cjs/FloatingPanel/echoContext.native.js +5 -28
  15. package/dist/cjs/FloatingPanel/echoContext.native.js.map +1 -1
  16. package/dist/cjs/FloatingPanel/index.cjs +49 -131
  17. package/dist/cjs/FloatingPanel/index.native.js +216 -135
  18. package/dist/cjs/FloatingPanel/index.native.js.map +1 -1
  19. package/dist/cjs/fields/Combobox/index.cjs +3 -10
  20. package/dist/cjs/fields/Combobox/index.native.js +3 -10
  21. package/dist/cjs/fields/Combobox/index.native.js.map +1 -1
  22. package/dist/cjs/fields/Select/InlineChoices.cjs +4 -8
  23. package/dist/cjs/fields/Select/InlineChoices.native.js +4 -8
  24. package/dist/cjs/fields/Select/InlineChoices.native.js.map +1 -1
  25. package/dist/cjs/fields/Select/index.cjs +65 -201
  26. package/dist/cjs/fields/Select/index.native.js +74 -209
  27. package/dist/cjs/fields/Select/index.native.js.map +1 -1
  28. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.cjs +2 -2
  29. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.native.js +2 -2
  30. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  31. package/dist/cjs/index.cjs +4 -0
  32. package/dist/cjs/index.native.js +4 -0
  33. package/dist/cjs/index.native.js.map +1 -1
  34. package/dist/cjs/shared/CheckGlyph.cjs +40 -0
  35. package/dist/cjs/shared/CheckGlyph.native.js +44 -0
  36. package/dist/cjs/shared/CheckGlyph.native.js.map +1 -0
  37. package/dist/cjs/shared/checkedOnField.cjs +104 -0
  38. package/dist/cjs/shared/checkedOnField.native.js +117 -0
  39. package/dist/cjs/shared/checkedOnField.native.js.map +1 -0
  40. package/dist/cjs/shared/floatingList.cjs +12 -5
  41. package/dist/cjs/shared/floatingList.native.js +15 -7
  42. package/dist/cjs/shared/floatingList.native.js.map +1 -1
  43. package/dist/esm/AdaptivePopup/index.mjs +3 -2
  44. package/dist/esm/AdaptivePopup/index.mjs.map +1 -1
  45. package/dist/esm/AdaptivePopup/index.native.js +3 -2
  46. package/dist/esm/AdaptivePopup/index.native.js.map +1 -1
  47. package/dist/esm/Button/index.mjs +12 -5
  48. package/dist/esm/Button/index.mjs.map +1 -1
  49. package/dist/esm/Button/index.native.js +14 -6
  50. package/dist/esm/Button/index.native.js.map +1 -1
  51. package/dist/esm/FloatingPanel/PanelHeader.mjs +3 -8
  52. package/dist/esm/FloatingPanel/PanelHeader.mjs.map +1 -1
  53. package/dist/esm/FloatingPanel/PanelHeader.native.js +3 -8
  54. package/dist/esm/FloatingPanel/PanelHeader.native.js.map +1 -1
  55. package/dist/esm/FloatingPanel/coverRect.mjs +1 -8
  56. package/dist/esm/FloatingPanel/coverRect.mjs.map +1 -1
  57. package/dist/esm/FloatingPanel/coverRect.native.js +1 -8
  58. package/dist/esm/FloatingPanel/coverRect.native.js.map +1 -1
  59. package/dist/esm/FloatingPanel/echoContext.mjs +4 -24
  60. package/dist/esm/FloatingPanel/echoContext.mjs.map +1 -1
  61. package/dist/esm/FloatingPanel/echoContext.native.js +4 -25
  62. package/dist/esm/FloatingPanel/echoContext.native.js.map +1 -1
  63. package/dist/esm/FloatingPanel/index.mjs +53 -135
  64. package/dist/esm/FloatingPanel/index.mjs.map +1 -1
  65. package/dist/esm/FloatingPanel/index.native.js +180 -99
  66. package/dist/esm/FloatingPanel/index.native.js.map +1 -1
  67. package/dist/esm/fields/Combobox/index.mjs +4 -11
  68. package/dist/esm/fields/Combobox/index.mjs.map +1 -1
  69. package/dist/esm/fields/Combobox/index.native.js +4 -11
  70. package/dist/esm/fields/Combobox/index.native.js.map +1 -1
  71. package/dist/esm/fields/Select/InlineChoices.mjs +2 -6
  72. package/dist/esm/fields/Select/InlineChoices.mjs.map +1 -1
  73. package/dist/esm/fields/Select/InlineChoices.native.js +2 -6
  74. package/dist/esm/fields/Select/InlineChoices.native.js.map +1 -1
  75. package/dist/esm/fields/Select/index.mjs +68 -204
  76. package/dist/esm/fields/Select/index.mjs.map +1 -1
  77. package/dist/esm/fields/Select/index.native.js +77 -212
  78. package/dist/esm/fields/Select/index.native.js.map +1 -1
  79. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.mjs +3 -3
  80. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.mjs.map +1 -1
  81. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.native.js +3 -3
  82. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  83. package/dist/esm/index.mjs +3 -1
  84. package/dist/esm/index.mjs.map +1 -1
  85. package/dist/esm/index.native.js +3 -1
  86. package/dist/esm/index.native.js.map +1 -1
  87. package/dist/esm/shared/CheckGlyph.mjs +15 -0
  88. package/dist/esm/shared/CheckGlyph.mjs.map +1 -0
  89. package/dist/esm/shared/CheckGlyph.native.js +16 -0
  90. package/dist/esm/shared/CheckGlyph.native.js.map +1 -0
  91. package/dist/esm/shared/checkedOnField.mjs +76 -0
  92. package/dist/esm/shared/checkedOnField.mjs.map +1 -0
  93. package/dist/esm/shared/checkedOnField.native.js +86 -0
  94. package/dist/esm/shared/checkedOnField.native.js.map +1 -0
  95. package/dist/esm/shared/floatingList.mjs +9 -5
  96. package/dist/esm/shared/floatingList.mjs.map +1 -1
  97. package/dist/esm/shared/floatingList.native.js +12 -7
  98. package/dist/esm/shared/floatingList.native.js.map +1 -1
  99. package/dist/jsx/AdaptivePopup/index.mjs +3 -2
  100. package/dist/jsx/AdaptivePopup/index.mjs.map +1 -1
  101. package/dist/jsx/AdaptivePopup/index.native.js +2 -1
  102. package/dist/jsx/AdaptivePopup/index.native.js.map +1 -1
  103. package/dist/jsx/Button/index.mjs +12 -5
  104. package/dist/jsx/Button/index.mjs.map +1 -1
  105. package/dist/jsx/Button/index.native.js +14 -6
  106. package/dist/jsx/Button/index.native.js.map +1 -1
  107. package/dist/jsx/FloatingPanel/PanelHeader.mjs +3 -8
  108. package/dist/jsx/FloatingPanel/PanelHeader.mjs.map +1 -1
  109. package/dist/jsx/FloatingPanel/PanelHeader.native.js +3 -8
  110. package/dist/jsx/FloatingPanel/PanelHeader.native.js.map +1 -1
  111. package/dist/jsx/FloatingPanel/coverRect.mjs +1 -8
  112. package/dist/jsx/FloatingPanel/coverRect.mjs.map +1 -1
  113. package/dist/jsx/FloatingPanel/coverRect.native.js +0 -8
  114. package/dist/jsx/FloatingPanel/coverRect.native.js.map +1 -1
  115. package/dist/jsx/FloatingPanel/echoContext.mjs +4 -24
  116. package/dist/jsx/FloatingPanel/echoContext.mjs.map +1 -1
  117. package/dist/jsx/FloatingPanel/echoContext.native.js +5 -28
  118. package/dist/jsx/FloatingPanel/echoContext.native.js.map +1 -1
  119. package/dist/jsx/FloatingPanel/index.mjs +53 -135
  120. package/dist/jsx/FloatingPanel/index.mjs.map +1 -1
  121. package/dist/jsx/FloatingPanel/index.native.js +216 -135
  122. package/dist/jsx/FloatingPanel/index.native.js.map +1 -1
  123. package/dist/jsx/fields/Combobox/index.mjs +4 -11
  124. package/dist/jsx/fields/Combobox/index.mjs.map +1 -1
  125. package/dist/jsx/fields/Combobox/index.native.js +3 -10
  126. package/dist/jsx/fields/Combobox/index.native.js.map +1 -1
  127. package/dist/jsx/fields/Select/InlineChoices.mjs +2 -6
  128. package/dist/jsx/fields/Select/InlineChoices.mjs.map +1 -1
  129. package/dist/jsx/fields/Select/InlineChoices.native.js +4 -8
  130. package/dist/jsx/fields/Select/InlineChoices.native.js.map +1 -1
  131. package/dist/jsx/fields/Select/index.mjs +68 -204
  132. package/dist/jsx/fields/Select/index.mjs.map +1 -1
  133. package/dist/jsx/fields/Select/index.native.js +74 -209
  134. package/dist/jsx/fields/Select/index.native.js.map +1 -1
  135. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.mjs +3 -3
  136. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.mjs.map +1 -1
  137. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.native.js +2 -2
  138. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  139. package/dist/jsx/index.js +3 -1
  140. package/dist/jsx/index.js.map +1 -1
  141. package/dist/jsx/index.mjs +3 -1
  142. package/dist/jsx/index.mjs.map +1 -1
  143. package/dist/jsx/index.native.js +4 -0
  144. package/dist/jsx/index.native.js.map +1 -1
  145. package/dist/jsx/shared/CheckGlyph.mjs +15 -0
  146. package/dist/jsx/shared/CheckGlyph.mjs.map +1 -0
  147. package/dist/jsx/shared/CheckGlyph.native.js +44 -0
  148. package/dist/jsx/shared/CheckGlyph.native.js.map +1 -0
  149. package/dist/jsx/shared/checkedOnField.mjs +76 -0
  150. package/dist/jsx/shared/checkedOnField.mjs.map +1 -0
  151. package/dist/jsx/shared/checkedOnField.native.js +117 -0
  152. package/dist/jsx/shared/checkedOnField.native.js.map +1 -0
  153. package/dist/jsx/shared/floatingList.mjs +9 -5
  154. package/dist/jsx/shared/floatingList.mjs.map +1 -1
  155. package/dist/jsx/shared/floatingList.native.js +15 -7
  156. package/dist/jsx/shared/floatingList.native.js.map +1 -1
  157. package/package.json +8 -8
  158. package/src/AdaptivePopup/index.tsx +6 -3
  159. package/src/Button/Button.iconButton.spec.tsx +39 -0
  160. package/src/Button/index.tsx +20 -8
  161. package/src/FloatingPanel/FloatingPanel.iconCover.spec.tsx +279 -0
  162. package/src/FloatingPanel/FloatingPanel.menuFace.native.spec.tsx +78 -69
  163. package/src/FloatingPanel/FloatingPanel.seamlessCover.spec.tsx +12 -0
  164. package/src/FloatingPanel/PanelHeader.tsx +4 -12
  165. package/src/FloatingPanel/coverRect.ts +2 -15
  166. package/src/FloatingPanel/echoContext.ts +6 -40
  167. package/src/FloatingPanel/index.native.tsx +209 -144
  168. package/src/FloatingPanel/index.tsx +71 -187
  169. package/src/FloatingPanel/pointAnchor.spec.ts +1 -1
  170. package/src/FloatingPanel/pointAnchor.ts +4 -4
  171. package/src/FloatingPanel/sheetGrabber.spec.tsx +53 -0
  172. package/src/fields/Combobox/index.tsx +4 -9
  173. package/src/fields/Select/InlineChoices.tsx +2 -2
  174. package/src/fields/Select/Select.containingBlock.spec.tsx +131 -130
  175. package/src/fields/Select/Select.popUp.native.spec.tsx +276 -0
  176. package/src/fields/Select/Select.stories.tsx +26 -0
  177. package/src/fields/Select/index.tsx +84 -255
  178. package/src/fields/nativeDateTime/OsDateTimeSheet.tsx +3 -3
  179. package/src/index.ts +2 -1
  180. package/src/shared/CheckGlyph.tsx +12 -0
  181. package/src/shared/checkedOnField.ts +116 -0
  182. package/src/shared/floatingList.spec.tsx +154 -1
  183. package/src/shared/floatingList.tsx +18 -9
  184. package/types/AdaptivePopup/index.d.ts.map +1 -1
  185. package/types/Button/index.d.ts.map +1 -1
  186. package/types/FloatingPanel/PanelHeader.d.ts +1 -6
  187. package/types/FloatingPanel/PanelHeader.d.ts.map +1 -1
  188. package/types/FloatingPanel/coverRect.d.ts +2 -11
  189. package/types/FloatingPanel/coverRect.d.ts.map +1 -1
  190. package/types/FloatingPanel/echoContext.d.ts +5 -22
  191. package/types/FloatingPanel/echoContext.d.ts.map +1 -1
  192. package/types/FloatingPanel/index.d.ts +22 -9
  193. package/types/FloatingPanel/index.d.ts.map +1 -1
  194. package/types/FloatingPanel/index.native.d.ts +8 -1
  195. package/types/FloatingPanel/index.native.d.ts.map +1 -1
  196. package/types/FloatingPanel/pointAnchor.d.ts +4 -4
  197. package/types/fields/Combobox/index.d.ts.map +1 -1
  198. package/types/fields/Select/index.d.ts.map +1 -1
  199. package/types/index.d.ts +2 -1
  200. package/types/index.d.ts.map +1 -1
  201. package/types/shared/CheckGlyph.d.ts +10 -0
  202. package/types/shared/CheckGlyph.d.ts.map +1 -0
  203. package/types/shared/checkedOnField.d.ts +54 -0
  204. package/types/shared/checkedOnField.d.ts.map +1 -0
  205. package/types/shared/floatingList.d.ts +5 -2
  206. package/types/shared/floatingList.d.ts.map +1 -1
  207. package/dist/cjs/FloatingPanel/iconEcho.cjs +0 -106
  208. package/dist/cjs/FloatingPanel/iconEcho.native.js +0 -116
  209. package/dist/cjs/FloatingPanel/iconEcho.native.js.map +0 -1
  210. package/dist/esm/FloatingPanel/iconEcho.mjs +0 -79
  211. package/dist/esm/FloatingPanel/iconEcho.mjs.map +0 -1
  212. package/dist/esm/FloatingPanel/iconEcho.native.js +0 -86
  213. package/dist/esm/FloatingPanel/iconEcho.native.js.map +0 -1
  214. package/dist/jsx/FloatingPanel/iconEcho.mjs +0 -79
  215. package/dist/jsx/FloatingPanel/iconEcho.mjs.map +0 -1
  216. package/dist/jsx/FloatingPanel/iconEcho.native.js +0 -116
  217. package/dist/jsx/FloatingPanel/iconEcho.native.js.map +0 -1
  218. package/src/FloatingPanel/FloatingPanel.iconEcho.spec.tsx +0 -357
  219. package/src/FloatingPanel/iconEcho.tsx +0 -124
  220. package/types/FloatingPanel/iconEcho.d.ts +0 -29
  221. package/types/FloatingPanel/iconEcho.d.ts.map +0 -1
@@ -61,10 +61,25 @@
61
61
  * OVERLAY_BREAKPOINT the sheet is the wrong form. The panel opens over its
62
62
  * field the way the web float does (LC-105, LC-112), sized by `widthMode`;
63
63
  * compact windows (iPhone, Slide Over) keep the sheet. `sheet` forces it.
64
+ *
65
+ * Pop-up (`popUp`, FZ-8655): a Select's menu face opens the way a macOS pop-up
66
+ * does, as the web's covering Select does. The rows are measured here, the
67
+ * caller reports the row that goes on the field, and `placeCheckedOnField`
68
+ * (shared with the web) places the panel. It's a touch list, so a panel keeps
69
+ * its box as it scrolls, as a UIMenu does, where a pointer's grows
70
+ * (`growOnScroll`).
64
71
  */
65
72
 
66
- import { nativeAnimationConfig, type MotionStop } from "@multiplatform.one/theme";
73
+ import { CaretDownIcon, CaretUpIcon } from "@phosphor-icons/react";
74
+ import {
75
+ nativeAnimationConfig,
76
+ type MotionStop,
77
+ overlayScrim,
78
+ sheetGrabber,
79
+ sheetGrabberZone,
80
+ } from "@multiplatform.one/theme";
67
81
  import {
82
+ type ComponentRef,
68
83
  isValidElement,
69
84
  type ReactNode,
70
85
  useCallback,
@@ -88,15 +103,22 @@ import {
88
103
  useWindowDimensions,
89
104
  View as RNView,
90
105
  type GestureResponderEvent,
106
+ type NativeScrollEvent,
107
+ type NativeSyntheticEvent,
91
108
  } from "react-native";
92
109
  import { SafeAreaInsetsContext, initialWindowMetrics } from "react-native-safe-area-context";
93
110
  import {
94
111
  Surface,
95
- SurfaceContext,
112
+ useGlyphColor,
96
113
  useResolvedKnobs,
97
114
  motionDurationMs,
98
115
  } from "@multiplatform.one/theme";
99
- import { View, YStack } from "tamagui";
116
+ import { getTokenValue, View, YStack } from "tamagui";
117
+ import {
118
+ placeCheckedOnField,
119
+ scrollArrowBand,
120
+ scrollArrowThreshold,
121
+ } from "../shared/checkedOnField";
100
122
  import { useInsideOverlay, useOverlayZIndex } from "../shared/overlayStack";
101
123
  import { OverlayWindow } from "../shared/OverlayWindow";
102
124
  import { t } from "../shared/t";
@@ -105,12 +127,7 @@ import { useKeyboardInset } from "../shared/useKeyboardInset";
105
127
  import type { FloatingPanelProps } from "./index";
106
128
  import { MENU_MIN_WIDTH, resolveCompactFace } from "./compactFace";
107
129
  import { echoOffset, type PanelWidth, panelWidthBox, screenBounds } from "./panelWidth";
108
- import {
109
- echoHeaderBox,
110
- PanelEchoContext,
111
- PanelIconEchoBoxContext,
112
- PanelTriggerRadiusContext,
113
- } from "./echoContext";
130
+ import { PanelEchoContext, PanelSheetContext, PanelTriggerRadiusContext } from "./echoContext";
114
131
  import {
115
132
  placeAtPoint,
116
133
  placeBesideTrigger,
@@ -122,8 +139,7 @@ import {
122
139
  import { RegularPanel } from "./RegularPanel";
123
140
  import { panelViewportPadding, resolvePanelWidthMode } from "./useFloatingPanel";
124
141
  import { TrailingActionOverlay, TriggerWithTrailingAction } from "./trailingAction";
125
- import { iconEchoElement, iconEchoOverlays, type IconEcho } from "./iconEcho";
126
- import { echoInCover, iconCoverRect } from "./coverRect";
142
+ import { iconCoverRect } from "./coverRect";
127
143
  import {
128
144
  IconButtonGroupSurfaceContext,
129
145
  useIconButtonGroupSurface,
@@ -184,7 +200,8 @@ export function FloatingPanel({
184
200
  triggerRow,
185
201
  triggerEcho = true,
186
202
  coverAlign = "start",
187
- iconEcho,
203
+ iconCover = false,
204
+ popUp,
188
205
  anchorMode,
189
206
  panelWidth = "content",
190
207
  sheet,
@@ -193,7 +210,6 @@ export function FloatingPanel({
193
210
  }: FloatingPanelProps) {
194
211
  // The panel floats over the page: it never inherits a painted ancestor.
195
212
  const { knobProps } = useResolvedKnobs({ painted: false });
196
- const triggerSurface = SurfaceContext.useStyledContext();
197
213
  const windowZIndex = useOverlayZIndex("sheet");
198
214
  // A grouped trigger's capsule is its own: a panel's content never covers it.
199
215
  const children = (
@@ -254,7 +270,7 @@ export function FloatingPanel({
254
270
  const insideOverlay = useInsideOverlay();
255
271
  const asMenu = isContextMenu
256
272
  ? compactFace === "menu"
257
- : resolveCompactFace(compactFace, insideOverlay) === "menu";
273
+ : resolveCompactFace(compactFace, insideOverlay) === "menu" || (popUp != null && regular);
258
274
  const asRegular = regular && !asMenu && !isContextMenu && sheet !== true;
259
275
  const anchored = asMenu || asRegular;
260
276
  const triggerRef = useRef<RNView | null>(null);
@@ -289,7 +305,7 @@ export function FloatingPanel({
289
305
  } else {
290
306
  setAnchor({ x: 0, y: 0, width: 0, height: 0 });
291
307
  }
292
- if (iconEcho && anchorMode !== "attach" && groupSurface) {
308
+ if (iconCover && anchorMode !== "attach" && groupSurface) {
293
309
  setGroup(undefined);
294
310
  groupSurface.measure((plate) => {
295
311
  if (live) setGroup(plate);
@@ -300,7 +316,7 @@ export function FloatingPanel({
300
316
  return () => {
301
317
  live = false;
302
318
  };
303
- }, [anchored, open, iconEcho, anchorMode, groupSurface]);
319
+ }, [anchored, open, iconCover, anchorMode, groupSurface]);
304
320
 
305
321
  const handleBackdropPress = useCallback(() => {
306
322
  if (backdropArmed) close();
@@ -669,15 +685,11 @@ export function FloatingPanel({
669
685
  Math.max(safeArea?.bottom ?? 0, keyboardInset) -
670
686
  panelViewportPadding,
671
687
  }}
672
- echo={triggerEcho && !iconEcho ? (triggerRow ?? trigger) : null}
688
+ echo={triggerEcho && !iconCover && !popUp ? (triggerRow ?? trigger) : null}
673
689
  echoLabel={triggerA11y?.label}
674
- iconEcho={
675
- iconEcho && anchorMode !== "attach"
676
- ? iconEchoElement(trigger, iconEcho, close, triggerSurface)
677
- : null
678
- }
679
- iconEchoKind={iconEcho}
680
- group={iconEcho && anchorMode !== "attach" ? (group ?? null) : null}
690
+ popUp={popUp}
691
+ iconCover={iconCover && anchorMode !== "attach"}
692
+ group={iconCover && anchorMode !== "attach" ? (group ?? null) : null}
681
693
  coverAlign={anchorMode === "attach" ? "start" : coverAlign}
682
694
  panelWidth={panelWidth}
683
695
  triggerRadius={anchorMode === "attach" ? null : triggerRadius}
@@ -749,7 +761,7 @@ export function FloatingPanel({
749
761
  accessibilityRole="button"
750
762
  accessibilityLabel={t("Close")}
751
763
  >
752
- <View flex={1} backgroundColor="$shadow6" />
764
+ <View flex={1} backgroundColor={overlayScrim} />
753
765
  </Pressable>
754
766
  {/*
755
767
  Sheet sits above the absoluteFill backdrop in z-order (later sibling).
@@ -782,19 +794,23 @@ export function FloatingPanel({
782
794
  {...knobProps.elevatedSurface}
783
795
  borderBottomLeftRadius={0}
784
796
  borderBottomRightRadius={0}
797
+ // A sheet spans the screen: only its top edge meets the page,
798
+ // so no side border gutters a full-width row (FZ-8557).
799
+ borderLeftWidth={0}
800
+ borderRightWidth={0}
801
+ borderBottomWidth={0}
785
802
  maxHeight={maxHeight}
786
803
  flexShrink={1}
787
804
  minHeight={0}
788
805
  // sheetFill opens at full sheet height (flexShrink keeps the
789
806
  // keyboard from pushing the top of the sheet off-screen).
790
807
  {...(sheetFill ? { height: maxHeight } : undefined)}
791
- paddingTop="$2"
792
808
  paddingBottom={sheetBottomPadding}
793
809
  // VoiceOver escape gesture (two-finger Z) dismisses the sheet.
794
810
  onAccessibilityEscape={close}
795
811
  >
796
812
  {/*
797
- The grab ZONE, not just the 4pt pill: a 4pt tall target is
813
+ The grab ZONE, not just the 5pt pill: a 5pt tall target is
798
814
  unhittable. The gesture lives here rather than on the whole
799
815
  sheet so it never fights the ScrollView inside it, which is the
800
816
  same division iOS uses.
@@ -803,14 +819,10 @@ export function FloatingPanel({
803
819
  {...panResponder.panHandlers}
804
820
  accessibilityRole="adjustable"
805
821
  accessibilityLabel={t("Drag to resize")}
806
- style={{
807
- alignSelf: "stretch",
808
- alignItems: "center",
809
- paddingTop: 4,
810
- paddingBottom: 12,
811
- }}
822
+ testID="floating-panel-grabber"
823
+ style={sheetGrabberZone}
812
824
  >
813
- <View width={40} height={4} borderRadius={2} backgroundColor="$color8" />
825
+ <View {...sheetGrabber} />
814
826
  </RNView>
815
827
  <YStack
816
828
  padding={framePadding}
@@ -819,8 +831,10 @@ export function FloatingPanel({
819
831
  {...(sheetFill ? { flex: 1, minHeight: 0 } : undefined)}
820
832
  >
821
833
  <Surface size="$true">
822
- {header}
823
- {content}
834
+ <PanelSheetContext.Provider value>
835
+ {header}
836
+ {content}
837
+ </PanelSheetContext.Provider>
824
838
  </Surface>
825
839
  </YStack>
826
840
  </YStack>
@@ -835,9 +849,9 @@ export function FloatingPanel({
835
849
 
836
850
  /**
837
851
  * The menu over its trigger (LC-112): measured once per open at its natural
838
- * size, then placed on the trigger's window rect. The first row repeats the
839
- * trigger in the trigger's own box, and pressing it closes the menu, as the
840
- * web echo row does.
852
+ * size, then placed on the trigger's window rect. A field's first row repeats
853
+ * the trigger in the trigger's own box, and pressing it closes the menu, as
854
+ * the web echo row does. An icon trigger repeats nothing (Round 45).
841
855
  */
842
856
  function TriggerMenuPanel({
843
857
  anchor,
@@ -845,8 +859,8 @@ function TriggerMenuPanel({
845
859
  bounds,
846
860
  echo,
847
861
  echoLabel,
848
- iconEcho = null,
849
- iconEchoKind = "menu",
862
+ popUp,
863
+ iconCover = false,
850
864
  group = null,
851
865
  coverAlign,
852
866
  panelWidth,
@@ -862,9 +876,10 @@ function TriggerMenuPanel({
862
876
  bounds: PanelBounds;
863
877
  echo: ReactNode;
864
878
  echoLabel?: string;
865
- /** LC-112 (FZ-8480): the icon trigger drawn again in its own spot. */
866
- iconEcho?: ReactNode;
867
- iconEchoKind?: IconEcho;
879
+ /** LC-112 (FZ-8655): the row that sits on the trigger, a macOS pop-up. */
880
+ popUp?: FloatingPanelProps["popUp"];
881
+ /** LC-112 (FZ-8665): an icon trigger, covered at the surface it is seen on. */
882
+ iconCover?: boolean;
868
883
  /** LC-112 (FZ-8550): the glass capsule a grouped trigger sits on. */
869
884
  group?: IconButtonGroupPlate | null;
870
885
  coverAlign: "start" | "auto";
@@ -877,12 +892,15 @@ function TriggerMenuPanel({
877
892
  children: ReactNode;
878
893
  }) {
879
894
  const { knobProps } = useResolvedKnobs({ painted: false });
895
+ const glyphColor = useGlyphColor();
880
896
  const [size, setSize] = useState<{ width: number; height: number } | null>(null);
897
+ const [contentHeight, setContentHeight] = useState<number | null>(null);
898
+ const [scrolled, setScrolled] = useState<{ up: boolean; down: boolean } | null>(null);
899
+ const scrollRef = useRef<ComponentRef<typeof RNScrollView>>(null);
900
+ const offsetRef = useRef(0);
881
901
  // LC-112 (FZ-8480, FZ-8550): an icon trigger's panel covers the surface the
882
- // trigger is seen on, its capsule or its press target, and the X keeps the
883
- // trigger's own box inside it.
884
- const cover = anchor && iconEcho != null ? iconCoverRect(anchor, group) : anchor;
885
- const echoCover = anchor && cover && iconEcho != null ? echoInCover(anchor, cover) : null;
902
+ // trigger is seen on, its capsule or its press target.
903
+ const cover = anchor && iconCover ? iconCoverRect(anchor, group) : anchor;
886
904
  // LC-112 width setting (FZ-8438 r3). `"content"` keeps the iOS pull-down's
887
905
  // 250pt floor (FZ-8220), and no panel is narrower than an echoed trigger.
888
906
  const room =
@@ -904,8 +922,31 @@ function TriggerMenuPanel({
904
922
  echo: echo != null,
905
923
  floor: Math.max(MENU_MIN_WIDTH, cover?.width ?? 0),
906
924
  });
907
- const placement =
908
- anchor && cover && size
925
+ const borderWidth = Number(knobProps.elevatedSurface.borderWidth) || 0;
926
+ const framePaddingToken = getTokenValue(
927
+ framePadding as Parameters<typeof getTokenValue>[0],
928
+ "space",
929
+ );
930
+ const framePaddingPx = typeof framePaddingToken === "number" ? framePaddingToken : 0;
931
+ // LC-112 (FZ-8655): the web's macOS pop-up, from the rows measured here.
932
+ const band = scrollArrowBand(knobProps.menuRow.minHeight);
933
+ const fit =
934
+ anchor && popUp?.row && contentHeight != null
935
+ ? placeCheckedOnField({
936
+ fieldTop: anchor.y,
937
+ fieldBottom: anchor.y + anchor.height,
938
+ rowTop: popUp.row.top + framePaddingPx,
939
+ rowHeight: popUp.row.height,
940
+ contentHeight,
941
+ boundsTop: bounds.top + borderWidth,
942
+ boundsBottom: bounds.bottom - borderWidth,
943
+ band,
944
+ })
945
+ : null;
946
+ const scrollTop = fit?.scrollTop ?? 0;
947
+ const scrolls = fit != null && contentHeight != null && fit.height < contentHeight;
948
+ const placed =
949
+ anchor && cover && size && (!popUp || fit)
909
950
  ? (attach ? placeBesideTrigger : placeOverTrigger)({
910
951
  trigger: cover,
911
952
  width: size.width,
@@ -913,23 +954,54 @@ function TriggerMenuPanel({
913
954
  bounds: room,
914
955
  rtl: I18nManager.isRTL,
915
956
  align: coverAlign,
916
- pinned: iconEcho != null,
957
+ pinned: iconCover,
917
958
  })
918
959
  : null;
960
+ const placement =
961
+ placed && fit
962
+ ? { ...placed, top: fit.top - borderWidth, maxHeight: fit.height + 2 * borderWidth }
963
+ : placed;
964
+ useLayoutEffect(() => {
965
+ offsetRef.current = scrollTop;
966
+ if (scrollTop > 0) scrollRef.current?.scrollTo({ y: scrollTop, animated: false });
967
+ }, [scrollTop]);
968
+ const atRest = {
969
+ up: scrolls && scrollTop > scrollArrowThreshold,
970
+ down:
971
+ scrolls &&
972
+ fit != null &&
973
+ contentHeight != null &&
974
+ contentHeight - scrollTop - fit.height > scrollArrowThreshold,
975
+ };
976
+ const arrows = scrolled ?? atRest;
977
+ const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
978
+ if (!scrolls) return;
979
+ const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
980
+ offsetRef.current = contentOffset.y;
981
+ const up = contentOffset.y > scrollArrowThreshold;
982
+ const down =
983
+ contentSize.height - contentOffset.y - layoutMeasurement.height > scrollArrowThreshold;
984
+ setScrolled((prev) => (prev?.up === up && prev?.down === down ? prev : { up, down }));
985
+ };
986
+ // A band's tap turns the page, less a row, so nothing goes unseen.
987
+ const scrollPage = (direction: "up" | "down") => {
988
+ if (!fit) return;
989
+ const page = Math.max(band, fit.height - 2 * band - (popUp?.row?.height ?? 0));
990
+ const next = offsetRef.current + (direction === "up" ? -page : page);
991
+ scrollRef.current?.scrollTo({ y: Math.max(0, next), animated: true });
992
+ };
919
993
  // LC-112 seamless cover: the menu is its trigger grown open, so it draws the
920
994
  // trigger's corner arc at all four corners. A field reports its radius; an
921
995
  // icon button with no echo is a circle or capsule, half its short side.
922
996
  const half = cover ? Math.min(cover.width, cover.height) / 2 : undefined;
923
997
  const radius =
924
- group != null && iconEcho != null
998
+ group != null && iconCover
925
999
  ? Math.min(group.radius, half ?? group.radius)
926
1000
  : triggerRadius != null && half != null
927
1001
  ? Math.min(triggerRadius, half)
928
1002
  : coverAlign === "auto" && half
929
1003
  ? half
930
1004
  : undefined;
931
- const borderWidth = Number(knobProps.elevatedSurface.borderWidth) || 0;
932
- const echoOverlays = iconEchoOverlays(iconEchoKind, placement?.up ?? false);
933
1005
  // A panel slid along the screen (a `"screen"` or fixed width, or one pushed
934
1006
  // back inside the margins) keeps its echo on the trigger.
935
1007
  const echoShift =
@@ -943,7 +1015,11 @@ function TriggerMenuPanel({
943
1015
  })
944
1016
  : 0;
945
1017
  return (
946
- <RNView style={StyleSheet.absoluteFill}>
1018
+ <RNView
1019
+ style={StyleSheet.absoluteFill}
1020
+ testID="floating-panel-menu-layer"
1021
+ accessibilityViewIsModal
1022
+ >
947
1023
  <Pressable
948
1024
  style={StyleSheet.absoluteFill}
949
1025
  onPress={onBackdropPress}
@@ -1013,49 +1089,47 @@ function TriggerMenuPanel({
1013
1089
  </RNView>
1014
1090
  </Pressable>
1015
1091
  ) : null}
1016
- {iconEcho != null && anchor && !placement?.up && !echoOverlays ? (
1017
- <IconEchoBand
1018
- anchor={anchor}
1019
- cover={echoCover}
1020
- shift={echoShift}
1021
- borderWidth={borderWidth}
1022
- edge="top"
1023
- >
1024
- {iconEcho}
1025
- </IconEchoBand>
1026
- ) : null}
1027
1092
  <RNScrollView
1093
+ ref={scrollRef}
1028
1094
  style={{ flexGrow: 0, flexShrink: 1 }}
1029
1095
  keyboardShouldPersistTaps="handled"
1030
- >
1031
- <YStack padding={framePadding} gap={frameGap}>
1032
- <PanelIconEchoBoxContext.Provider
1033
- value={
1034
- iconEcho != null && anchor && placement && echoOverlays
1035
- ? echoHeaderBox({
1036
- offset: echoShift,
1037
- width: anchor.width,
1038
- height: echoCover?.height ?? anchor.height,
1039
- panelWidth: placement.width,
1040
- rtl: I18nManager.isRTL,
1041
- })
1042
- : null
1096
+ {...(popUp
1097
+ ? {
1098
+ contentOffset: { x: 0, y: scrollTop },
1099
+ scrollEnabled: scrolls,
1100
+ bounces: scrolls,
1101
+ onScroll: handleScroll,
1102
+ scrollEventThrottle: 16,
1043
1103
  }
1044
- >
1104
+ : null)}
1105
+ >
1106
+ <RNView
1107
+ testID="floating-panel-menu-content"
1108
+ collapsable={false}
1109
+ onLayout={popUp ? (e) => setContentHeight(e.nativeEvent.layout.height) : undefined}
1110
+ >
1111
+ <YStack padding={framePadding} gap={frameGap}>
1045
1112
  {children}
1046
- </PanelIconEchoBoxContext.Provider>
1047
- </YStack>
1113
+ </YStack>
1114
+ </RNView>
1048
1115
  </RNScrollView>
1049
- {iconEcho != null && anchor && (placement?.up || echoOverlays) ? (
1050
- <IconEchoBand
1051
- anchor={anchor}
1052
- cover={echoCover}
1053
- shift={echoShift}
1054
- borderWidth={borderWidth}
1055
- edge={echoOverlays ? "overlay" : "bottom"}
1056
- >
1057
- {iconEcho}
1058
- </IconEchoBand>
1116
+ {scrolls ? (
1117
+ <>
1118
+ <ScrollBand
1119
+ direction="up"
1120
+ band={band}
1121
+ visible={arrows.up}
1122
+ glyphColor={glyphColor}
1123
+ onPress={scrollPage}
1124
+ />
1125
+ <ScrollBand
1126
+ direction="down"
1127
+ band={band}
1128
+ visible={arrows.down}
1129
+ glyphColor={glyphColor}
1130
+ onPress={scrollPage}
1131
+ />
1132
+ </>
1059
1133
  ) : null}
1060
1134
  </YStack>
1061
1135
  </YStack>
@@ -1065,63 +1139,50 @@ function TriggerMenuPanel({
1065
1139
  }
1066
1140
 
1067
1141
  /**
1068
- * The band an icon trigger's echo rides (LC-112, FZ-8480): the trigger's
1069
- * height on the edge the menu shares with it, outside the scroll view so the
1070
- * rows scroll under it, and the button in the trigger's own box, pulled back
1071
- * over the frame's border so it lands exactly where the trigger is.
1142
+ * A pop-up's scroll arrow on native (LC-112, FZ-8655): the web's opaque band in
1143
+ * the panel's own fill, half a menu row at a clipped edge, so a cut row hides
1144
+ * under it. A tap on it stays off the row beneath and turns the page.
1072
1145
  */
1073
- function IconEchoBand({
1074
- anchor,
1075
- cover = null,
1076
- shift,
1077
- borderWidth,
1078
- edge,
1079
- children,
1146
+ function ScrollBand({
1147
+ direction,
1148
+ band,
1149
+ visible,
1150
+ glyphColor,
1151
+ onPress,
1080
1152
  }: {
1081
- anchor: TriggerRect;
1082
- /** The trigger's offset in, and the height of, a cover larger than it. */
1083
- cover?: { top: number; height: number } | null;
1084
- shift: number;
1085
- borderWidth: number;
1086
- /** `"overlay"`: an X laid over the content's top corner, taking no height. */
1087
- edge: "top" | "bottom" | "overlay";
1088
- children: ReactNode;
1153
+ direction: "up" | "down";
1154
+ band: number;
1155
+ visible: boolean;
1156
+ glyphColor: string;
1157
+ onPress: (direction: "up" | "down") => void;
1089
1158
  }) {
1090
- const bandHeight = cover?.height ?? anchor.height;
1159
+ const { knobProps } = useResolvedKnobs({ painted: false });
1160
+ const Icon = direction === "up" ? CaretUpIcon : CaretDownIcon;
1161
+ if (!visible) return null;
1091
1162
  return (
1092
- <RNView
1093
- testID="floating-panel-icon-echo-band"
1094
- pointerEvents="box-none"
1095
- style={
1096
- edge === "overlay"
1097
- ? {
1098
- position: "absolute",
1099
- top: -borderWidth,
1100
- start: -borderWidth,
1101
- end: -borderWidth,
1102
- height: bandHeight,
1103
- zIndex: 1,
1104
- }
1105
- : {
1106
- height: bandHeight,
1107
- marginHorizontal: -borderWidth,
1108
- ...(edge === "top" ? { marginTop: -borderWidth } : { marginBottom: -borderWidth }),
1109
- }
1110
- }
1163
+ <Pressable
1164
+ testID={`floating-panel-scroll-${direction}`}
1165
+ onPress={() => onPress(direction)}
1166
+ accessibilityElementsHidden
1167
+ importantForAccessibility="no-hide-descendants"
1168
+ style={{
1169
+ position: "absolute",
1170
+ start: 0,
1171
+ end: 0,
1172
+ height: band,
1173
+ zIndex: 1,
1174
+ ...(direction === "up" ? { top: 0 } : { bottom: 0 }),
1175
+ }}
1111
1176
  >
1112
- <RNView
1113
- style={{
1114
- width: anchor.width,
1115
- height: anchor.height,
1116
- marginStart: shift,
1117
- marginTop: cover?.top ?? 0,
1118
- alignItems: "flex-start",
1119
- justifyContent: "flex-start",
1120
- }}
1177
+ <View
1178
+ flex={1}
1179
+ alignItems="center"
1180
+ justifyContent="center"
1181
+ backgroundColor={knobProps.elevatedSurface.backgroundColor}
1121
1182
  >
1122
- {children}
1123
- </RNView>
1124
- </RNView>
1183
+ <Icon size={knobProps.controlIcon.width} color={glyphColor} />
1184
+ </View>
1185
+ </Pressable>
1125
1186
  );
1126
1187
  }
1127
1188
 
@@ -1159,7 +1220,11 @@ function PointPanel({
1159
1220
  })
1160
1221
  : null;
1161
1222
  return (
1162
- <RNView style={StyleSheet.absoluteFill}>
1223
+ <RNView
1224
+ style={StyleSheet.absoluteFill}
1225
+ testID="floating-panel-point-layer"
1226
+ accessibilityViewIsModal
1227
+ >
1163
1228
  <Pressable
1164
1229
  style={StyleSheet.absoluteFill}
1165
1230
  onPress={onBackdropPress}