@multiplatform.one/forms 7.45.0 → 7.46.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 (241) hide show
  1. package/dist/cjs/AdaptivePopup/index.native.js +1 -1
  2. package/dist/cjs/AdaptivePopup/index.native.js.map +1 -1
  3. package/dist/cjs/Button/index.native.js +1 -1
  4. package/dist/cjs/Button/index.native.js.map +1 -1
  5. package/dist/cjs/Chip/index.native.js.map +1 -1
  6. package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js +1 -1
  7. package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
  8. package/dist/cjs/ContextualSaveBar/clientKnownIdentity.native.js.map +1 -1
  9. package/dist/cjs/ContextualSaveBar/commitImmediateField.native.js.map +1 -1
  10. package/dist/cjs/ContextualSaveBar/index.native.js +3 -3
  11. package/dist/cjs/ContextualSaveBar/index.native.js.map +1 -1
  12. package/dist/cjs/ContextualSaveBar/useUnsavedChangesGuard.native.js.map +1 -1
  13. package/dist/cjs/ControlGroup/index.native.js +1 -1
  14. package/dist/cjs/ControlGroup/index.native.js.map +1 -1
  15. package/dist/cjs/DescriptionList/index.native.js +2 -2
  16. package/dist/cjs/DescriptionList/index.native.js.map +1 -1
  17. package/dist/cjs/ErrorSummary.native.js.map +1 -1
  18. package/dist/cjs/FieldDisplay/editMode.native.js.map +1 -1
  19. package/dist/cjs/FieldDisplay/formatters.native.js.map +1 -1
  20. package/dist/cjs/FieldDisplay/index.native.js +1 -1
  21. package/dist/cjs/FieldDisplay/index.native.js.map +1 -1
  22. package/dist/cjs/FieldGroup.native.js.map +1 -1
  23. package/dist/cjs/FloatingPanel/index.cjs +158 -40
  24. package/dist/cjs/FloatingPanel/nestedAction.native.js.map +1 -1
  25. package/dist/cjs/FloatingPanel/pointAnchor.native.js.map +1 -1
  26. package/dist/cjs/FloatingPanel/useFloatingPanel.native.js.map +1 -1
  27. package/dist/cjs/Form/index.native.js +1 -1
  28. package/dist/cjs/Form/index.native.js.map +1 -1
  29. package/dist/cjs/Form/submissionController.native.js.map +1 -1
  30. package/dist/cjs/Form/validateOn.native.js.map +1 -1
  31. package/dist/cjs/FormGrid.native.js.map +1 -1
  32. package/dist/cjs/FormSection.native.js.map +1 -1
  33. package/dist/cjs/InputParts/index.native.js.map +1 -1
  34. package/dist/cjs/Label/index.native.js.map +1 -1
  35. package/dist/cjs/MediaPicker/base64.native.js.map +1 -1
  36. package/dist/cjs/MediaPicker/payload.native.js.map +1 -1
  37. package/dist/cjs/MediaPicker/types.native.js.map +1 -1
  38. package/dist/cjs/MediaPicker/useMediaPicker.native.js.map +1 -1
  39. package/dist/cjs/Skeleton/index.native.js.map +1 -1
  40. package/dist/cjs/Spinner/SpinnerGlyph.native.js.map +1 -1
  41. package/dist/cjs/Spinner/spinnerSize.native.js.map +1 -1
  42. package/dist/cjs/Table/index.native.js.map +1 -1
  43. package/dist/cjs/Wheel/haptic.native.js.map +1 -1
  44. package/dist/cjs/Wheel/index.native.js.map +1 -1
  45. package/dist/cjs/Wheel/wheelShared.native.js.map +1 -1
  46. package/dist/cjs/actionLabel.native.js.map +1 -1
  47. package/dist/cjs/fieldDefaults.native.js.map +1 -1
  48. package/dist/cjs/fieldLayout.native.js +1 -1
  49. package/dist/cjs/fieldLayout.native.js.map +1 -1
  50. package/dist/cjs/fields/Barcode/BarcodePreview.native.js.map +1 -1
  51. package/dist/cjs/fields/Barcode/BarcodeScanner.native.js +1 -1
  52. package/dist/cjs/fields/Barcode/code128.native.js.map +1 -1
  53. package/dist/cjs/fields/Barcode/index.native.js +2 -2
  54. package/dist/cjs/fields/Barcode/index.native.js.map +1 -1
  55. package/dist/cjs/fields/Barcode/qr.native.js.map +1 -1
  56. package/dist/cjs/fields/Barcode/scannerTypes.native.js.map +1 -1
  57. package/dist/cjs/fields/Checkbox/Checkbox.native.js +1 -1
  58. package/dist/cjs/fields/Checkbox/Checkbox.native.js.map +1 -1
  59. package/dist/cjs/fields/Checkbox/CheckboxBox.native.js.map +1 -1
  60. package/dist/cjs/fields/Checkbox/CheckboxCard.native.js.map +1 -1
  61. package/dist/cjs/fields/Checkbox/CheckboxFieldFrame.native.js.map +1 -1
  62. package/dist/cjs/fields/Checkbox/CheckboxGroup.native.js +1 -1
  63. package/dist/cjs/fields/Checkbox/CheckboxGroup.native.js.map +1 -1
  64. package/dist/cjs/fields/Checkbox/checkboxGroupContext.native.js.map +1 -1
  65. package/dist/cjs/fields/Checkbox/index.native.js.map +1 -1
  66. package/dist/cjs/fields/ColorPicker/index.native.js +3 -3
  67. package/dist/cjs/fields/ColorPicker/index.native.js.map +1 -1
  68. package/dist/cjs/fields/ColorPicker/parts.native.js +2 -2
  69. package/dist/cjs/fields/ColorPicker/parts.native.js.map +1 -1
  70. package/dist/cjs/fields/Combobox/authorOptions.native.js.map +1 -1
  71. package/dist/cjs/fields/Combobox/index.native.js +6 -6
  72. package/dist/cjs/fields/Combobox/index.native.js.map +1 -1
  73. package/dist/cjs/fields/DatePicker/Calendar.native.js.map +1 -1
  74. package/dist/cjs/fields/DatePicker/DateRangePicker.native.js +2 -2
  75. package/dist/cjs/fields/DatePicker/DateRangePicker.native.js.map +1 -1
  76. package/dist/cjs/fields/DatePicker/DatetimePicker.catalog.native.js +3 -3
  77. package/dist/cjs/fields/DatePicker/DatetimePicker.catalog.native.js.map +1 -1
  78. package/dist/cjs/fields/DatePicker/DatetimePicker.native.js +2 -2
  79. package/dist/cjs/fields/DatePicker/MonthPicker.native.js +2 -2
  80. package/dist/cjs/fields/DatePicker/MonthPicker.native.js.map +1 -1
  81. package/dist/cjs/fields/DatePicker/MultiDatePicker.native.js +2 -2
  82. package/dist/cjs/fields/DatePicker/MultiDatePicker.native.js.map +1 -1
  83. package/dist/cjs/fields/DatePicker/catalog.native.js +2 -2
  84. package/dist/cjs/fields/DatePicker/catalog.native.js.map +1 -1
  85. package/dist/cjs/fields/DatePicker/fieldValueSync.native.js.map +1 -1
  86. package/dist/cjs/fields/DatePicker/index.native.js +2 -2
  87. package/dist/cjs/fields/DatePicker/parts.native.js +2 -2
  88. package/dist/cjs/fields/DatePicker/parts.native.js.map +1 -1
  89. package/dist/cjs/fields/DatePicker/utils.native.js.map +1 -1
  90. package/dist/cjs/fields/Duration/durationMath.native.js.map +1 -1
  91. package/dist/cjs/fields/Duration/index.native.js +3 -3
  92. package/dist/cjs/fields/Duration/index.native.js.map +1 -1
  93. package/dist/cjs/fields/FileUpload/filePickerTypes.native.js.map +1 -1
  94. package/dist/cjs/fields/FileUpload/index.native.js +2 -2
  95. package/dist/cjs/fields/FileUpload/index.native.js.map +1 -1
  96. package/dist/cjs/fields/FileUpload/nativePickerLoader.native.js.map +1 -1
  97. package/dist/cjs/fields/Geolocation/expoMapsLoader.native.js.map +1 -1
  98. package/dist/cjs/fields/Geolocation/geoValue.native.js.map +1 -1
  99. package/dist/cjs/fields/Geolocation/geocoder.native.js.map +1 -1
  100. package/dist/cjs/fields/Geolocation/geocoderContext.native.js.map +1 -1
  101. package/dist/cjs/fields/Geolocation/index.native.js +5 -5
  102. package/dist/cjs/fields/Geolocation/index.native.js.map +1 -1
  103. package/dist/cjs/fields/Geolocation/mapMath.native.js.map +1 -1
  104. package/dist/cjs/fields/Geolocation/mapTypes.native.js.map +1 -1
  105. package/dist/cjs/fields/Geolocation/nativeSearchFocus.native.js.map +1 -1
  106. package/dist/cjs/fields/Geolocation/photonGeocoder.native.js.map +1 -1
  107. package/dist/cjs/fields/Icon/drawnSet.native.js.map +1 -1
  108. package/dist/cjs/fields/Icon/index.native.js +3 -3
  109. package/dist/cjs/fields/Icon/index.native.js.map +1 -1
  110. package/dist/cjs/fields/ImageField/index.native.js +2 -2
  111. package/dist/cjs/fields/ImageField/index.native.js.map +1 -1
  112. package/dist/cjs/fields/Input/index.native.js +3 -3
  113. package/dist/cjs/fields/Input/index.native.js.map +1 -1
  114. package/dist/cjs/fields/MentionInput/index.native.js +3 -3
  115. package/dist/cjs/fields/MentionInput/index.native.js.map +1 -1
  116. package/dist/cjs/fields/OTPInput/index.native.js +3 -3
  117. package/dist/cjs/fields/OTPInput/index.native.js.map +1 -1
  118. package/dist/cjs/fields/OTPInput/otpCellCss.native.js.map +1 -1
  119. package/dist/cjs/fields/PhoneInput/dialCodes.native.js.map +1 -1
  120. package/dist/cjs/fields/PhoneInput/index.native.js +3 -3
  121. package/dist/cjs/fields/PhoneInput/index.native.js.map +1 -1
  122. package/dist/cjs/fields/PhoneInput/phone.native.js.map +1 -1
  123. package/dist/cjs/fields/PhoneInput/regionNames.native.js.map +1 -1
  124. package/dist/cjs/fields/Progress/index.native.js +1 -1
  125. package/dist/cjs/fields/Progress/index.native.js.map +1 -1
  126. package/dist/cjs/fields/RadioGroup/index.native.js +1 -1
  127. package/dist/cjs/fields/RadioGroup/index.native.js.map +1 -1
  128. package/dist/cjs/fields/Rating/index.native.js +1 -1
  129. package/dist/cjs/fields/Rating/index.native.js.map +1 -1
  130. package/dist/cjs/fields/Rating/ratingMath.native.js.map +1 -1
  131. package/dist/cjs/fields/Rating/types.native.js.map +1 -1
  132. package/dist/cjs/fields/SearchInput/index.native.js +2 -2
  133. package/dist/cjs/fields/SearchInput/index.native.js.map +1 -1
  134. package/dist/cjs/fields/Select/index.native.js +4 -4
  135. package/dist/cjs/fields/Select/index.native.js.map +1 -1
  136. package/dist/cjs/fields/Signature/index.native.js +2 -2
  137. package/dist/cjs/fields/Signature/index.native.js.map +1 -1
  138. package/dist/cjs/fields/Slider/index.native.js +1 -1
  139. package/dist/cjs/fields/Slider/index.native.js.map +1 -1
  140. package/dist/cjs/fields/Slider/jumpMotion.native.js.map +1 -1
  141. package/dist/cjs/fields/Stepper/index.native.js +2 -2
  142. package/dist/cjs/fields/Stepper/index.native.js.map +1 -1
  143. package/dist/cjs/fields/Stepper/resolveStepperPlacement.native.js.map +1 -1
  144. package/dist/cjs/fields/Switch/footprint.native.js.map +1 -1
  145. package/dist/cjs/fields/Switch/index.native.js +2 -2
  146. package/dist/cjs/fields/Switch/index.native.js.map +1 -1
  147. package/dist/cjs/fields/TextArea/index.native.js +2 -2
  148. package/dist/cjs/fields/TextArea/index.native.js.map +1 -1
  149. package/dist/cjs/fields/TimePicker/catalog.native.js +3 -3
  150. package/dist/cjs/fields/TimePicker/catalog.native.js.map +1 -1
  151. package/dist/cjs/fields/TimePicker/index.native.js +2 -2
  152. package/dist/cjs/fields/TimePicker/parts.native.js +1 -1
  153. package/dist/cjs/fields/TimePicker/parts.native.js.map +1 -1
  154. package/dist/cjs/fields/TimeRangeScrubber/geometry.native.js.map +1 -1
  155. package/dist/cjs/fields/TimeRangeScrubber/index.native.js +2 -2
  156. package/dist/cjs/fields/TimeRangeScrubber/index.native.js.map +1 -1
  157. package/dist/cjs/fields/ToggleGroup/index.native.js +1 -1
  158. package/dist/cjs/fields/ToggleGroup/index.native.js.map +1 -1
  159. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  160. package/dist/cjs/fields/nativeDateTime/dateTimePickerLoader.native.js.map +1 -1
  161. package/dist/cjs/fields/nativeDateTime/osDateTimeIosDisplay.native.js.map +1 -1
  162. package/dist/cjs/fields/nativeSelect.native.js.map +1 -1
  163. package/dist/cjs/fields/selectedOrder.native.js.map +1 -1
  164. package/dist/cjs/formSpacing.native.js.map +1 -1
  165. package/dist/cjs/hooks/useFocusManagement.native.js.map +1 -1
  166. package/dist/cjs/index.native.js +44 -44
  167. package/dist/cjs/index.native.js.map +1 -1
  168. package/dist/cjs/keyboard/KeyboardDoneBar.native.js +1 -1
  169. package/dist/cjs/keyboard/chain.native.js.map +1 -1
  170. package/dist/cjs/keyboard/index.native.js.map +1 -1
  171. package/dist/cjs/keyboard/types.native.js.map +1 -1
  172. package/dist/cjs/requiredMarking.native.js.map +1 -1
  173. package/dist/cjs/shared/TimeColumn.native.js +1 -1
  174. package/dist/cjs/shared/TimeColumn.native.js.map +1 -1
  175. package/dist/cjs/shared/capabilityPreflight.native.js.map +1 -1
  176. package/dist/cjs/shared/colorRamps.native.js.map +1 -1
  177. package/dist/cjs/shared/devWarn.native.js.map +1 -1
  178. package/dist/cjs/shared/floatingList.native.js +1 -1
  179. package/dist/cjs/shared/floatingList.native.js.map +1 -1
  180. package/dist/cjs/shared/focusManagement.cjs +3 -3
  181. package/dist/cjs/shared/focusManagement.native.js +3 -3
  182. package/dist/cjs/shared/focusManagement.native.js.map +1 -1
  183. package/dist/cjs/shared/groupContext.native.js.map +1 -1
  184. package/dist/cjs/shared/i18n.native.js.map +1 -1
  185. package/dist/cjs/shared/instantMotion.native.js.map +1 -1
  186. package/dist/cjs/shared/nativeA11y.native.js.map +1 -1
  187. package/dist/cjs/shared/nativeAutoFocus.native.js.map +1 -1
  188. package/dist/cjs/shared/panelPortal.native.js.map +1 -1
  189. package/dist/cjs/shared/pickerTriggerContract.native.js.map +1 -1
  190. package/dist/cjs/shared/pressSlopProps.native.js.map +1 -1
  191. package/dist/cjs/shared/t.native.js.map +1 -1
  192. package/dist/cjs/shared/tableCellContext.native.js.map +1 -1
  193. package/dist/cjs/shared/useDragScroll.native.js.map +1 -1
  194. package/dist/cjs/shared/useKeyboardInset.native.js.map +1 -1
  195. package/dist/cjs/shared/useSheetEscapeDismiss.cjs +5 -4
  196. package/dist/cjs/shared/useSheetEscapeDismiss.native.js +5 -3
  197. package/dist/cjs/shared/useSheetEscapeDismiss.native.js.map +1 -1
  198. package/dist/cjs/shared/useWheelIncrement.native.js.map +1 -1
  199. package/dist/cjs/shared/utils.native.js +1 -1
  200. package/dist/cjs/shared/utils.native.js.map +1 -1
  201. package/dist/cjs/shared/zIndex.native.js.map +1 -1
  202. package/dist/cjs/themedPrimitives.native.js.map +1 -1
  203. package/dist/cjs/types.native.js.map +1 -1
  204. package/dist/cjs/utils/LinkCell.native.js.map +1 -1
  205. package/dist/esm/FloatingPanel/index.mjs +160 -42
  206. package/dist/esm/FloatingPanel/index.mjs.map +1 -1
  207. package/dist/esm/shared/focusManagement.mjs +3 -3
  208. package/dist/esm/shared/focusManagement.mjs.map +1 -1
  209. package/dist/esm/shared/focusManagement.native.js +3 -3
  210. package/dist/esm/shared/focusManagement.native.js.map +1 -1
  211. package/dist/esm/shared/useSheetEscapeDismiss.mjs +5 -4
  212. package/dist/esm/shared/useSheetEscapeDismiss.mjs.map +1 -1
  213. package/dist/esm/shared/useSheetEscapeDismiss.native.js +5 -3
  214. package/dist/esm/shared/useSheetEscapeDismiss.native.js.map +1 -1
  215. package/dist/jsx/FloatingPanel/index.mjs +160 -42
  216. package/dist/jsx/FloatingPanel/index.mjs.map +1 -1
  217. package/dist/jsx/index.native.js.map +1 -1
  218. package/dist/jsx/shared/focusManagement.mjs +3 -3
  219. package/dist/jsx/shared/focusManagement.mjs.map +1 -1
  220. package/dist/jsx/shared/focusManagement.native.js +3 -3
  221. package/dist/jsx/shared/focusManagement.native.js.map +1 -1
  222. package/dist/jsx/shared/useSheetEscapeDismiss.mjs +5 -4
  223. package/dist/jsx/shared/useSheetEscapeDismiss.mjs.map +1 -1
  224. package/dist/jsx/shared/useSheetEscapeDismiss.native.js +5 -3
  225. package/dist/jsx/shared/useSheetEscapeDismiss.native.js.map +1 -1
  226. package/package.json +8 -8
  227. package/src/FloatingPanel/FloatingPanel.coverTrigger.spec.tsx +184 -0
  228. package/src/FloatingPanel/FloatingPanel.spec.tsx +3 -5
  229. package/src/FloatingPanel/FloatingPanel.stories.tsx +26 -0
  230. package/src/FloatingPanel/index.tsx +255 -59
  231. package/src/FloatingPanel/useFloatingPanel.spec.tsx +4 -2
  232. package/src/shared/focusManagement.spec.tsx +11 -1
  233. package/src/shared/focusManagement.ts +16 -7
  234. package/src/shared/interactionHooks.spec.tsx +13 -0
  235. package/src/shared/useSheetEscapeDismiss.ts +9 -4
  236. package/types/FloatingPanel/index.d.ts +9 -1
  237. package/types/FloatingPanel/index.d.ts.map +1 -1
  238. package/types/shared/focusManagement.d.ts +10 -5
  239. package/types/shared/focusManagement.d.ts.map +1 -1
  240. package/types/shared/useSheetEscapeDismiss.d.ts +1 -1
  241. package/types/shared/useSheetEscapeDismiss.d.ts.map +1 -1
