@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
@@ -1,4 +1,9 @@
1
- import { transitionProps } from "@multiplatform.one/theme";
1
+ import {
2
+ transitionProps,
3
+ overlayScrim,
4
+ sheetGrabber,
5
+ sheetGrabberClearance,
6
+ } from "@multiplatform.one/theme";
2
7
  import type { ReactNode, RefObject, UIEvent } from "react";
3
8
  import {
4
9
  cloneElement,
@@ -15,7 +20,6 @@ import {
15
20
  OVERLAY_ATTACH_GAP,
16
21
  OVERLAY_BREAKPOINT,
17
22
  Surface,
18
- SurfaceContext,
19
23
  useResolvedKnobs,
20
24
  wasKeyboardFocus,
21
25
  ensureKeyboardModalityTracking,
@@ -41,14 +45,8 @@ import { OverlayStackProvider, useInsideOverlay, useOverlayZIndex } from "../sha
41
45
  import { resolveCompactFace, type CompactFace } from "./compactFace";
42
46
  import { isNestedPanelAction, type PanelActionEvent } from "./nestedAction";
43
47
  import { PanelPortal } from "./PanelPortal";
44
- import {
45
- echoHeaderBox,
46
- PanelIconEchoBoxContext,
47
- sameEchoBox,
48
- type PanelIconEchoBox,
49
- } from "./echoContext";
50
- import { iconEchoElement, iconEchoOverlays, type IconEcho } from "./iconEcho";
51
- import { echoInCover, iconCoverRect } from "./coverRect";
48
+ import { PanelSheetContext } from "./echoContext";
49
+ import { iconCoverRect } from "./coverRect";
52
50
  import { groupSurfaceOf, paintedTriggerBox } from "./paintedBox";
53
51
  import {
54
52
  contextMenuPoint,
@@ -103,14 +101,23 @@ export interface FloatingPanelProps {
103
101
  */
104
102
  coverAlign?: "start" | "auto";
105
103
  /**
106
- * An icon-only trigger's covering panel draws the pressed button again in
107
- * its own spot as an X, same size and plate, the way iOS morphs a + into an
108
- * X (LC-112, FZ-8480 Round 37). `"close"` lays the X over the content's
109
- * header row; `"menu"` gives it its own band above the rows. It closes the
110
- * panel and reads "Close". The panel never slides off the button: it opens
111
- * down from the button's top edge, or up from its bottom edge.
104
+ * The trigger is an icon-only button (LC-112, FZ-8665 Round 45). Its panel
105
+ * covers the surface the button is seen on, a glass capsule or its press
106
+ * target, and repeats nothing: the content starts at the panel's top edge,
107
+ * and a press outside it closes it. The panel never slides off the button:
108
+ * it opens down from the button's top edge, or up from its bottom edge.
109
+ */
110
+ iconCover?: boolean;
111
+ /**
112
+ * Native only: the panel opens the way a macOS pop-up does (LC-112, FZ-8655).
113
+ * `row` is the row that sits on the trigger, its offset and height in the
114
+ * panel's content, `null` until the caller has measured it. There's no
115
+ * trigger echo, a list that fits the safe edges shows whole, and a taller one
116
+ * scrolls under opaque arrow bands (`placeCheckedOnField`). At regular width
117
+ * this is the panel's face at every list length, so a Select never takes
118
+ * RegularPanel's echo or a sheet there.
112
119
  */
113
- iconEcho?: IconEcho;
120
+ popUp?: { row: { top: number; height: number } | null };
114
121
  /**
115
122
  * Optional control rendered beside the trigger but OUTSIDE the native press
116
123
  * capture (e.g. DatePicker/ColorPicker clear). On native the trigger subtree
@@ -410,7 +417,6 @@ export function TriggerEchoRow({
410
417
  const stretched = grown > rect.width;
411
418
  row.style.width = `${stretched ? grown : rect.width}px`;
412
419
  row.style.height = `${rect.height}px`;
413
- const rtl = isRtl(source);
414
420
  row.style.alignSelf = "flex-start";
415
421
  const frameStyle = row.parentElement ? getComputedStyle(row.parentElement) : null;
416
422
  row.style.position = "sticky";
@@ -419,12 +425,10 @@ export function TriggerEchoRow({
419
425
  row.style.zIndex = "1";
420
426
  // Sticky over a scrolled body, so it carries the panel's own fill.
421
427
  row.style.backgroundColor = frameStyle?.backgroundColor ?? "";
422
- row.style.marginLeft = rtl
423
- ? ""
424
- : `${-(parseFloat(frameStyle?.borderLeftWidth || "0") || 0)}px`;
425
- row.style.marginRight = rtl
426
- ? `${-(parseFloat(frameStyle?.borderRightWidth || "0") || 0)}px`
427
- : "";
428
+ // Both sides overlap the frame's border, so a trigger-wide row fits a
429
+ // trigger-wide panel instead of pushing it a border wider (FZ-8557).
430
+ row.style.marginLeft = `${-(parseFloat(frameStyle?.borderLeftWidth || "0") || 0)}px`;
431
+ row.style.marginRight = `${-(parseFloat(frameStyle?.borderRightWidth || "0") || 0)}px`;
428
432
  const snapshot =
429
433
  children == null ? (source.cloneNode(true) as HTMLElement) : visualRef.current;
430
434
  if (!snapshot) return;
@@ -521,97 +525,6 @@ export function placeEchoOverTrigger(
521
525
  }
522
526
  }
523
527
 
524
- /**
525
- * The band an icon trigger's echo rides (LC-112, FZ-8480): as tall as the
526
- * trigger, sticky on the panel's top edge (or its bottom edge when the panel
527
- * opened upward), filled with the panel's surface so scrolled rows pass under
528
- * it. It comes last in the document, so a keyboard open lands on the panel's
529
- * content and the echo is the last tab stop; CSS order draws it first.
530
- */
531
- function IconEchoRow({ children }: { children: ReactNode }) {
532
- return (
533
- <View
534
- data-fp-icon-echo=""
535
- flexShrink={0}
536
- alignSelf="stretch"
537
- pointerEvents="box-none"
538
- style={{ order: -1, position: "sticky", zIndex: 1 }}
539
- >
540
- <View data-fp-icon-echo-box="" alignItems="flex-start" justifyContent="flex-start">
541
- {children}
542
- </View>
543
- </View>
544
- );
545
- }
546
-
547
- /**
548
- * Lay the icon echo on the trigger's measured box: the band is the trigger's
549
- * height on the edge the panel shares with it, and the box sits `offset` in
550
- * from the panel's start edge, pulled back over the frame's border so the
551
- * button lands exactly where the trigger is. An overlaid echo (an X) takes no
552
- * height of its own and lets the content show beside it.
553
- */
554
- function placeIconEcho(
555
- panelElement: HTMLElement,
556
- {
557
- offset,
558
- width,
559
- height,
560
- up,
561
- overlay,
562
- group = null,
563
- }: {
564
- offset: number;
565
- width: number;
566
- height: number;
567
- up: boolean;
568
- overlay: boolean;
569
- /**
570
- * The surface the panel covers when it is not the trigger's own box (a
571
- * grouped capsule, or the 44pt target of a smaller avatar): the band
572
- * covers all of it, and only the pressed trigger shows, at its own spot.
573
- */
574
- group?: { top: number; height: number } | null;
575
- },
576
- ) {
577
- const row = panelElement.querySelector<HTMLElement>("[data-fp-icon-echo]");
578
- const box = row?.querySelector<HTMLElement>("[data-fp-icon-echo-box]");
579
- if (!row || !box) return;
580
- const frame = getComputedStyle(panelElement);
581
- const border = (side: "Top" | "Bottom" | "Left" | "Right") =>
582
- parseFloat(frame[`border${side}Width`] || "0") || 0;
583
- const rtl = panelElement.dir === "rtl";
584
- row.style.order = up ? "1" : "-1";
585
- row.style.height = `${height}px`;
586
- row.style.backgroundColor = overlay ? "transparent" : frame.backgroundColor;
587
- if (up) {
588
- row.style.top = "";
589
- row.style.marginTop = "";
590
- row.style.marginBottom = `${-border("Bottom")}px`;
591
- row.style.bottom = row.style.marginBottom;
592
- } else {
593
- row.style.bottom = "";
594
- row.style.marginTop = `${-border("Top")}px`;
595
- row.style.marginBottom = overlay ? `${-(height - border("Top"))}px` : "";
596
- row.style.top = row.style.marginTop;
597
- }
598
- row.setAttribute("data-fp-icon-echo", overlay ? "overlay" : "band");
599
- if (group) {
600
- row.style.height = `${group.height}px`;
601
- if (!up && overlay) row.style.marginBottom = `${-(group.height - border("Top"))}px`;
602
- }
603
- row.style.marginLeft = `${-border("Left")}px`;
604
- row.style.marginRight = `${-border("Right")}px`;
605
- box.style.width = `${width}px`;
606
- box.style.height = `${height}px`;
607
- box.style.marginLeft = rtl ? "" : `${offset}px`;
608
- box.style.marginRight = rtl ? `${offset}px` : "";
609
- box.style.alignSelf = "flex-start";
610
- box.style.marginTop = group ? `${group.top}px` : "";
611
- box.style.position = "relative";
612
- box.style.zIndex = "1";
613
- }
614
-
615
528
  /** The box that lays an element out, past any `display: contents` wrappers. */
616
529
  function layoutParentOf(element: HTMLElement | null): HTMLElement | null {
617
530
  let parent = element?.parentElement ?? null;
@@ -815,11 +728,11 @@ function FloatingPanelSheet({
815
728
  >
816
729
  <Sheet.Overlay
817
730
  {...transitionProps(scrimMotion)}
818
- backgroundColor="$shadow6"
731
+ backgroundColor={overlayScrim}
819
732
  enterStyle={{ opacity: 0 }}
820
733
  exitStyle={{ opacity: 0 }}
821
734
  />
822
- <Sheet.Handle />
735
+ <Sheet.Handle {...sheetGrabber} aria-label="Resize" />
823
736
  <Sheet.Frame
824
737
  data-testid="floating-panel-sheet"
825
738
  // Sheet.Frame forwards its ref at runtime; the styled prop type
@@ -829,19 +742,32 @@ function FloatingPanelSheet({
829
742
  {...knobProps.elevatedSurface}
830
743
  borderBottomLeftRadius={0}
831
744
  borderBottomRightRadius={0}
745
+ // A sheet spans the screen: only its top edge meets the page, so the
746
+ // sides and bottom draw no border beside a full-width row (FZ-8557).
747
+ borderLeftWidth={0}
748
+ borderRightWidth={0}
749
+ borderBottomWidth={0}
832
750
  {...panelChromeFocus}
833
751
  padding={framePadding}
752
+ {...(contentPadding === "none" ? { paddingTop: sheetGrabberClearance } : null)}
834
753
  gap={frameGap}
835
754
  >
836
755
  <OverlayStackProvider zIndex={sheetZIndex}>
837
- {header}
838
- {scrollable ? (
839
- <Sheet.ScrollView flex={header ? 1 : undefined} {...scrollViewProps}>
840
- {children}
841
- </Sheet.ScrollView>
842
- ) : (
843
- children
844
- )}
756
+ <PanelSheetContext.Provider value>
757
+ {header}
758
+ {scrollable ? (
759
+ <Sheet.ScrollView flex={header ? 1 : undefined} {...scrollViewProps}>
760
+ {children}
761
+ </Sheet.ScrollView>
762
+ ) : (
763
+ children
764
+ )}
765
+ {contentPadding === "none" ? (
766
+ // An edge-to-edge list clears the home indicator (HIG); a padded
767
+ // frame's own padding already does.
768
+ <View style={{ height: "env(safe-area-inset-bottom)" }} flexShrink={0} />
769
+ ) : null}
770
+ </PanelSheetContext.Provider>
845
771
  </OverlayStackProvider>
846
772
  </Sheet.Frame>
847
773
  </Sheet>
@@ -867,7 +793,7 @@ function FloatingPanelWeb({
867
793
  cover = true,
868
794
  triggerEcho = true,
869
795
  coverAlign = "start",
870
- iconEcho,
796
+ iconCover,
871
797
  triggerRow,
872
798
  anchorRect,
873
799
  clampCollisions = false,
@@ -880,10 +806,8 @@ function FloatingPanelWeb({
880
806
  // A context menu anchored on its trigger (a bar button's long-press menu)
881
807
  // covers the trigger like any other menu; only a free one opens at the point.
882
808
  const pointMenu = isContextMenu && sheetProps.anchorMode !== "cover-trigger";
883
- const showIconEcho = cover && !pointMenu && iconEcho != null;
884
- const triggerSurface = SurfaceContext.useStyledContext();
885
- const iconEchoUpRef = useRef(false);
886
- const [echoBox, setEchoBox] = useState<PanelIconEchoBox | null>(null);
809
+ const coversIcon = cover && !pointMenu && iconCover === true;
810
+ const openedUpRef = useRef(false);
887
811
  const overlayStyleRef = useRef<{ top: number; left?: number; maxHeight: number } | null>(null);
888
812
  const pinnedTopRef = useRef(0);
889
813
  const pinnedHeightRef = useRef(0);
@@ -996,11 +920,10 @@ function FloatingPanelWeb({
996
920
  const viewportBottom = viewportTop + (viewport?.height ?? window.innerHeight);
997
921
  dropdownElement.style.left = `${viewportLeft - containingBlock.left}px`;
998
922
  // LC-112 (FZ-8480 Round 37): a grouped icon trigger's surface is the
999
- // group's plate, so the panel covers the whole plate; only the pressed
1000
- // trigger shows, as its X.
1001
- const groupSurface = showIconEcho ? groupSurfaceOf(trig) : null;
923
+ // group's plate, so the panel covers the whole plate.
924
+ const groupSurface = coversIcon ? groupSurfaceOf(trig) : null;
1002
925
  const groupBox = groupSurface?.getBoundingClientRect() ?? null;
1003
- if (showIconEcho) dropdownElement.style.minWidth = groupBox ? `${groupBox.width}px` : "";
926
+ if (coversIcon) dropdownElement.style.minWidth = groupBox ? `${groupBox.width}px` : "";
1004
927
  const layoutStyle = getComputedStyle(dropdownElement);
1005
928
  const scale =
1006
929
  layoutStyle.transform &&
@@ -1069,7 +992,7 @@ function FloatingPanelWeb({
1069
992
  : totalContentHeight;
1070
993
  // LC-112 (FZ-8480): an icon trigger's panel covers the surface the
1071
994
  // trigger is seen on: its capsule, else its press target.
1072
- const painted = showIconEcho ? paintedTriggerBox(trig, measured) : null;
995
+ const painted = coversIcon ? paintedTriggerBox(trig, measured) : null;
1073
996
  const coverRect = painted
1074
997
  ? iconCoverRect(
1075
998
  {
@@ -1088,20 +1011,14 @@ function FloatingPanelWeb({
1088
1011
  : null,
1089
1012
  )
1090
1013
  : null;
1091
- const echoCover = coverRect
1092
- ? echoInCover(
1093
- { x: measured.left, y: measured.top, width: measured.width, height: measured.height },
1094
- coverRect,
1095
- )
1096
- : null;
1097
1014
  let placedTop = measured.top;
1098
1015
  let placedLeft = left;
1099
1016
  let placedMaxHeight: number | null = null;
1100
- iconEchoUpRef.current = false;
1017
+ openedUpRef.current = false;
1101
1018
  if (coverRect && measured.width > 0 && measured.height > 0) {
1102
- // LC-112 (FZ-8480): the button is drawn again in its own spot, so the
1103
- // panel stays on it: down from its top edge, or up from its bottom.
1104
- // It covers the button's plate and press box where they reach past it.
1019
+ // LC-112 (FZ-8480): the panel stays on its button: down from its top
1020
+ // edge, or up from its bottom. It covers the button's plate and press
1021
+ // box where they reach past it.
1105
1022
  const placed = placeOverTrigger({
1106
1023
  trigger: coverRect,
1107
1024
  width,
@@ -1120,7 +1037,7 @@ function FloatingPanelWeb({
1120
1037
  if (!slides) placedLeft = placed.left;
1121
1038
  alignEnd = placed.end;
1122
1039
  placedMaxHeight = placed.maxHeight;
1123
- iconEchoUpRef.current = placed.up;
1040
+ openedUpRef.current = placed.up;
1124
1041
  } else if (
1125
1042
  measured.width > 0 &&
1126
1043
  measured.height > 0 &&
@@ -1162,33 +1079,9 @@ function FloatingPanelWeb({
1162
1079
  alignEnd = placed.end;
1163
1080
  }
1164
1081
  coverAlignEndRef.current = alignEnd;
1165
- const echoStart = echoOffset({
1166
- triggerLeft: measured.left,
1167
- triggerRight: measured.right,
1168
- panelLeft: placedLeft,
1169
- panelWidth: width,
1170
- rtl,
1171
- });
1172
- const echoOverlays = iconEcho != null && iconEchoOverlays(iconEcho, iconEchoUpRef.current);
1173
- const nextEchoBox =
1174
- showIconEcho && echoOverlays
1175
- ? echoHeaderBox({
1176
- offset: echoStart,
1177
- width: measured.width,
1178
- height: echoCover?.height ?? measured.height,
1179
- panelWidth: width,
1180
- rtl,
1181
- })
1182
- : null;
1183
- setEchoBox((previous) => (sameEchoBox(previous, nextEchoBox) ? previous : nextEchoBox));
1184
- placeIconEcho(dropdownElement, {
1185
- offset: echoStart,
1186
- width: measured.width,
1187
- height: measured.height,
1188
- up: iconEchoUpRef.current,
1189
- overlay: echoOverlays,
1190
- group: echoCover,
1191
- });
1082
+ // A whole-pixel start edge: at a half pixel the rows paint a device
1083
+ // pixel short of the panel's far edge, a gutter of panel fill (FZ-8557).
1084
+ placedLeft = Math.round(placedLeft);
1192
1085
  placeEchoOverTrigger(
1193
1086
  dropdownElement,
1194
1087
  {
@@ -1208,7 +1101,7 @@ function FloatingPanelWeb({
1208
1101
  dropdownElement.style.left = `${cssLeft}px`;
1209
1102
  dropdownElement.style.maxHeight = `${maxHeight}px`;
1210
1103
  dropdownElement.dir = rtl ? "rtl" : "ltr";
1211
- dropdownElement.style.transformOrigin = `${iconEchoUpRef.current ? "bottom" : "top"} ${rtl !== alignEnd ? "right" : "left"}`;
1104
+ dropdownElement.style.transformOrigin = `${openedUpRef.current ? "bottom" : "top"} ${rtl !== alignEnd ? "right" : "left"}`;
1212
1105
  dropdownElement.style.visibility = "visible";
1213
1106
  overlayStyleRef.current = { top, left: cssLeft, maxHeight };
1214
1107
  panel.initializingRef.current = false;
@@ -1369,7 +1262,7 @@ function FloatingPanelWeb({
1369
1262
  isFill,
1370
1263
  isContextMenu,
1371
1264
  pointMenu,
1372
- showIconEcho,
1265
+ coversIcon,
1373
1266
  cover,
1374
1267
  anchorRect,
1375
1268
  clampCollisions,
@@ -1644,7 +1537,7 @@ function FloatingPanelWeb({
1644
1537
  transition: webTransition,
1645
1538
  transformOrigin:
1646
1539
  cover && !pointMenu
1647
- ? `${iconEchoUpRef.current ? "bottom" : "top"} ${
1540
+ ? `${openedUpRef.current ? "bottom" : "top"} ${
1648
1541
  isRtl(panel.triggerRef.current) !== coverAlignEndRef.current
1649
1542
  ? "right"
1650
1543
  : "left"
@@ -1680,7 +1573,7 @@ function FloatingPanelWeb({
1680
1573
  paddingVertical={0}
1681
1574
  paddingBottom={panel.coverCornerInset || 0}
1682
1575
  >
1683
- {pointMenu || !cover || !triggerEcho || showIconEcho ? null : (
1576
+ {pointMenu || !cover || !triggerEcho || coversIcon ? null : (
1684
1577
  <TriggerEchoRow
1685
1578
  sourceRef={panel.triggerRef}
1686
1579
  source={trigger}
@@ -1701,11 +1594,7 @@ function FloatingPanelWeb({
1701
1594
  {...(contentPadding !== "none" ? panel.knobProps.panelPadding : undefined)}
1702
1595
  {...(contentPadding !== "none" ? panel.knobProps.gapLg : undefined)}
1703
1596
  >
1704
- <OverlayStackProvider zIndex={panelZIndex}>
1705
- <PanelIconEchoBoxContext.Provider value={echoBox}>
1706
- {children}
1707
- </PanelIconEchoBoxContext.Provider>
1708
- </OverlayStackProvider>
1597
+ <OverlayStackProvider zIndex={panelZIndex}>{children}</OverlayStackProvider>
1709
1598
  </View>
1710
1599
  <ScrollArrow
1711
1600
  direction="down"
@@ -1713,11 +1602,6 @@ function FloatingPanelWeb({
1713
1602
  visible={panel.arrowDown}
1714
1603
  arrowScrollDirRef={panel.arrowScrollDirRef}
1715
1604
  />
1716
- {showIconEcho && iconEcho ? (
1717
- <IconEchoRow>
1718
- {iconEchoElement(trigger, iconEcho, () => onOpenChange(false), triggerSurface)}
1719
- </IconEchoRow>
1720
- ) : null}
1721
1605
  </YStack>
1722
1606
  </Theme>
1723
1607
  </PanelPortal>
@@ -277,7 +277,7 @@ describe("placeOverTrigger (FZ-7830, LC-112 where no sheet may take it)", () =>
277
277
  });
278
278
  });
279
279
 
280
- describe("placeOverTrigger pinned: an echoed icon trigger stays in its spot (LC-112, FZ-8480)", () => {
280
+ describe("placeOverTrigger pinned: a panel stays on its icon trigger (LC-112, FZ-8480)", () => {
281
281
  const phone: PanelBounds = { top: 57, left: 10, right: 380, bottom: 800 };
282
282
 
283
283
  it("opens down from the button's top edge where the menu fits", () => {
@@ -169,10 +169,10 @@ export function placeOverTrigger({
169
169
  */
170
170
  align?: "start" | "auto";
171
171
  /**
172
- * The menu echoes its icon trigger in the button's own spot (LC-112,
173
- * FZ-8480), so it never slides off the button vertically: it opens down
174
- * from the button's top edge, or up from its bottom edge where there is
175
- * more room above, and scrolls what still does not fit.
172
+ * The menu covers its icon trigger (LC-112, FZ-8480), so it never slides
173
+ * off the button vertically: it opens down from the button's top edge, or
174
+ * up from its bottom edge where there is more room above, and scrolls what
175
+ * still does not fit.
176
176
  */
177
177
  pinned?: boolean;
178
178
  }): TriggerPlacement {
@@ -0,0 +1,53 @@
1
+ /**
2
+ * FZ-8648: one grabber. A FloatingPanel's phone sheet draws the theme's
3
+ * `sheetGrabber` on the web and on native, with nothing local on top, so it
4
+ * can't drift from SheetModal's (whose spec holds it to the same recipe).
5
+ */
6
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
7
+ import { sheetGrabber } from "@multiplatform.one/theme";
8
+ import { describe, expect, it, vi } from "vitest";
9
+ import { Text, View } from "tamagui";
10
+ import { FloatingPanel as NativePanel } from "./index.native";
11
+ import { FloatingPanel } from "./index";
12
+
13
+ vi.mock("react-native-web", async (importOriginal) => ({
14
+ ...(await importOriginal<typeof import("react-native")>()),
15
+ ActionSheetIOS: undefined,
16
+ Modal: ({ children }: { children: import("react").ReactNode }) => children,
17
+ useWindowDimensions: () => ({ width: 390, height: 844, scale: 3, fontScale: 1 }),
18
+ }));
19
+
20
+ const atoms = (node: Element) => String(node.className).split(/\s+/).filter(Boolean);
21
+
22
+ function recipeAtoms() {
23
+ const view = renderWithProviders(<View testID="recipe" {...sheetGrabber} />);
24
+ const list = atoms(view.getByTestId("recipe"));
25
+ view.unmount();
26
+ return list;
27
+ }
28
+
29
+ describe("FloatingPanel sheet grabber (FZ-8648)", () => {
30
+ it("draws the recipe in the native sheet", () => {
31
+ const recipe = recipeAtoms();
32
+ const result = renderWithProviders(
33
+ <NativePanel open onOpenChange={() => {}} trigger={<Text>Open</Text>}>
34
+ <Text>Row</Text>
35
+ </NativePanel>,
36
+ );
37
+ const capsule = result.getByTestId("floating-panel-grabber").firstElementChild as Element;
38
+ expect(atoms(capsule)).toEqual(expect.arrayContaining(recipe));
39
+ });
40
+
41
+ it("draws the recipe in the web sheet, with no transform", async () => {
42
+ const recipe = recipeAtoms();
43
+ const result = renderWithProviders(
44
+ <FloatingPanel open sheet onOpenChange={() => {}} trigger={<Text>Open</Text>}>
45
+ <Text>Row</Text>
46
+ </FloatingPanel>,
47
+ );
48
+ await result.findByTestId("floating-panel-sheet");
49
+ const handle = result.getByLabelText("Resize");
50
+ expect(atoms(handle)).toEqual(expect.arrayContaining(recipe));
51
+ expect(handle.getAttribute("style") ?? "").not.toMatch(/transform/);
52
+ });
53
+ });
@@ -1,4 +1,5 @@
1
1
  import { useTouchSurface } from "@multiplatform.one/theme";
2
+ import { CheckGlyph } from "../../shared/CheckGlyph";
2
3
  import type { ReactNode } from "react";
3
4
  import {
4
5
  Fragment,
@@ -11,13 +12,7 @@ import {
11
12
  memo,
12
13
  } from "react";
13
14
  import { useTranslation } from "../../shared/i18n";
14
- import {
15
- CaretDownIcon,
16
- CheckIcon,
17
- MagnifyingGlassIcon,
18
- PlusIcon,
19
- XIcon,
20
- } from "@phosphor-icons/react";
15
+ import { CaretDownIcon, MagnifyingGlassIcon, PlusIcon, XIcon } from "@phosphor-icons/react";
21
16
  import {
22
17
  useClick,
23
18
  useDismiss,
@@ -427,7 +422,7 @@ const ComboboxItem = memo(function ComboboxItem({
427
422
  {option.label}
428
423
  </Text>
429
424
  )}
430
- {isSelected && <CheckIcon size={14} weight="bold" color={ctx.markColor} />}
425
+ {isSelected && <CheckGlyph />}
431
426
  </ComboboxRow>
432
427
  );
433
428
  });
@@ -2237,7 +2232,7 @@ function NativeComboboxCore({
2237
2232
  {option.label}
2238
2233
  </Text>
2239
2234
  )}
2240
- {selected && <CheckIcon size={14} weight="bold" color={markColor} />}
2235
+ {selected && <CheckGlyph />}
2241
2236
  </View>
2242
2237
  );
2243
2238
  return (
@@ -1,5 +1,5 @@
1
1
  import { hairline, IconSlot, keyboardFocusRingProps } from "@multiplatform.one/theme";
2
- import { CheckIcon } from "@phosphor-icons/react";
2
+ import { CheckGlyph } from "../../shared/CheckGlyph";
3
3
  import { useRef, type KeyboardEvent, type ReactNode } from "react";
4
4
  import { Pressable } from "react-native";
5
5
  import { isWeb, Text, View, XStack, YStack } from "tamagui";
@@ -100,7 +100,7 @@ export function InlineChoices({
100
100
  >
101
101
  {option.label}
102
102
  </Text>
103
- {selected ? <CheckIcon size={14} weight="bold" color={markColor} /> : null}
103
+ {selected ? <CheckGlyph /> : null}
104
104
  </XStack>
105
105
  );
106
106