@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/forms",
3
- "version": "7.80.0",
3
+ "version": "7.81.1",
4
4
  "description": "Cross-platform form components with URL state management",
5
5
  "keywords": [
6
6
  "cross-platform",
@@ -57,10 +57,10 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@floating-ui/react": "^0.27.20",
60
- "@multiplatform.one/platform": "^7.80.0",
61
- "@multiplatform.one/router": "^7.80.0",
62
- "@multiplatform.one/table-primitives": "^7.80.0",
63
- "@multiplatform.one/theme": "^7.80.0",
60
+ "@multiplatform.one/platform": "^7.81.1",
61
+ "@multiplatform.one/router": "^7.81.1",
62
+ "@multiplatform.one/table-primitives": "^7.81.1",
63
+ "@multiplatform.one/theme": "^7.81.1",
64
64
  "@phosphor-icons/react": "^2.1.10",
65
65
  "@tamagui/get-font-sized": "2.7.6",
66
66
  "@tamagui/get-token": "2.7.6",
@@ -80,9 +80,9 @@
80
80
  },
81
81
  "devDependencies": {
82
82
  "@expo/ui": "~55.0.17",
83
- "@multiplatform.one/config": "7.80.0",
84
- "@multiplatform.one/storybook": "7.80.0",
85
- "@multiplatform.one/test-utils": "7.80.0",
83
+ "@multiplatform.one/config": "7.81.1",
84
+ "@multiplatform.one/storybook": "7.81.1",
85
+ "@multiplatform.one/test-utils": "7.81.1",
86
86
  "@tamagui/build": "2.7.6",
87
87
  "@tamagui/config": "2.7.6",
88
88
  "@tamagui/vite-plugin": "2.7.6",
@@ -1,4 +1,4 @@
1
- import { transitionProps } from "@multiplatform.one/theme";
1
+ import { sheetGrabber, transitionProps, overlayScrim } from "@multiplatform.one/theme";
2
2
  import { DialogContent, DialogOverlay } from "@multiplatform.one/theme";
3
3
  import {
4
4
  cloneElement,
@@ -399,12 +399,15 @@ export function AdaptivePopup({
399
399
  {...transitionProps(knobProps.motion.slide)}
400
400
  >
401
401
  <Sheet.Overlay
402
- backgroundColor="$shadow6"
402
+ backgroundColor={overlayScrim}
403
403
  {...transitionProps(knobProps.motion.scrim)}
404
404
  enterStyle={{ opacity: 0 }}
405
405
  exitStyle={{ opacity: 0 }}
406
406
  />
407
- <Sheet.Handle {...({ "aria-hidden": true } as Record<string, unknown>)} />
407
+ <Sheet.Handle
408
+ {...sheetGrabber}
409
+ {...({ "aria-hidden": true } as Record<string, unknown>)}
410
+ />
408
411
  <Sheet.Frame
409
412
  // Sheet.Frame forwards its ref at runtime; the styled prop type
410
413
  // just omits `ref`, hence the cast. tabIndex -1 makes the frame a
@@ -112,6 +112,45 @@ function rest(el: Element, prefix: string): string[] {
112
112
  .sort();
113
113
  }
114
114
 
115
+ describe("LC-176: one ink on a bold glyph (FZ-8649)", () => {
116
+ function Inked({ color, stroke }: { color?: string; stroke?: string }) {
117
+ return <svg data-glyph="" data-color={color} data-stroke={stroke} />;
118
+ }
119
+
120
+ it.each(SCHEMES)("%s: a bar glyph takes the bar tint on its fill and its stroke", (scheme) => {
121
+ houseRender(
122
+ <Surface bar>
123
+ <Button chromeless icon={Inked} aria-label="Menu" />
124
+ <Button chromeless icon={<Inked />} aria-label="Bell" />
125
+ </Surface>,
126
+ scheme,
127
+ );
128
+ for (const label of ["Menu", "Bell"]) {
129
+ const ink = glyphOf(label).getAttribute("data-color");
130
+ expect(ink).toBeTruthy();
131
+ expect(ink).not.toBe("currentColor");
132
+ expect(glyphOf(label).getAttribute("data-stroke")).toBe(ink);
133
+ }
134
+ });
135
+
136
+ it("a caller's colour stays on the fill and the stroke", () => {
137
+ houseRender(
138
+ <Surface bar>
139
+ <Button chromeless icon={<Inked color="red" />} aria-label="Bell" />
140
+ </Surface>,
141
+ "light",
142
+ );
143
+ expect(glyphOf("Bell").getAttribute("data-color")).toBe("red");
144
+ expect(glyphOf("Bell").getAttribute("data-stroke")).toBe("red");
145
+ });
146
+
147
+ it("a content glyph has no bar tint to pass, so it keeps currentColor", () => {
148
+ houseRender(<Button icon={Inked} aria-label="More" />, "light");
149
+ expect(glyphOf("More").hasAttribute("data-color")).toBe(false);
150
+ expect(glyphOf("More").getAttribute("data-stroke")).toBe("currentColor");
151
+ });
152
+ });
153
+
115
154
  describe("LC-176: an icon-only Button is a bare glyph", () => {
116
155
  it.each(SCHEMES)("%s: in a bar, no fill and no border at rest, the bar tint as ink", (scheme) => {
117
156
  houseRender(
@@ -465,28 +465,40 @@ export function iconButtonGlyphPx(rowLinePx: number | undefined): number {
465
465
  /** LC-176: an inline glyph is the text's size; Phosphor's 256 grid pads ~12%. */
466
466
  const INLINE_GLYPH_RATIO = 1.2;
467
467
 
468
+ /**
469
+ * `ink` is the bar tint. It goes to the glyph as well as to its stroke, because
470
+ * a `themed` glyph fills with the theme's text colour unless told otherwise and
471
+ * the stroke would then outline it in a second colour (FZ-8649).
472
+ */
468
473
  function withGlyphWeight(
469
474
  icon: IconLike | undefined,
470
475
  size: number,
471
476
  weight: "regular" | "bold",
472
477
  floor = false,
478
+ ink?: string | null,
473
479
  ): IconLike | undefined {
474
480
  if (icon == null || icon === false) return icon;
481
+ const tint = ink ?? undefined;
475
482
  if (isValidElement<{ size?: number; weight?: string; color?: string }>(icon)) {
476
483
  const resolved = (icon.props.weight ?? weight) as "regular" | "bold";
477
484
  const own = icon.props.size;
478
485
  return cloneElement(icon, {
479
486
  ...(own == null || (floor && typeof own === "number" && own < size) ? { size } : undefined),
480
487
  ...(icon.props.weight == null ? { weight } : undefined),
481
- ...boldStroke(resolved, icon.props.color),
488
+ ...(icon.props.color == null && tint ? { color: tint } : undefined),
489
+ ...boldStroke(resolved, icon.props.color ?? tint),
482
490
  });
483
491
  }
484
492
  if (typeof icon === "function" || (typeof icon === "object" && "$$typeof" in icon)) {
485
- return createElement(icon as ComponentType<{ size?: number; weight?: string }>, {
486
- size,
487
- weight,
488
- ...boldStroke(weight),
489
- });
493
+ return createElement(
494
+ icon as ComponentType<{ size?: number; weight?: string; color?: string }>,
495
+ {
496
+ size,
497
+ weight,
498
+ ...(tint ? { color: tint } : undefined),
499
+ ...boldStroke(weight, tint),
500
+ },
501
+ );
490
502
  }
491
503
  return icon;
492
504
  }
@@ -911,7 +923,7 @@ export function Button({
911
923
  const barGlyphFloor = barGlyph && !framedCircle && !inlineGlyph && !nested;
912
924
  const glyph =
913
925
  barGlyph || inlineGlyph || framedCircle
914
- ? withGlyphWeight(icon, iconSize, resolvedGlyphWeight, barGlyphFloor)
926
+ ? withGlyphWeight(icon, iconSize, resolvedGlyphWeight, barGlyphFloor, barInk)
915
927
  : icon;
916
928
 
917
929
  // LC-158: the frame keeps its recipe height under Dynamic Type, so the
@@ -998,7 +1010,7 @@ export function Button({
998
1010
  ) : null;
999
1011
  const glyphAfter =
1000
1012
  iconAfter && (barGlyph || inlineGlyph || framedCircle)
1001
- ? withGlyphWeight(iconAfter, iconSize, resolvedGlyphWeight, barGlyphFloor)
1013
+ ? withGlyphWeight(iconAfter, iconSize, resolvedGlyphWeight, barGlyphFloor, barInk)
1002
1014
  : iconAfter;
1003
1015
  const iconAfterElement = glyphAfter ? (
1004
1016
  <View flexShrink={0}>
@@ -0,0 +1,279 @@
1
+ /**
2
+ * LC-112 icon trigger cover (FZ-8665 Round 45, reversing Round 37's X): a
3
+ * panel that covers the icon button it opened from sits exactly over the
4
+ * surface that button is seen on and repeats nothing. Its content starts at
5
+ * its top edge, a press outside closes it, and Escape closes it on the web.
6
+ */
7
+ import { PlusIcon } from "@phosphor-icons/react";
8
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
9
+ import { fireEvent, waitFor, within } from "@testing-library/react";
10
+ import { useState } from "react";
11
+ import { MIN_PRESS_TARGET } from "@multiplatform.one/theme";
12
+ import { afterEach, describe, expect, it, vi } from "vitest";
13
+ import { Text, XStack } from "tamagui";
14
+ import { Button } from "../Button";
15
+ import { FloatingPanel } from "./index";
16
+ import { PanelHeader } from "./PanelHeader";
17
+
18
+ type Rect = { x: number; y: number; width: number; height: number };
19
+
20
+ function measure(trigger: Rect, panelWidth = 240, panelHeight = 160, surface?: Rect) {
21
+ const original = HTMLElement.prototype.getBoundingClientRect;
22
+ const rect = {
23
+ ...trigger,
24
+ top: trigger.y,
25
+ left: trigger.x,
26
+ right: trigger.x + trigger.width,
27
+ bottom: trigger.y + trigger.height,
28
+ toJSON: () => ({}),
29
+ };
30
+ vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
31
+ function (this: HTMLElement) {
32
+ if (this.hasAttribute("data-fp-reference")) return rect;
33
+ if (surface && this.hasAttribute("data-fp-cover-surface")) {
34
+ return {
35
+ ...surface,
36
+ top: surface.y,
37
+ left: surface.x,
38
+ right: surface.x + surface.width,
39
+ bottom: surface.y + surface.height,
40
+ toJSON: () => ({}),
41
+ };
42
+ }
43
+ return original.call(this);
44
+ },
45
+ );
46
+ vi.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockImplementation(
47
+ function (this: HTMLElement) {
48
+ return this.hasAttribute("data-fp-panel") ? panelWidth : 0;
49
+ },
50
+ );
51
+ vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockImplementation(
52
+ function (this: HTMLElement) {
53
+ return this.hasAttribute("data-fp-panel") ? panelHeight : 0;
54
+ },
55
+ );
56
+ }
57
+
58
+ function AddPanel({ header = false }: { header?: boolean }) {
59
+ const [open, setOpen] = useState(false);
60
+ return (
61
+ <FloatingPanel
62
+ open={open}
63
+ onOpenChange={setOpen}
64
+ anchorMode="cover-trigger"
65
+ compactFace="menu"
66
+ triggerEcho={false}
67
+ iconCover
68
+ coverAlign="auto"
69
+ widthMode="fit-content"
70
+ triggerSizing="content"
71
+ contentPadding="none"
72
+ trigger={<Button icon={PlusIcon} aria-label="Add node" />}
73
+ >
74
+ {header ? (
75
+ <PanelHeader>
76
+ <Text>Add a node</Text>
77
+ </PanelHeader>
78
+ ) : (
79
+ <Text>Add a node</Text>
80
+ )}
81
+ <Button>Copy</Button>
82
+ </FloatingPanel>
83
+ );
84
+ }
85
+
86
+ /** An account menu opened from a 32pt avatar: a trigger with no `icon`. */
87
+ function AvatarPanel() {
88
+ return (
89
+ <FloatingPanel
90
+ open
91
+ onOpenChange={() => {}}
92
+ anchorMode="cover-trigger"
93
+ compactFace="menu"
94
+ triggerEcho={false}
95
+ iconCover
96
+ coverAlign="auto"
97
+ widthMode="fit-content"
98
+ triggerSizing="content"
99
+ contentPadding="none"
100
+ trigger={
101
+ <span role="button" aria-label="Account">
102
+ AD
103
+ </span>
104
+ }
105
+ >
106
+ <Text>Sign out</Text>
107
+ </FloatingPanel>
108
+ );
109
+ }
110
+
111
+ async function open(result: ReturnType<typeof renderWithProviders>) {
112
+ fireEvent.click(result.getByRole("button", { name: "Add node" }), { detail: 0 });
113
+ const panel = (await result.findByTestId("floating-panel-viewport")) as HTMLElement;
114
+ await waitFor(() => expect(panel.style.visibility).toBe("visible"));
115
+ return panel;
116
+ }
117
+
118
+ afterEach(() => {
119
+ vi.restoreAllMocks();
120
+ });
121
+
122
+ const buttonsIn = (panel: HTMLElement) =>
123
+ within(panel)
124
+ .getAllByRole("button")
125
+ .map((button) => button.textContent || button.getAttribute("aria-label"));
126
+
127
+ /** Whatever sits above the content in the panel: a hidden scroll arrow at most. */
128
+ const aboveContent = (panel: HTMLElement) => {
129
+ const children = [...panel.children] as HTMLElement[];
130
+ const content = children.findIndex((child) => child.textContent?.includes("Add a node"));
131
+ expect(content).toBeGreaterThanOrEqual(0);
132
+ return children.slice(0, content);
133
+ };
134
+
135
+ describe("LC-112 icon trigger cover on the web (FZ-8665)", () => {
136
+ const viewportWidth = () => document.documentElement.clientWidth || window.innerWidth;
137
+
138
+ it("covers the + from the button's top trailing corner and repeats nothing", async () => {
139
+ const button = { x: viewportWidth() - 16 - 44, y: 12, width: 44, height: 44 };
140
+ measure(button, 240);
141
+ const result = renderWithProviders(<AddPanel />);
142
+ const panel = await open(result);
143
+ expect(panel.style.top).toBe(`${button.y}px`);
144
+ expect(panel.style.left).toBe(`${button.x + button.width - 240}px`);
145
+ expect(within(panel).queryByRole("button", { name: "Close" })).toBeNull();
146
+ expect(within(panel).queryByRole("button", { name: "Add node" })).toBeNull();
147
+ expect(panel.querySelector("[data-fp-trigger-row]")).toBeNull();
148
+ expect(panel.querySelector("[data-fp-icon-echo]")).toBeNull();
149
+ });
150
+
151
+ it("starts the content at the panel's top edge, with no band above it", async () => {
152
+ measure({ x: 16, y: 12, width: 44, height: 44 });
153
+ const result = renderWithProviders(<AddPanel />);
154
+ const panel = await open(result);
155
+ for (const element of aboveContent(panel)) {
156
+ expect(element.textContent).toBe("");
157
+ expect(element.querySelector("button, [role='button']")).toBeNull();
158
+ expect(element.style.height).toBe("");
159
+ }
160
+ });
161
+
162
+ it("covers a grouped trigger's whole plate, with both edges, and shows none of its buttons", async () => {
163
+ const button = { x: viewportWidth() - 16 - 44, y: 12, width: 44, height: 44 };
164
+ const plate = { x: button.x - 100, y: 12, width: 144, height: 44 };
165
+ measure(button, 120, 160, plate);
166
+ const result = renderWithProviders(
167
+ <div data-fp-cover-surface="">
168
+ <Button icon={PlusIcon} aria-label="Search" />
169
+ <AddPanel />
170
+ </div>,
171
+ );
172
+ const panel = await open(result);
173
+ // The plate is wider than the menu, so the menu takes the plate's width
174
+ // and both edges.
175
+ expect(panel.style.left).toBe(`${plate.x}px`);
176
+ expect(panel.style.top).toBe(`${plate.y}px`);
177
+ expect(panel.style.minWidth).toBe(`${plate.width}px`);
178
+ expect(buttonsIn(panel)).toEqual(["Copy"]);
179
+ });
180
+
181
+ it("closes on a press outside the panel", async () => {
182
+ measure({ x: 16, y: 12, width: 44, height: 44 });
183
+ const result = renderWithProviders(<AddPanel />);
184
+ await open(result);
185
+ fireEvent.click(document.body);
186
+ await waitFor(() => expect(result.queryByTestId("floating-panel-viewport")).toBeNull());
187
+ });
188
+
189
+ it("closes on Escape and hands focus back to the button", async () => {
190
+ measure({ x: 16, y: 12, width: 44, height: 44 });
191
+ const result = renderWithProviders(<AddPanel />);
192
+ const trigger = result.getByRole("button", { name: "Add node" });
193
+ trigger.focus();
194
+ await open(result);
195
+ fireEvent.keyDown(document, { key: "Escape" });
196
+ await waitFor(() => expect(result.queryByTestId("floating-panel-viewport")).toBeNull());
197
+ await waitFor(() => expect(document.activeElement).toBe(trigger));
198
+ });
199
+
200
+ it("puts a panel's title on the panel's normal padding, keeping clear of nothing", async () => {
201
+ const button = { x: viewportWidth() - 16 - 44, y: 12, width: 44, height: 44 };
202
+ measure(button, 240);
203
+ const result = renderWithProviders(
204
+ <>
205
+ <AddPanel header />
206
+ <XStack
207
+ data-testid="normal-padding"
208
+ alignItems="center"
209
+ gap="$2"
210
+ paddingLeft="$4"
211
+ paddingRight="$4"
212
+ paddingTop="$4"
213
+ />
214
+ </>,
215
+ );
216
+ const panel = await open(result);
217
+ const header = panel.querySelector<HTMLElement>("[data-fp-panel-header]") as HTMLElement;
218
+ expect(header.className).toBe(result.getByTestId("normal-padding").className);
219
+ expect(header.style.minHeight).toBe("");
220
+ });
221
+
222
+ it("keeps a content-sized trigger on a centring row's line", async () => {
223
+ measure({ x: 16, y: 12, width: 44, height: 44 });
224
+ const centred = renderWithProviders(
225
+ <div style={{ display: "flex", flexDirection: "row", alignItems: "center", height: 60 }}>
226
+ <AddPanel />
227
+ </div>,
228
+ );
229
+ const inRow = centred.container.querySelector<HTMLElement>(
230
+ "[data-fp-reference]",
231
+ ) as HTMLElement;
232
+ await waitFor(() => expect(getComputedStyle(inRow).alignSelf).not.toBe("flex-start"));
233
+ centred.unmount();
234
+ const column = renderWithProviders(
235
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "stretch" }}>
236
+ <AddPanel />
237
+ </div>,
238
+ );
239
+ const inColumn = column.container.querySelector<HTMLElement>(
240
+ "[data-fp-reference]",
241
+ ) as HTMLElement;
242
+ expect(getComputedStyle(inColumn).alignSelf).toBe("flex-start");
243
+ });
244
+
245
+ it("gives a keyboard its content and no extra stop", async () => {
246
+ measure({ x: 16, y: 12, width: 44, height: 44 });
247
+ const result = renderWithProviders(<AddPanel />);
248
+ const panel = await open(result);
249
+ expect(buttonsIn(panel)).toEqual(["Copy"]);
250
+ });
251
+
252
+ it("opens up from the button's bottom edge near the bottom, still covering it", async () => {
253
+ const button = { x: 16, y: window.innerHeight - 60, width: 44, height: 44 };
254
+ measure(button, 240, 300);
255
+ const result = renderWithProviders(<AddPanel />);
256
+ const panel = await open(result);
257
+ const frame = getComputedStyle(panel);
258
+ const borders =
259
+ (parseFloat(frame.borderTopWidth) || 0) + (parseFloat(frame.borderBottomWidth) || 0);
260
+ expect(parseFloat(panel.style.top) + 300 + borders).toBe(button.y + button.height);
261
+ expect(panel.style.left).toBe(`${button.x}px`);
262
+ expect(panel.style.transformOrigin).toBe("bottom left");
263
+ expect(result.queryByTestId("floating-panel-sheet")).toBeNull();
264
+ });
265
+
266
+ it("covers a 32pt avatar at its 44pt target, its centre on the row-icon column (FZ-8480 GAP)", async () => {
267
+ const avatar = { x: 100, y: 18, width: 32, height: 32 };
268
+ measure(avatar, 240);
269
+ const result = renderWithProviders(<AvatarPanel />);
270
+ const panel = (await result.findByTestId("floating-panel-viewport")) as HTMLElement;
271
+ await waitFor(() => expect(panel.style.visibility).toBe("visible"));
272
+ const inset = (MIN_PRESS_TARGET - avatar.width) / 2;
273
+ expect(panel.style.left).toBe(`${avatar.x - inset}px`);
274
+ expect(panel.style.top).toBe(`${avatar.y - inset}px`);
275
+ // The avatar's centre sits half a press target in, the centre of a menu
276
+ // row's leading glyph slot.
277
+ expect(inset + avatar.width / 2).toBe(MIN_PRESS_TARGET / 2);
278
+ });
279
+ });