@@ -0,0 +1,184 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { fireEvent, waitFor, within } from "@testing-library/react";
3
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
4
+ import { useState } from "react";
5
+ import { Preset, defaultPreset, heroPreset, boldPreset } from "@multiplatform.one/theme";
6
+ import { Button } from "../Button";
7
+ import { FloatingPanel } from "./index";
8
+
9
+ const triggerBounds = {
10
+ x: 80,
11
+ y: 240,
12
+ top: 240,
13
+ bottom: 280,
14
+ left: 80,
15
+ right: 200,
16
+ width: 120,
17
+ height: 40,
18
+ };
19
+
20
+ function measure(bounds = triggerBounds, panelWidth = 240, panelHeight = 180) {
21
+ const original = HTMLElement.prototype.getBoundingClientRect;
22
+ vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
23
+ function (this: HTMLElement) {
24
+ if (this.hasAttribute("data-fp-reference")) return { ...bounds, toJSON: () => ({}) };
25
+ return original.call(this);
26
+ },
27
+ );
28
+ vi.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockImplementation(
29
+ function (this: HTMLElement) {
30
+ return this.hasAttribute("data-fp-panel") ? panelWidth : 0;
31
+ },
32
+ );
33
+ vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockImplementation(
34
+ function (this: HTMLElement) {
35
+ return this.hasAttribute("data-fp-panel") ? panelHeight : 0;
36
+ },
37
+ );
38
+ }
39
+
40
+ function Harness({ scrollable }: { scrollable?: boolean }) {
41
+ const [open, setOpen] = useState(false);
42
+ return (
43
+ <FloatingPanel
44
+ open={open}
45
+ onOpenChange={setOpen}
46
+ anchorMode="cover-trigger"
47
+ scrollable={scrollable}
48
+ trigger={
49
+ <Button aria-label="Timeline filters">
50
+ <span aria-hidden="true">◇</span>Filters
51
+ </Button>
52
+ }
53
+ >
54
+ <Button>Assigned to me</Button>
55
+ <Button>All entries</Button>
56
+ </FloatingPanel>
57
+ );
58
+ }
59
+
60
+ afterEach(() => {
61
+ vi.restoreAllMocks();
62
+ document.documentElement.removeAttribute("dir");
63
+ });
64
+
65
+ describe("LC-112 exact cover-trigger placement", () => {
66
+ it("keeps an explicitly scrollable panel anchored when a full action row fits", async () => {
67
+ measure(triggerBounds, 240, 2000);
68
+ const result = renderWithProviders(<Harness scrollable />);
69
+ fireEvent.click(result.getByRole("button", { name: "Timeline filters" }), { detail: 0 });
70
+ const panel = await result.findByTestId("floating-panel-viewport");
71
+ expect(result.queryByTestId("floating-panel-sheet")).toBeNull();
72
+ expect(panel.style.top).toBe("240px");
73
+ const echo = within(panel).getByRole("button", { name: "Timeline filters" });
74
+ expect(echo.style.position).toBe("sticky");
75
+ });
76
+ it("keeps a menu floating when its visible trigger sits flush with the viewport", async () => {
77
+ measure({ ...triggerBounds, x: 0, left: 0, right: 120, y: 0, top: 0, bottom: 40 });
78
+ const result = renderWithProviders(<Harness />);
79
+ fireEvent.click(result.getByRole("button", { name: "Timeline filters" }), { detail: 0 });
80
+ const panel = await result.findByTestId("floating-panel-viewport");
81
+ expect(panel.style.top).toBe("0px");
82
+ expect(result.queryByTestId("floating-panel-sheet")).toBeNull();
83
+ });
84
+ for (const [name, preset] of Object.entries({
85
+ Default: defaultPreset,
86
+ Hero: heroPreset,
87
+ Bold: boldPreset,
88
+ })) {
89
+ for (const theme of ["light", "dark"] as const) {
90
+ it(`preserves the measured trigger row in ${name} ${theme}`, async () => {
91
+ measure();
92
+ const result = renderWithProviders(
93
+ <Preset overrides={preset.knobs} theme={theme}>
94
+ <Harness />
95
+ </Preset>,
96
+ );
97
+ fireEvent.click(result.getByRole("button", { name: "Timeline filters" }), { detail: 0 });
98
+ const panel = await result.findByTestId("floating-panel-viewport");
99
+ const echo = within(panel).getByRole("button", { name: "Timeline filters" });
100
+ expect(echo.style.width).toBe("120px");
101
+ expect(echo.style.height).toBe("40px");
102
+ expect(echo.getAttribute("aria-expanded")).toBe("true");
103
+ expect(echo.querySelector("[id]")).toBeNull();
104
+ });
105
+ }
106
+ }
107
+
108
+ it("keeps the measured first row over its trigger, exposes its name, and closes on activation", async () => {
109
+ measure();
110
+ const result = renderWithProviders(<Harness />);
111
+ const trigger = result.getByRole("button", { name: "Timeline filters" });
112
+ fireEvent.click(trigger, { detail: 0 });
113
+ const panel = await result.findByTestId("floating-panel-viewport");
114
+ await waitFor(() => expect(panel.style.top).toBe("240px"));
115
+ expect(panel.style.left).toBe("80px");
116
+ const echo = within(panel).getByRole("button", { name: "Timeline filters" });
117
+ expect(echo.style.width).toBe("120px");
118
+ expect(echo.style.height).toBe("40px");
119
+ expect(echo.textContent).toContain("◇");
120
+ await waitFor(() =>
121
+ expect(document.activeElement).toBe(result.getByRole("button", { name: "Assigned to me" })),
122
+ );
123
+ fireEvent.click(echo);
124
+ await waitFor(() => expect(result.queryByTestId("floating-panel-viewport")).toBeNull());
125
+ });
126
+
127
+ it("uses a bottom sheet at the bottom edge instead of moving the first row upward", async () => {
128
+ measure({ ...triggerBounds, y: 730, top: 730, bottom: 770 });
129
+ const result = renderWithProviders(<Harness />);
130
+ const trigger = result.getByRole("button", { name: "Timeline filters" });
131
+ trigger.focus();
132
+ const parentChildren = trigger.closest("[data-fp-reference]")?.parentElement?.children.length;
133
+ fireEvent.click(trigger, { detail: 0 });
134
+ await result.findByTestId("floating-panel-sheet");
135
+ expect(result.queryByTestId("floating-panel-viewport")).toBeNull();
136
+ expect(result.getByRole("button", { name: "Assigned to me" })).toBeTruthy();
137
+ expect(result.getByRole("button", { name: "Timeline filters" })).toBe(trigger);
138
+ expect(trigger.closest("[data-fp-reference]")?.parentElement?.children.length).toBe(
139
+ parentChildren,
140
+ );
141
+ fireEvent.keyDown(document, { key: "Escape" });
142
+ await waitFor(() => expect(document.activeElement).toBe(trigger));
143
+ });
144
+
145
+ it("closes the collision sheet on Escape while the trigger's tooltip is open", async () => {
146
+ measure({ ...triggerBounds, y: 730, top: 730, bottom: 770 });
147
+ const tooltipEscape = (event: KeyboardEvent) => {
148
+ if (event.key === "Escape") event.stopPropagation();
149
+ };
150
+ document.addEventListener("keydown", tooltipEscape);
151
+ try {
152
+ const result = renderWithProviders(<Harness />);
153
+ const trigger = result.getByRole("button", { name: "Timeline filters" });
154
+ fireEvent.click(trigger, { detail: 0 });
155
+ await result.findByTestId("floating-panel-sheet");
156
+ fireEvent.keyDown(trigger, { key: "Escape" });
157
+ await waitFor(() => expect(result.queryByTestId("floating-panel-sheet")).toBeNull());
158
+ } finally {
159
+ document.removeEventListener("keydown", tooltipEscape);
160
+ }
161
+ });
162
+
163
+ it("uses a bottom sheet for horizontal collisions instead of shifting the trigger row", async () => {
164
+ measure({ ...triggerBounds, x: 900, left: 900, right: 1020 });
165
+ const result = renderWithProviders(<Harness />);
166
+ fireEvent.click(result.getByRole("button", { name: "Timeline filters" }), { detail: 0 });
167
+ await result.findByTestId("floating-panel-sheet");
168
+ expect(result.queryByTestId("floating-panel-viewport")).toBeNull();
169
+ });
170
+
171
+ it("aligns the trigger row at the logical start edge in RTL", async () => {
172
+ document.documentElement.dir = "rtl";
173
+ measure({ ...triggerBounds, x: 400, left: 400, right: 520 });
174
+ const result = renderWithProviders(<Harness />);
175
+ (result.container.querySelector("[data-fp-reference]") as HTMLElement).style.direction = "rtl";
176
+ fireEvent.click(result.getByRole("button", { name: "Timeline filters" }), { detail: 0 });
177
+ const panel = await result.findByTestId("floating-panel-viewport");
178
+ await waitFor(() => expect(panel.style.left).toBe("280px"));
179
+ expect(panel.style.transformOrigin).toBe("top right");
180
+ const echo = within(panel).getByRole("button", { name: "Timeline filters" });
181
+ expect(echo.style.alignSelf).toBe("flex-start");
182
+ expect(panel.dir).toBe("rtl");
183
+ });
184
+ });
@@ -4,7 +4,7 @@ import { renderWithProviders } from "@multiplatform.one/test-utils";
4
4
  import { useState, type ReactNode } from "react";
