@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,7 +1,8 @@
1
1
  import { IconSlot, useTouchSurface } from "@multiplatform.one/theme";
2
+ import { CheckGlyph } from "../../shared/CheckGlyph";
2
3
  import type { KeyboardEvent, MouseEvent, ReactNode, RefObject, UIEvent } from "react";
3
4
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, memo } from "react";
4
- import { CaretDownIcon, CaretRightIcon, CheckIcon } from "@phosphor-icons/react";
5
+ import { CaretDownIcon, CaretRightIcon } from "@phosphor-icons/react";
5
6
  import {
6
7
  useClick,
7
8
  useDismiss,
@@ -50,11 +51,14 @@ import {
50
51
  useFormField,
51
52
  useResolvedValidators,
52
53
  } from "../../shared/utils";
53
- import { ScrollArrow } from "../../shared/floatingList";
54
+ import {
55
+ growOnScroll,
56
+ placeCheckedOnField,
57
+ ScrollArrow,
58
+ scrollArrowBand,
59
+ } from "../../shared/floatingList";
54
60
  import {
55
61
  FloatingPanel,
56
- TriggerEchoRow,
57
- placeEchoOverTrigger,
58
62
  useFloatingPanel,
59
63
  panelViewportPadding,
60
64
  useViewportGtSm,
@@ -349,8 +353,8 @@ function selectItemRingProps(isActive: boolean, kbNav: boolean) {
349
353
  * row. On native the shim can only resolve a token the theme in scope actually
350
354
  * carries, and the component sub-themes carry no accent solid at all.
351
355
  * Resolve it here, where the value becomes a paint — the same shape as
352
- * `mutedColor` in Combobox. Keyed off `useMenuRowCheckInk` (`menuRowCheckInk`, ink when hue-less), the selection tint's
353
- * own ink, so the tick holds 4.5:1 on the chosen row (FZ-8355 r6), degrading
356
+ * `mutedColor` in Combobox. Keyed off `useMenuRowCheckInk` (the accent, ink when hue-less), so the tick
357
+ * holds 4.5:1 on the chosen row (FZ-8355 r6, FZ-8557), degrading
354
358
  * to `$color` ink rather than black when a narrow sub-theme lacks the key.
355
359
  */
356
360
  function useSelectedMarkColor(): string {
@@ -506,7 +510,7 @@ const SelectItem = memo(function SelectItem({
506
510
  >
507
511
  {option.label}
508
512
  </Text>
509
- {isSelected && <CheckIcon size={14} weight="bold" color={ctx.markColor} />}
513
+ {isSelected && <CheckGlyph />}
510
514
  </SelectRow>
511
515
  );
512
516
  });
@@ -624,20 +628,13 @@ function FloatingSelectCore({
624
628
  }, [options, value, multiple, selectedOrder]);
625
629
 
626
630
  const programmaticScrollRef = useRef(0);
627
- const pinnedTopRef = useRef(0);
628
- const pinnedHeightRef = useRef(0);
629
- const grownRef = useRef(false);
630
- const growUpOnlyRef = useRef(false);
631
- const growDownOnlyRef = useRef(false);
632
631
  const lastScrollTopRef = useRef(0);
633
- // LC-112 cover and scroll (FZ-8463): a list that doesn't fit below its
634
- // field covers the field with no echo row and grows toward the side with
635
- // more room, then scrolls.
636
- const coverScrollRef = useRef(false);
637
- const [coverScroll, setCoverScroll] = useState(false);
638
- const growsUpRef = useRef(false);
639
- const coverTrimRef = useRef(0);
640
- const echoed = !beside && !coverScroll;
632
+ // The panel's box with the checked row on the field (FZ-8557), in the
633
+ // padding box's viewport coordinates.
634
+ const onFieldRef = useRef<{ top: number; height: number; scrollTop: number } | null>(null);
635
+ const opensUpRef = useRef(false);
636
+ // One band for the layout and the arrows that draw it (FZ-8557).
637
+ const arrowBand = scrollArrowBand(knobProps.menuRow.minHeight);
641
638
 
642
639
  const repositionRef = useRef<(() => void) | null>(null);
643
640
 
@@ -646,21 +643,13 @@ function FloatingSelectCore({
646
643
  onOpenChange: setOpen,
647
644
  disabled,
648
645
  repositionRef,
649
- wheelGrowRef: selectedIndex >= 0 ? grownRef : undefined,
650
646
  onBeforeOpen: () => {
651
647
  computeSortedOptions();
652
648
  overlayStyle.current = null;
653
- pinnedTopRef.current = 0;
654
- pinnedHeightRef.current = 0;
655
- grownRef.current = false;
656
- growUpOnlyRef.current = false;
657
- growDownOnlyRef.current = false;
658
649
  lastScrollTopRef.current = 0;
659
650
  programmaticScrollRef.current = 0;
660
- coverScrollRef.current = false;
661
- growsUpRef.current = false;
662
- coverTrimRef.current = 0;
663
- setCoverScroll(false);
651
+ onFieldRef.current = null;
652
+ opensUpRef.current = false;
664
653
  },
665
654
  panelWidth,
666
655
  });
@@ -728,11 +717,6 @@ function FloatingSelectCore({
728
717
  const isInitial = panel.initializingRef.current;
729
718
  if (isInitial) dropdownElement.style.transition = "none";
730
719
 
731
- if (pinnedTopRef.current === 0 && pinnedHeightRef.current === 0) {
732
- programmaticScrollRef.current++;
733
- dropdownElement.scrollTop = 0;
734
- }
735
-
736
720
  const triggerBounds = trigger.getBoundingClientRect();
737
721
  const viewportHeight = window.innerHeight;
738
722
  // The math runs in viewport coordinates; a panel hosted in a transformed
@@ -744,153 +728,46 @@ function FloatingSelectCore({
744
728
  const borderBottomWidth = parseFloat(computedStyle.borderBottomWidth) || 0;
745
729
  const totalBorderWidth = borderTopWidth + borderBottomWidth;
746
730
  const scrollContentHeight = dropdownElement.scrollHeight;
747
- const totalContentHeight = scrollContentHeight + totalBorderWidth;
748
-
749
- // LC-112 seamless cover (FZ-8438): the panel's top edge is the trigger's,
750
- // the trigger echoed as its first row, the list below it. A list that
751
- // doesn't fit below its field covers the field with no echo row and
752
- // grows toward the side with more room: down from the field's top edge,
753
- // or up from its bottom edge. Past the safe screen edge it scrolls, and
754
- // scrolling grows it to fill the screen (FZ-8463).
755
- const firstPlacement = pinnedTopRef.current === 0 && pinnedHeightRef.current === 0;
756
- const boundsTop = panelViewportPadding;
757
- const boundsBottom = viewportHeight - panelViewportPadding;
758
- const echoRow = dropdownElement.querySelector<HTMLElement>("[data-fp-trigger-row]");
759
- const echoHeight = echoRow ? triggerBounds.height : 0;
760
- const listHeight = totalContentHeight - echoHeight;
761
- const roomBelow = boundsBottom - triggerBounds.top;
762
- const roomAbove = triggerBounds.bottom - boundsTop;
763
- if (firstPlacement && totalContentHeight > roomBelow) {
764
- coverScrollRef.current = true;
765
- growsUpRef.current = roomAbove > roomBelow;
766
- setCoverScroll(true);
767
- }
768
731
 
769
- if (coverScrollRef.current) {
770
- const grown = grownRef.current || growUpOnlyRef.current || growDownOnlyRef.current;
771
- const room = grown ? boundsBottom - boundsTop : growsUpRef.current ? roomAbove : roomBelow;
772
- const trim = grown ? 0 : coverTrimRef.current;
773
- const height = Math.max(0, Math.min(listHeight, room - trim));
774
- const anchor = growsUpRef.current ? triggerBounds.bottom - height : triggerBounds.top;
775
- const top = Math.min(Math.max(anchor, boundsTop), boundsBottom - height);
776
- const previousTop = pinnedTopRef.current;
777
- if (
778
- !overlayStyle.current ||
779
- overlayStyle.current.top !== top - originTop ||
780
- overlayStyle.current.maxHeight !== height
781
- ) {
782
- dropdownElement.style.top = `${top - originTop}px`;
783
- dropdownElement.style.maxHeight = `${height}px`;
784
- dropdownElement.style.height = "";
785
- overlayStyle.current = { top: top - originTop, maxHeight: height };
786
- void dropdownElement.offsetHeight;
787
- }
788
- pinnedTopRef.current = top;
789
- pinnedHeightRef.current = height;
790
- // Rows stay under the pointer while the panel grows upward.
791
- if (grown && previousTop > top && dropdownElement.scrollTop > 0) {
792
- programmaticScrollRef.current++;
793
- dropdownElement.scrollTop = Math.max(0, dropdownElement.scrollTop - (previousTop - top));
794
- }
795
- if (echoRow) return;
796
- const rowHeight = selectedItem.offsetHeight || 1;
797
- if (isInitial && selectedIndex >= 0) {
798
- // The checked row lands on the field when the list can scroll that
799
- // far. Opening down, short of that it stops a whole row short, so the
800
- // top row isn't cut in half. Two pixels of rounding are not a row.
801
- const maxScrollTop = dropdownElement.scrollHeight - dropdownElement.clientHeight;
802
- let scrollTop: number;
803
- if (growsUpRef.current) {
804
- scrollTop = selectedItem.offsetTop + rowHeight - (triggerBounds.bottom - top);
805
- } else {
806
- const short = Math.max(0, selectedItem.offsetTop - maxScrollTop - 2);
807
- scrollTop = selectedItem.offsetTop - Math.ceil(short / rowHeight) * rowHeight;
808
- }
809
- programmaticScrollRef.current++;
810
- dropdownElement.scrollTop = Math.min(maxScrollTop, Math.max(0, scrollTop));
811
- }
812
- if (isInitial && !grown) {
813
- // The far edge ends on a whole row: growing up, the row on the field
814
- // isn't cut in half; growing down, no sliver sits at the screen edge.
815
- const firstRow = listItemsRef.current[0];
816
- const visibleBottom = dropdownElement.scrollTop + dropdownElement.clientHeight;
817
- if (firstRow && visibleBottom < dropdownElement.scrollHeight - 2) {
818
- const cut = (visibleBottom - firstRow.offsetTop) % rowHeight;
819
- if (cut > 3 && cut < rowHeight - 3) {
820
- coverTrimRef.current = cut;
821
- const trimmedHeight = height - cut;
822
- const trimmedTop = growsUpRef.current ? top + cut : top;
823
- dropdownElement.style.top = `${trimmedTop - originTop}px`;
824
- dropdownElement.style.maxHeight = `${trimmedHeight}px`;
825
- overlayStyle.current = { top: trimmedTop - originTop, maxHeight: trimmedHeight };
826
- pinnedTopRef.current = trimmedTop;
827
- pinnedHeightRef.current = trimmedHeight;
828
- }
829
- }
830
- }
831
- panel.initializingRef.current = false;
832
- void dropdownElement.offsetHeight;
833
- if (isInitial) dropdownElement.style.transition = panel.panelTransition;
834
- requestAnimationFrame(() => {
835
- if (dropdownElement) panel.updateArrows(dropdownElement);
732
+ // LC-112 (FZ-8557 r3): a covering Select opens the way a macOS pop-up
733
+ // does. Its checked row sits on the field (the first row when nothing is
734
+ // checked), so the value never shows twice. A list that fits the safe
735
+ // viewport shows whole, slid the least distance to stay on screen; a
736
+ // taller one clips at the safe edges and scrolls under its arrow bands.
737
+ // Scrolling grows a panel that stops short of a safe edge toward it,
738
+ // the rows staying under the pointer.
739
+ const boundsTop = panelViewportPadding + borderTopWidth;
740
+ const boundsBottom = viewportHeight - panelViewportPadding - borderBottomWidth;
741
+ if (!onFieldRef.current) {
742
+ const fit = placeCheckedOnField({
743
+ fieldTop: triggerBounds.top,
744
+ fieldBottom: triggerBounds.bottom,
745
+ rowTop: selectedItem.offsetTop,
746
+ rowHeight: selectedItem.offsetHeight || 1,
747
+ contentHeight: scrollContentHeight,
748
+ boundsTop,
749
+ boundsBottom,
750
+ band: arrowBand,
751
+ });
752
+ onFieldRef.current = fit;
753
+ opensUpRef.current =
754
+ fit.top + fit.height / 2 < (triggerBounds.top + triggerBounds.bottom) / 2;
755
+ } else if (!isInitial) {
756
+ onFieldRef.current = growOnScroll({
757
+ ...onFieldRef.current,
758
+ scrollTop: dropdownElement.scrollTop,
759
+ contentHeight: scrollContentHeight,
760
+ boundsTop,
761
+ boundsBottom,
836
762
  });
837
- return;
838
- }
839
- const idealTop = triggerBounds.top;
840
- const idealBottom = idealTop + totalContentHeight;
841
- const pinnedTop = Math.max(panelViewportPadding, idealTop);
842
- const pinnedBottom = Math.min(viewportHeight - panelViewportPadding, idealBottom);
843
- const pinnedHeight = Math.max(120, pinnedBottom - pinnedTop);
844
-
845
- if (firstPlacement) {
846
- pinnedTopRef.current = pinnedTop;
847
- pinnedHeightRef.current = pinnedHeight;
848
- }
849
-
850
- let top = pinnedTopRef.current;
851
- let height = pinnedHeightRef.current;
852
-
853
- if (grownRef.current) {
854
- const canGrowDown = Math.max(0, viewportHeight - panelViewportPadding - (top + height));
855
- height += canGrowDown;
856
- const canGrowUp = Math.max(0, top - panelViewportPadding);
857
- top -= canGrowUp;
858
- height += canGrowUp;
859
- }
860
-
861
- if (growUpOnlyRef.current) {
862
- const canGrowUp = Math.max(0, top - panelViewportPadding);
863
- top -= canGrowUp;
864
- height += canGrowUp;
865
- }
866
- if (growDownOnlyRef.current) {
867
- const canGrowDown = Math.max(0, viewportHeight - panelViewportPadding - (top + height));
868
- height += canGrowDown;
869
- }
870
-
871
- height = Math.min(height, viewportHeight - top - panelViewportPadding);
872
- height = Math.min(height, totalContentHeight);
873
- height = Math.max(120, height);
874
-
875
- // When grown but content is shorter than viewport: anchor dropdown to bottom (gap above), dropup to top (gap below)
876
- const availableViewport = viewportHeight - 2 * panelViewportPadding;
877
- if (
878
- (grownRef.current || growUpOnlyRef.current || growDownOnlyRef.current) &&
879
- height < availableViewport
880
- ) {
881
- if (panel.placement.startsWith("top")) {
882
- top = panelViewportPadding;
883
- } else {
884
- top = viewportHeight - panelViewportPadding - height;
885
- }
886
763
  }
887
-
888
- const cachedOverlayStyle = overlayStyle.current;
889
-
764
+ const placed = onFieldRef.current;
765
+ const top = placed.top - borderTopWidth;
766
+ const height = placed.height + totalBorderWidth;
890
767
  if (
891
- !cachedOverlayStyle ||
892
- cachedOverlayStyle.top !== top - originTop ||
893
- cachedOverlayStyle.maxHeight !== height
768
+ !overlayStyle.current ||
769
+ overlayStyle.current.top !== top - originTop ||
770
+ overlayStyle.current.maxHeight !== height
894
771
  ) {
895
772
  dropdownElement.style.top = `${top - originTop}px`;
896
773
  dropdownElement.style.maxHeight = `${height}px`;
@@ -898,63 +775,24 @@ function FloatingSelectCore({
898
775
  overlayStyle.current = { top: top - originTop, maxHeight: height };
899
776
  void dropdownElement.offsetHeight;
900
777
  }
901
-
902
- if (
903
- isInitial &&
904
- selectedIndex >= 0 &&
905
- !grownRef.current &&
906
- !growUpOnlyRef.current &&
907
- !growDownOnlyRef.current
908
- ) {
909
- // The selected row starts in view, just under the sticky echo row.
910
- const rowBottom = selectedItem.offsetTop + selectedItem.offsetHeight;
911
- if (rowBottom > dropdownElement.clientHeight) {
912
- const maxScrollTop = dropdownElement.scrollHeight - dropdownElement.clientHeight;
913
- programmaticScrollRef.current++;
914
- dropdownElement.scrollTop = Math.max(
915
- 0,
916
- Math.min(selectedItem.offsetTop - echoHeight, maxScrollTop),
917
- );
918
- }
778
+ if (Math.abs(dropdownElement.scrollTop - placed.scrollTop) >= 0.5) {
779
+ programmaticScrollRef.current++;
780
+ dropdownElement.scrollTop = placed.scrollTop;
919
781
  }
920
-
921
- // A screen-wide or fixed-width list slides inside the margins; its echo
922
- // stays on the field (LC-112 r3). The scale origin is the left edge, so
923
- // the client rect's left is the layout left.
924
- placeEchoOverTrigger(
925
- dropdownElement,
926
- {
927
- triggerLeft: triggerBounds.left,
928
- triggerRight: triggerBounds.right,
929
- panelLeft: dropdownElement.getBoundingClientRect().left,
930
- panelWidth: dropdownElement.offsetWidth,
931
- rtl: getComputedStyle(trigger).direction === "rtl",
932
- },
933
- { stretch: panelWidth !== undefined },
934
- );
935
-
936
782
  panel.initializingRef.current = false;
937
-
938
783
  void dropdownElement.offsetHeight;
939
784
  if (isInitial) dropdownElement.style.transition = panel.panelTransition;
940
785
  requestAnimationFrame(() => {
941
- if (dropdownElement) {
942
- panel.updateArrows(dropdownElement);
943
- requestAnimationFrame(() => {
944
- if (dropdownElement) panel.updateArrows(dropdownElement);
945
- });
946
- }
786
+ if (dropdownElement) panel.updateArrows(dropdownElement);
947
787
  });
948
788
  }, [
949
789
  selectedIndex,
950
790
  panel.refs,
951
791
  panel.updateArrows,
952
- panel.placement,
953
792
  panel.panelTransition,
954
793
  options.length,
955
794
  beside,
956
- coverScroll,
957
- panelWidth,
795
+ arrowBand,
958
796
  ]);
959
797
 
960
798
  repositionRef.current = repositionOverlay;
@@ -1156,23 +994,10 @@ function FloatingSelectCore({
1156
994
  return;
1157
995
  }
1158
996
  const arrowDir = panel.arrowScrollDirRef.current;
1159
- if (arrowDir) {
1160
- panel.arrowScrollDirRef.current = null;
1161
- lastScrollTopRef.current = el.scrollTop;
1162
- if (arrowDir === "down") {
1163
- growUpOnlyRef.current = true;
1164
- } else {
1165
- growDownOnlyRef.current = true;
1166
- }
1167
- repositionOverlay();
1168
- return;
1169
- }
997
+ if (arrowDir) panel.arrowScrollDirRef.current = null;
1170
998
  const delta = el.scrollTop - lastScrollTopRef.current;
1171
999
  lastScrollTopRef.current = el.scrollTop;
1172
- if (delta !== 0) {
1173
- grownRef.current = true;
1174
- repositionOverlay();
1175
- }
1000
+ if (delta !== 0) repositionOverlay();
1176
1001
  },
1177
1002
  [panel.updateArrows, repositionOverlay],
1178
1003
  );
@@ -1290,10 +1115,7 @@ function FloatingSelectCore({
1290
1115
  opacity: panel.visible ? 1 : 0,
1291
1116
  transform: panel.visible ? "scale(1)" : "scale(0.96)",
1292
1117
  transition: panel.panelTransition,
1293
- transformOrigin:
1294
- (coverScroll && growsUpRef.current) || panel.placement.startsWith("top")
1295
- ? "bottom left"
1296
- : "top left",
1118
+ transformOrigin: opensUpRef.current ? "bottom left" : "top left",
1297
1119
  ...dragProps.style,
1298
1120
  },
1299
1121
  onPointerDown: dragProps.onPointerDown,
@@ -1336,22 +1158,12 @@ function FloatingSelectCore({
1336
1158
  paddingVertical={0}
1337
1159
  paddingBottom={panel.coverCornerInset || 0}
1338
1160
  >
1339
- {echoed ? (
1340
- <TriggerEchoRow
1341
- sourceRef={panel.triggerRef}
1342
- source={selectedOption?.label ?? placeholder}
1343
- onPress={() => setOpen(false)}
1344
- presentational
1345
- />
1346
- ) : null}
1347
1161
  <ScrollArrow
1348
1162
  direction="up"
1349
1163
  scrollRef={panel.refs.floating}
1350
1164
  visible={panel.arrowUp}
1351
1165
  arrowScrollDirRef={panel.arrowScrollDirRef}
1352
- topOffset={
1353
- echoed ? (panel.triggerRef.current?.getBoundingClientRect().height ?? 0) : 0
1354
- }
1166
+ band={arrowBand}
1355
1167
  />
1356
1168
  {groupLabel && (
1357
1169
  <GroupHeader color={textColor} paddingHorizontal={knobProps.panelPadding.padding}>
@@ -1402,6 +1214,7 @@ function FloatingSelectCore({
1402
1214
  scrollRef={panel.refs.floating}
1403
1215
  visible={panel.arrowDown}
1404
1216
  arrowScrollDirRef={panel.arrowScrollDirRef}
1217
+ band={arrowBand}
1405
1218
  />
1406
1219
  </YStack>
1407
1220
  </FloatingFocusManager>
@@ -1434,6 +1247,7 @@ function NativeSelectCore({
1434
1247
  const insideOverlay = useInsideOverlay();
1435
1248
  const compactFace = pickerCompactFace(options.length);
1436
1249
  const asMenu = resolveCompactFace(compactFace, insideOverlay) === "menu";
1250
+ const beside = asMenu && insideOverlay && opensBesideTrigger(options.length);
1437
1251
  const hydrationTouch = useTouchSurface();
1438
1252
  const describedBy = useFieldDescribedBy();
1439
1253
  const fieldA11y = useFieldA11y();
@@ -1497,6 +1311,13 @@ function NativeSelectCore({
1497
1311
  }
1498
1312
  }, [open, selectedIndex]);
1499
1313
 
1314
+ // LC-112 (FZ-8655): the native pop-up puts this row on the field, measured
1315
+ // fresh each open.
1316
+ const [onFieldRow, setOnFieldRow] = useState<{ top: number; height: number } | null>(null);
1317
+ useEffect(() => {
1318
+ if (!open) setOnFieldRow(null);
1319
+ }, [open]);
1320
+
1500
1321
  const handleSelect = useCallback(
1501
1322
  (optionValue: string) => {
1502
1323
  if (multiple) {
@@ -1629,7 +1450,7 @@ function NativeSelectCore({
1629
1450
  >
1630
1451
  {option.label}
1631
1452
  </Text>
1632
- {selected && <CheckIcon size={14} weight="bold" color={markColor} />}
1453
+ {selected && <CheckGlyph />}
1633
1454
  </View>
1634
1455
  );
1635
1456
  if (isWeb) return row;
@@ -1641,6 +1462,12 @@ function NativeSelectCore({
1641
1462
  accessibilityRole="button"
1642
1463
  accessibilityLabel={option.label}
1643
1464
  accessibilityState={{ selected }}
1465
+ onLayout={
1466
+ i === Math.max(selectedIndex, 0)
1467
+ ? ({ nativeEvent: { layout } }) =>
1468
+ setOnFieldRow({ top: layout.y, height: layout.height })
1469
+ : undefined
1470
+ }
1644
1471
  >
1645
1472
  {row}
1646
1473
  </RNPressable>
@@ -1742,9 +1569,11 @@ function NativeSelectCore({
1742
1569
  // sheet at page level and opens as a menu inside another overlay (FZ-7830).
1743
1570
  compactFace={compactFace}
1744
1571
  panelWidth={panelWidth}
1745
- {...(asMenu && insideOverlay && opensBesideTrigger(options.length)
1572
+ {...(beside
1746
1573
  ? { anchorMode: "attach" as const, triggerEcho: false }
1747
- : null)}
1574
+ : isWeb || displayOptions.length === 0
1575
+ ? null
1576
+ : { popUp: { row: onFieldRow } })}
1748
1577
  triggerA11y={{
1749
1578
  label: ariaLabel ?? fieldA11y?.label,
1750
1579
  value: triggerValueText,
@@ -21,7 +21,7 @@ import {
21
21
  } from "react-native";
22
22
  import { platform } from "@multiplatform.one/platform";
23
23
  import { SafeAreaInsetsContext, initialWindowMetrics } from "react-native-safe-area-context";
24
- import { useResolvedKnobs, Text } from "@multiplatform.one/theme";
24
+ import { useResolvedKnobs, Text, overlayScrim } from "@multiplatform.one/theme";
25
25
  import { View, XStack, YStack, useTheme, useThemeName } from "tamagui";
26
26
  import { ArrowRightIcon } from "@phosphor-icons/react";
27
27
  import { IconButton } from "../../Button";
@@ -159,7 +159,7 @@ export function OsDateTimeSheet({
159
159
  right={0}
160
160
  bottom={0}
161
161
  left={0}
162
- backgroundColor="$shadow6"
162
+ backgroundColor={overlayScrim}
163
163
  />
164
164
  <YStack
165
165
  testID="os-date-time-sheet-unavailable"
@@ -205,7 +205,7 @@ export function OsDateTimeSheet({
205
205
  */}
206
206
  <RNView style={[styles.modalOverlay, { paddingBottom: keyboardInset }]}>
207
207
  <Pressable style={StyleSheet.absoluteFill} onPress={onCancel}>
208
- <View pointerEvents="none" flex={1} backgroundColor="$shadow6" />
208
+ <View pointerEvents="none" flex={1} backgroundColor={overlayScrim} />
209
209
  </Pressable>
210
210
  <Pressable onPress={(e) => e.stopPropagation()}>
211
211
  <YStack
package/src/index.ts CHANGED
@@ -310,6 +310,8 @@ export { PanelHeader } from "./FloatingPanel/PanelHeader";
310
310
  export { PanelPortal } from "./FloatingPanel/PanelPortal";
311
311
  export type { PanelPortalProps } from "./FloatingPanel/PanelPortal";
312
312
  export { containsAcrossPanels } from "./shared/panelPortal";
313
+ export { CheckGlyph } from "./shared/CheckGlyph";
314
+ export { useInPanelSheet } from "./FloatingPanel/echoContext";
313
315
  export type {
314
316
  FloatingPanelProps,
315
317
  PanelWidthMode,
@@ -317,7 +319,6 @@ export type {
317
319
  UseFloatingPanelReturn,
318
320
  } from "./FloatingPanel";
319
321
  export type { PanelWidth } from "./FloatingPanel/panelWidth";
320
- export type { IconEcho } from "./FloatingPanel/iconEcho";
321
322
 
322
323
  // ── Adaptive Popup (Dialog / Drawer on desktop, Sheet on mobile) ─
323
324
  export { AdaptivePopup, pickAdaptivePopupFace } from "./AdaptivePopup";
@@ -0,0 +1,12 @@
1
+ import { CheckIcon } from "@phosphor-icons/react";
2
+ import { menuRowCheckGlyph, type MenuRowGlyph, useMenuRowCheckInk } from "@multiplatform.one/theme";
3
+
4
+ /**
5
+ * The check on a checked menu or picker row (LC-262, FZ-8557): the theme's
6
+ * `menuRowCheckGlyph` recipe in the check ink. A row with an icon passes
7
+ * `menuRowIconGlyph`, so the check takes the icons' stroke (FZ-8557 r3). Components draws the same glyph through its own twin, and a spec keeps the two equal.
8
+ */
9
+ export function CheckGlyph({ glyph = menuRowCheckGlyph }: { glyph?: MenuRowGlyph }) {
10
+ const ink = useMenuRowCheckInk();
11
+ return <CheckIcon size={glyph.size} weight={glyph.weight} color={ink} />;
12
+ }
@@ -0,0 +1,116 @@
1
+ export const scrollArrowThreshold = 8;
2
+
3
+ /**
4
+ * A scroll arrow's band: half a menu row, opaque in the panel's own fill.
5
+ * Rows scroll under it, and a row cut at a clipped edge hides under it
6
+ * (FZ-8557, LC-112).
7
+ */
8
+ export function scrollArrowBand(rowHeight: number): number {
9
+ return Math.round(rowHeight / 2);
10
+ }
11
+
12
+ export interface ArrowBandFit {
13
+ /** The padding box's top edge, in viewport pixels. */
14
+ top: number;
15
+ /** The padding box's visible height. */
16
+ height: number;
17
+ scrollTop: number;
18
+ }
19
+
20
+ export interface CheckedOnFieldInput {
21
+ /** The field's edges, in viewport pixels: the checked row centres on them. */
22
+ fieldTop: number;
23
+ fieldBottom: number;
24
+ /** The checked row's offset in the scroll content, and its height. */
25
+ rowTop: number;
26
+ rowHeight: number;
27
+ /** The scroll content's full height. */
28
+ contentHeight: number;
29
+ /** The safe edges the panel's padding box may reach. */
30
+ boundsTop: number;
31
+ boundsBottom: number;
32
+ band: number;
33
+ threshold?: number;
34
+ }
35
+
36
+ const clamp = (value: number, low: number, high: number) => Math.max(low, Math.min(high, value));
37
+
38
+ /**
39
+ * A Select covering its field opens the way a macOS pop-up does: its checked
40
+ * row sits on the field, centred on it, so the value stays where the closed
41
+ * field drew it (FZ-8557, LC-112).
42
+ *
43
+ * A list that fits between the safe edges shows every row with no scrolling,
44
+ * slid the least distance that keeps it on screen. A taller list keeps the
45
+ * checked row on the field and clips at the safe edges: a clipped edge sits on
46
+ * the safe edge, and the rows past it scroll under the opaque arrow band there.
47
+ * The checked row is never under a band; where one would cover it, the panel
48
+ * or the list shifts by that much. Returns the padding box.
49
+ */
50
+ export function placeCheckedOnField(input: CheckedOnFieldInput): ArrowBandFit {
51
+ const { fieldTop, fieldBottom, rowTop, rowHeight, contentHeight, boundsTop, boundsBottom, band } =
52
+ input;
53
+ const threshold = input.threshold ?? scrollArrowThreshold;
54
+ const target = clamp(
55
+ (fieldTop + fieldBottom) / 2 - rowHeight / 2,
56
+ boundsTop,
57
+ boundsBottom - rowHeight,
58
+ );
59
+ if (contentHeight <= boundsBottom - boundsTop) {
60
+ const top = clamp(target - rowTop, boundsTop, boundsBottom - contentHeight);
61
+ return { top, height: contentHeight, scrollTop: 0 };
62
+ }
63
+ let top = target - rowTop;
64
+ let scrollTop = 0;
65
+ if (top < boundsTop) {
66
+ scrollTop = boundsTop - top;
67
+ top = boundsTop;
68
+ }
69
+ const end = () => top - scrollTop + contentHeight;
70
+ const bottom = () => Math.min(boundsBottom, end());
71
+ const checkedTop = () => top - scrollTop + rowTop;
72
+ // Clear of the down band: the panel's top lifts first, then the list scrolls.
73
+ if (end() - bottom() > threshold) {
74
+ const short = checkedTop() + rowHeight - (bottom() - band);
75
+ if (short > 0) {
76
+ const lift = scrollTop > 0 ? 0 : Math.min(short, top - boundsTop);
77
+ top -= lift;
78
+ scrollTop += short - lift;
79
+ }
80
+ }
81
+ // Clear of the up band: the list drops, never past its start.
82
+ if (scrollTop > threshold) {
83
+ const short = top + band - checkedTop();
84
+ if (short > 0) scrollTop = Math.max(0, scrollTop - short);
85
+ }
86
+ // A scroll too small to show an arrow is the panel's own offset instead.
87
+ if (scrollTop > 0 && scrollTop <= threshold) {
88
+ top += scrollTop;
89
+ scrollTop = 0;
90
+ }
91
+ return { top, height: bottom() - top, scrollTop };
92
+ }
93
+
94
+ /**
95
+ * A scrolled list whose panel stops short of a safe edge grows toward it, the
96
+ * way a macOS menu does: the edge follows the rows until it meets the safe
97
+ * edge, and the rows stay where the scroll put them (FZ-8463, FZ-8557). The
98
+ * panel never shrinks. Takes and returns the padding box.
99
+ */
100
+ export function growOnScroll({
101
+ top,
102
+ height,
103
+ scrollTop,
104
+ contentHeight,
105
+ boundsTop,
106
+ boundsBottom,
107
+ }: ArrowBandFit & {
108
+ contentHeight: number;
109
+ boundsTop: number;
110
+ boundsBottom: number;
111
+ }): ArrowBandFit {
112
+ const origin = top - scrollTop;
113
+ const nextTop = Math.min(top, Math.max(boundsTop, origin));
114
+ const nextBottom = Math.max(top + height, Math.min(boundsBottom, origin + contentHeight));
115
+ return { top: nextTop, height: nextBottom - nextTop, scrollTop: nextTop - origin };
116
+ }