5
5
  import { Text } from "tamagui";
6
6
  import { Button } from "../Button";
7
- import { FloatingPanel, panelViewportPadding } from "./index";
7
+ import { FloatingPanel } from "./index";
8
8
 
9
9
  /**
10
10
  * The trigger contract, and why every case fires a REAL press rather than
@@ -436,9 +436,7 @@ describe("FloatingPanel nesting (MPO-221)", () => {
436
436
  await waitFor(() => expect(floatingViewport()).not.toBeNull());
437
437
  const viewport = floatingViewport() as HTMLElement;
438
438
  expect(scroller.contains(viewport)).toBe(false);
439
- // jsdom puts everything at viewport y 0, so the panel pins to the
440
- // viewport padding, now in viewport coordinates with no scroller offset.
441
- await waitFor(() => expect(viewport.style.top).toBe(`${panelViewportPadding}px`));
439
+ await waitFor(() => expect(viewport.style.top).toBe("0px"));
442
440
  });
443
441
 
444
442
  it("offsets its top by a transformed dialog it stays inside", async () => {
@@ -459,7 +457,7 @@ describe("FloatingPanel nesting (MPO-221)", () => {
459
457
  expect(dialog.contains(viewport)).toBe(true);
460
458
  // Laid out against the dialog, whose content is scrolled 40px, its css
461
459
  // top must be 40px lower to land at the same viewport y.
462
- await waitFor(() => expect(viewport.style.top).toBe(`${panelViewportPadding + 40}px`));
460
+ await waitFor(() => expect(viewport.style.top).toBe("40px"));
463
461
  });
464
462
  });
465
463
 
@@ -52,6 +52,32 @@ export const Basic: Story = {
52
52
  },
53
53
  };
54
54
 
55
+ export const CoverTriggerEdges: Story = {
56
+ render: () => {
57
+ const [open, setOpen] = useState(false);
58
+ return (
59
+ <YStack
60
+ height="100%"
61
+ minHeight="$20"
62
+ padding="$4"
63
+ justifyContent="flex-end"
64
+ alignItems="flex-end"
65
+ >
66
+ <FloatingPanel
67
+ open={open}
68
+ onOpenChange={setOpen}
69
+ anchorMode="cover-trigger"
70
+ triggerSizing="content"
71
+ trigger={<Button>Edge menu</Button>}
72
+ >
73
+ <Button onPress={() => setOpen(false)}>First action</Button>
74
+ <Button onPress={() => setOpen(false)}>Second action</Button>
75
+ </FloatingPanel>
76
+ </YStack>
77
+ );
78
+ },
79
+ };
80
+
55
81
  export const ScrollableContent: Story = {
56
82
  render: () => {
57
83
  const [open, setOpen] = useState(false);