@multiplatform.one/forms 7.77.1 → 7.78.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 (296) hide show
  1. package/dist/cjs/Button/IconButtonGroup.cjs +58 -27
  2. package/dist/cjs/Button/IconButtonGroup.native.js +64 -29
  3. package/dist/cjs/Button/IconButtonGroup.native.js.map +1 -1
  4. package/dist/cjs/Button/circleGlyph.cjs +12 -2
  5. package/dist/cjs/Button/circleGlyph.native.js +12 -1
  6. package/dist/cjs/Button/circleGlyph.native.js.map +1 -1
  7. package/dist/cjs/Button/glass.cjs +2 -1
  8. package/dist/cjs/Button/glass.native.js +44 -19
  9. package/dist/cjs/Button/glass.native.js.map +1 -1
  10. package/dist/cjs/Button/iconButtonGroupContext.cjs +7 -1
  11. package/dist/cjs/Button/iconButtonGroupContext.native.js +7 -1
  12. package/dist/cjs/Button/iconButtonGroupContext.native.js.map +1 -1
  13. package/dist/cjs/Button/index.cjs +15 -23
  14. package/dist/cjs/Button/index.native.js +15 -24
  15. package/dist/cjs/Button/index.native.js.map +1 -1
  16. package/dist/cjs/Button/webGlass.cjs +9 -5
  17. package/dist/cjs/Button/webGlass.native.js +9 -5
  18. package/dist/cjs/Button/webGlass.native.js.map +1 -1
  19. package/dist/cjs/FloatingPanel/coverRect.cjs +50 -0
  20. package/dist/cjs/FloatingPanel/coverRect.native.js +54 -0
  21. package/dist/cjs/FloatingPanel/coverRect.native.js.map +1 -0
  22. package/dist/cjs/FloatingPanel/iconEcho.cjs +3 -1
  23. package/dist/cjs/FloatingPanel/iconEcho.native.js +3 -1
  24. package/dist/cjs/FloatingPanel/iconEcho.native.js.map +1 -1
  25. package/dist/cjs/FloatingPanel/index.cjs +23 -13
  26. package/dist/cjs/FloatingPanel/index.native.js +41 -15
  27. package/dist/cjs/FloatingPanel/index.native.js.map +1 -1
  28. package/dist/cjs/InputParts/index.cjs +5 -0
  29. package/dist/cjs/InputParts/index.native.js +7 -0
  30. package/dist/cjs/InputParts/index.native.js.map +1 -1
  31. package/dist/cjs/{Button/mediaPlate.cjs → InputParts/keyboardAppearance.cjs} +7 -8
  32. package/dist/{jsx/Button/mediaPlate.native.js → cjs/InputParts/keyboardAppearance.native.js} +8 -9
  33. package/dist/cjs/InputParts/keyboardAppearance.native.js.map +1 -0
  34. package/dist/cjs/{Button/mediaPlate.native.js → fields/Duration/DurationWheel.cjs} +11 -11
  35. package/dist/cjs/fields/Duration/DurationWheel.native.js +132 -0
  36. package/dist/cjs/fields/Duration/DurationWheel.native.js.map +1 -0
  37. package/dist/cjs/fields/Duration/durationMath.cjs +19 -1
  38. package/dist/cjs/fields/Duration/durationMath.native.js +19 -1
  39. package/dist/cjs/fields/Duration/durationMath.native.js.map +1 -1
  40. package/dist/cjs/fields/Duration/durationWheelTypes.cjs +18 -0
  41. package/dist/cjs/fields/Duration/durationWheelTypes.native.js +21 -0
  42. package/dist/cjs/fields/Duration/durationWheelTypes.native.js.map +1 -0
  43. package/dist/cjs/fields/Duration/index.cjs +30 -10
  44. package/dist/cjs/fields/Duration/index.native.js +32 -12
  45. package/dist/cjs/fields/Duration/index.native.js.map +1 -1
  46. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.cjs +8 -7
  47. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.native.js +8 -7
  48. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  49. package/dist/cjs/fields/nativeDateTime/osDateTimeIosDisplay.cjs +1 -1
  50. package/dist/cjs/fields/nativeDateTime/osDateTimeIosDisplay.native.js +1 -1
  51. package/dist/cjs/fields/nativeDateTime/osDateTimeIosDisplay.native.js.map +1 -1
  52. package/dist/cjs/fields/nativeDateTime/sheetKeyboard.cjs +1 -1
  53. package/dist/cjs/fields/nativeDateTime/sheetKeyboard.native.js +1 -1
  54. package/dist/cjs/fields/nativeDateTime/sheetKeyboard.native.js.map +1 -1
  55. package/dist/cjs/index.cjs +0 -1
  56. package/dist/cjs/index.native.js +0 -1
  57. package/dist/cjs/index.native.js.map +1 -1
  58. package/dist/esm/Button/IconButtonGroup.mjs +65 -33
  59. package/dist/esm/Button/IconButtonGroup.mjs.map +1 -1
  60. package/dist/esm/Button/IconButtonGroup.native.js +71 -35
  61. package/dist/esm/Button/IconButtonGroup.native.js.map +1 -1
  62. package/dist/esm/Button/circleGlyph.mjs +9 -1
  63. package/dist/esm/Button/circleGlyph.mjs.map +1 -1
  64. package/dist/esm/Button/circleGlyph.native.js +10 -1
  65. package/dist/esm/Button/circleGlyph.native.js.map +1 -1
  66. package/dist/esm/Button/glass.mjs +2 -1
  67. package/dist/esm/Button/glass.mjs.map +1 -1
  68. package/dist/esm/Button/glass.native.js +43 -19
  69. package/dist/esm/Button/glass.native.js.map +1 -1
  70. package/dist/esm/Button/iconButtonGroupContext.mjs +5 -1
  71. package/dist/esm/Button/iconButtonGroupContext.mjs.map +1 -1
  72. package/dist/esm/Button/iconButtonGroupContext.native.js +5 -1
  73. package/dist/esm/Button/iconButtonGroupContext.native.js.map +1 -1
  74. package/dist/esm/Button/index.mjs +15 -22
  75. package/dist/esm/Button/index.mjs.map +1 -1
  76. package/dist/esm/Button/index.native.js +15 -23
  77. package/dist/esm/Button/index.native.js.map +1 -1
  78. package/dist/esm/Button/webGlass.mjs +10 -6
  79. package/dist/esm/Button/webGlass.mjs.map +1 -1
  80. package/dist/esm/Button/webGlass.native.js +10 -6
  81. package/dist/esm/Button/webGlass.native.js.map +1 -1
  82. package/dist/esm/FloatingPanel/coverRect.mjs +23 -0
  83. package/dist/esm/FloatingPanel/coverRect.mjs.map +1 -0
  84. package/dist/esm/FloatingPanel/coverRect.native.js +24 -0
  85. package/dist/esm/FloatingPanel/coverRect.native.js.map +1 -0
  86. package/dist/esm/FloatingPanel/iconEcho.mjs +4 -2
  87. package/dist/esm/FloatingPanel/iconEcho.mjs.map +1 -1
  88. package/dist/esm/FloatingPanel/iconEcho.native.js +4 -2
  89. package/dist/esm/FloatingPanel/iconEcho.native.js.map +1 -1
  90. package/dist/esm/FloatingPanel/index.mjs +23 -13
  91. package/dist/esm/FloatingPanel/index.mjs.map +1 -1
  92. package/dist/esm/FloatingPanel/index.native.js +41 -15
  93. package/dist/esm/FloatingPanel/index.native.js.map +1 -1
  94. package/dist/esm/InputParts/index.mjs +6 -1
  95. package/dist/esm/InputParts/index.mjs.map +1 -1
  96. package/dist/esm/InputParts/index.native.js +8 -1
  97. package/dist/esm/InputParts/index.native.js.map +1 -1
  98. package/dist/esm/InputParts/keyboardAppearance.mjs +5 -0
  99. package/dist/esm/InputParts/keyboardAppearance.mjs.map +1 -0
  100. package/dist/esm/InputParts/keyboardAppearance.native.js +5 -0
  101. package/dist/esm/InputParts/keyboardAppearance.native.js.map +1 -0
  102. package/dist/esm/fields/Duration/DurationWheel.mjs +8 -0
  103. package/dist/esm/fields/Duration/DurationWheel.mjs.map +1 -0
  104. package/dist/esm/fields/Duration/DurationWheel.native.js +103 -0
  105. package/dist/esm/fields/Duration/DurationWheel.native.js.map +1 -0
  106. package/dist/esm/fields/Duration/durationMath.mjs +16 -1
  107. package/dist/esm/fields/Duration/durationMath.mjs.map +1 -1
  108. package/dist/esm/fields/Duration/durationMath.native.js +16 -1
  109. package/dist/esm/fields/Duration/durationMath.native.js.map +1 -1
  110. package/dist/esm/fields/Duration/durationWheelTypes.mjs +2 -0
  111. package/dist/esm/fields/Duration/durationWheelTypes.mjs.map +1 -0
  112. package/dist/esm/fields/Duration/durationWheelTypes.native.js +2 -0
  113. package/dist/esm/fields/Duration/durationWheelTypes.native.js.map +1 -0
  114. package/dist/esm/fields/Duration/index.mjs +28 -11
  115. package/dist/esm/fields/Duration/index.mjs.map +1 -1
  116. package/dist/esm/fields/Duration/index.native.js +30 -13
  117. package/dist/esm/fields/Duration/index.native.js.map +1 -1
  118. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.mjs +8 -7
  119. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.mjs.map +1 -1
  120. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.native.js +8 -7
  121. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  122. package/dist/esm/fields/nativeDateTime/osDateTimeIosDisplay.mjs +1 -1
  123. package/dist/esm/fields/nativeDateTime/osDateTimeIosDisplay.mjs.map +1 -1
  124. package/dist/esm/fields/nativeDateTime/osDateTimeIosDisplay.native.js +1 -1
  125. package/dist/esm/fields/nativeDateTime/osDateTimeIosDisplay.native.js.map +1 -1
  126. package/dist/esm/fields/nativeDateTime/sheetKeyboard.mjs +1 -1
  127. package/dist/esm/fields/nativeDateTime/sheetKeyboard.mjs.map +1 -1
  128. package/dist/esm/fields/nativeDateTime/sheetKeyboard.native.js +1 -1
  129. package/dist/esm/fields/nativeDateTime/sheetKeyboard.native.js.map +1 -1
  130. package/dist/esm/index.mjs +2 -2
  131. package/dist/esm/index.mjs.map +1 -1
  132. package/dist/esm/index.native.js +2 -2
  133. package/dist/esm/index.native.js.map +1 -1
  134. package/dist/jsx/Button/IconButtonGroup.mjs +65 -33
  135. package/dist/jsx/Button/IconButtonGroup.mjs.map +1 -1
  136. package/dist/jsx/Button/IconButtonGroup.native.js +64 -29
  137. package/dist/jsx/Button/IconButtonGroup.native.js.map +1 -1
  138. package/dist/jsx/Button/circleGlyph.mjs +9 -1
  139. package/dist/jsx/Button/circleGlyph.mjs.map +1 -1
  140. package/dist/jsx/Button/circleGlyph.native.js +12 -1
  141. package/dist/jsx/Button/circleGlyph.native.js.map +1 -1
  142. package/dist/jsx/Button/glass.mjs +2 -1
  143. package/dist/jsx/Button/glass.mjs.map +1 -1
  144. package/dist/jsx/Button/glass.native.js +44 -19
  145. package/dist/jsx/Button/glass.native.js.map +1 -1
  146. package/dist/jsx/Button/iconButtonGroupContext.mjs +5 -1
  147. package/dist/jsx/Button/iconButtonGroupContext.mjs.map +1 -1
  148. package/dist/jsx/Button/iconButtonGroupContext.native.js +7 -1
  149. package/dist/jsx/Button/iconButtonGroupContext.native.js.map +1 -1
  150. package/dist/jsx/Button/index.mjs +15 -22
  151. package/dist/jsx/Button/index.mjs.map +1 -1
  152. package/dist/jsx/Button/index.native.js +15 -24
  153. package/dist/jsx/Button/index.native.js.map +1 -1
  154. package/dist/jsx/Button/webGlass.mjs +10 -6
  155. package/dist/jsx/Button/webGlass.mjs.map +1 -1
  156. package/dist/jsx/Button/webGlass.native.js +9 -5
  157. package/dist/jsx/Button/webGlass.native.js.map +1 -1
  158. package/dist/jsx/FloatingPanel/coverRect.mjs +23 -0
  159. package/dist/jsx/FloatingPanel/coverRect.mjs.map +1 -0
  160. package/dist/jsx/FloatingPanel/coverRect.native.js +54 -0
  161. package/dist/jsx/FloatingPanel/coverRect.native.js.map +1 -0
  162. package/dist/jsx/FloatingPanel/iconEcho.mjs +4 -2
  163. package/dist/jsx/FloatingPanel/iconEcho.mjs.map +1 -1
  164. package/dist/jsx/FloatingPanel/iconEcho.native.js +3 -1
  165. package/dist/jsx/FloatingPanel/iconEcho.native.js.map +1 -1
  166. package/dist/jsx/FloatingPanel/index.mjs +23 -13
  167. package/dist/jsx/FloatingPanel/index.mjs.map +1 -1
  168. package/dist/jsx/FloatingPanel/index.native.js +41 -15
  169. package/dist/jsx/FloatingPanel/index.native.js.map +1 -1
  170. package/dist/jsx/InputParts/index.mjs +6 -1
  171. package/dist/jsx/InputParts/index.mjs.map +1 -1
  172. package/dist/jsx/InputParts/index.native.js +7 -0
  173. package/dist/jsx/InputParts/index.native.js.map +1 -1
  174. package/dist/jsx/InputParts/keyboardAppearance.mjs +5 -0
  175. package/dist/jsx/InputParts/keyboardAppearance.mjs.map +1 -0
  176. package/dist/jsx/InputParts/keyboardAppearance.native.js +33 -0
  177. package/dist/jsx/InputParts/keyboardAppearance.native.js.map +1 -0
  178. package/dist/jsx/fields/Duration/DurationWheel.mjs +8 -0
  179. package/dist/jsx/fields/Duration/DurationWheel.mjs.map +1 -0
  180. package/dist/jsx/fields/Duration/DurationWheel.native.js +132 -0
  181. package/dist/jsx/fields/Duration/DurationWheel.native.js.map +1 -0
  182. package/dist/jsx/fields/Duration/durationMath.mjs +16 -1
  183. package/dist/jsx/fields/Duration/durationMath.mjs.map +1 -1
  184. package/dist/jsx/fields/Duration/durationMath.native.js +19 -1
  185. package/dist/jsx/fields/Duration/durationMath.native.js.map +1 -1
  186. package/dist/jsx/fields/Duration/durationWheelTypes.mjs +2 -0
  187. package/dist/jsx/fields/Duration/durationWheelTypes.mjs.map +1 -0
  188. package/dist/jsx/fields/Duration/durationWheelTypes.native.js +21 -0
  189. package/dist/jsx/fields/Duration/durationWheelTypes.native.js.map +1 -0
  190. package/dist/jsx/fields/Duration/index.mjs +28 -11
  191. package/dist/jsx/fields/Duration/index.mjs.map +1 -1
  192. package/dist/jsx/fields/Duration/index.native.js +32 -12
  193. package/dist/jsx/fields/Duration/index.native.js.map +1 -1
  194. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.mjs +8 -7
  195. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.mjs.map +1 -1
  196. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.native.js +8 -7
  197. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  198. package/dist/jsx/fields/nativeDateTime/osDateTimeIosDisplay.mjs +1 -1
  199. package/dist/jsx/fields/nativeDateTime/osDateTimeIosDisplay.mjs.map +1 -1
  200. package/dist/jsx/fields/nativeDateTime/osDateTimeIosDisplay.native.js +1 -1
  201. package/dist/jsx/fields/nativeDateTime/sheetKeyboard.mjs +1 -1
  202. package/dist/jsx/fields/nativeDateTime/sheetKeyboard.mjs.map +1 -1
  203. package/dist/jsx/fields/nativeDateTime/sheetKeyboard.native.js +1 -1
  204. package/dist/jsx/fields/nativeDateTime/sheetKeyboard.native.js.map +1 -1
  205. package/dist/jsx/index.js +2 -2
  206. package/dist/jsx/index.js.map +1 -1
  207. package/dist/jsx/index.mjs +2 -2
  208. package/dist/jsx/index.mjs.map +1 -1
  209. package/dist/jsx/index.native.js +0 -1
  210. package/dist/jsx/index.native.js.map +1 -1
  211. package/package.json +8 -8
  212. package/src/Button/Button.iconButton.spec.tsx +47 -7
  213. package/src/Button/IconButtonGroup.spec.tsx +103 -19
  214. package/src/Button/IconButtonGroup.tsx +84 -46
  215. package/src/Button/circleGlyph.ts +18 -0
  216. package/src/Button/glass.native.spec.ts +62 -0
  217. package/src/Button/glass.native.ts +58 -37
  218. package/src/Button/glass.spec.ts +64 -16
  219. package/src/Button/glass.ts +16 -12
  220. package/src/Button/iconButtonGroupContext.ts +27 -3
  221. package/src/Button/index.tsx +21 -33
  222. package/src/Button/webGlass.ts +25 -18
  223. package/src/FloatingPanel/FloatingPanel.iconEcho.spec.tsx +61 -1
  224. package/src/FloatingPanel/FloatingPanel.menuFace.native.spec.tsx +104 -0
  225. package/src/FloatingPanel/coverRect.ts +42 -0
  226. package/src/FloatingPanel/iconEcho.tsx +9 -3
  227. package/src/FloatingPanel/index.native.tsx +63 -18
  228. package/src/FloatingPanel/index.tsx +35 -15
  229. package/src/InputParts/index.tsx +6 -0
  230. package/src/InputParts/keyboardAppearance.spec.ts +12 -0
  231. package/src/InputParts/keyboardAppearance.ts +4 -0
  232. package/src/fields/Duration/Duration.spec.tsx +19 -10
  233. package/src/fields/Duration/Duration.wheel.native.spec.tsx +98 -0
  234. package/src/fields/Duration/DurationWheel.native.tsx +101 -0
  235. package/src/fields/Duration/DurationWheel.tsx +11 -0
  236. package/src/fields/Duration/durationMath.spec.ts +43 -0
  237. package/src/fields/Duration/durationMath.ts +31 -0
  238. package/src/fields/Duration/durationWheelTypes.ts +18 -0
  239. package/src/fields/Duration/index.tsx +58 -27
  240. package/src/fields/nativeDateTime/OsDateTimeSheet.countdown.native.spec.tsx +121 -0
  241. package/src/fields/nativeDateTime/OsDateTimeSheet.spec.ts +4 -0
  242. package/src/fields/nativeDateTime/OsDateTimeSheet.tsx +14 -9
  243. package/src/fields/nativeDateTime/osDateTimeIosDisplay.ts +3 -3
  244. package/src/fields/nativeDateTime/sheetKeyboard.spec.ts +6 -0
  245. package/src/fields/nativeDateTime/sheetKeyboard.ts +2 -2
  246. package/src/index.ts +0 -1
  247. package/types/Button/IconButtonGroup.d.ts +7 -12
  248. package/types/Button/IconButtonGroup.d.ts.map +1 -1
  249. package/types/Button/circleGlyph.d.ts +12 -0
  250. package/types/Button/circleGlyph.d.ts.map +1 -1
  251. package/types/Button/glass.d.ts +7 -11
  252. package/types/Button/glass.d.ts.map +1 -1
  253. package/types/Button/glass.native.d.ts +22 -10
  254. package/types/Button/glass.native.d.ts.map +1 -1
  255. package/types/Button/iconButtonGroupContext.d.ts +21 -3
  256. package/types/Button/iconButtonGroupContext.d.ts.map +1 -1
  257. package/types/Button/index.d.ts +3 -10
  258. package/types/Button/index.d.ts.map +1 -1
  259. package/types/Button/webGlass.d.ts +8 -16
  260. package/types/Button/webGlass.d.ts.map +1 -1
  261. package/types/FloatingPanel/coverRect.d.ts +25 -0
  262. package/types/FloatingPanel/coverRect.d.ts.map +1 -0
  263. package/types/FloatingPanel/index.d.ts.map +1 -1
  264. package/types/FloatingPanel/index.native.d.ts.map +1 -1
  265. package/types/InputParts/index.d.ts.map +1 -1
  266. package/types/InputParts/keyboardAppearance.d.ts +3 -0
  267. package/types/InputParts/keyboardAppearance.d.ts.map +1 -0
  268. package/types/fields/Duration/DurationWheel.d.ts +5 -0
  269. package/types/fields/Duration/DurationWheel.d.ts.map +1 -0
  270. package/types/fields/Duration/DurationWheel.native.d.ts +5 -0
  271. package/types/fields/Duration/DurationWheel.native.d.ts.map +1 -0
  272. package/types/fields/Duration/durationMath.d.ts +13 -0
  273. package/types/fields/Duration/durationMath.d.ts.map +1 -1
  274. package/types/fields/Duration/durationWheelTypes.d.ts +18 -0
  275. package/types/fields/Duration/durationWheelTypes.d.ts.map +1 -0
  276. package/types/fields/Duration/index.d.ts +8 -3
  277. package/types/fields/Duration/index.d.ts.map +1 -1
  278. package/types/fields/nativeDateTime/OsDateTimeSheet.d.ts +1 -1
  279. package/types/fields/nativeDateTime/OsDateTimeSheet.d.ts.map +1 -1
  280. package/types/fields/nativeDateTime/osDateTimeIosDisplay.d.ts +2 -2
  281. package/types/fields/nativeDateTime/osDateTimeIosDisplay.d.ts.map +1 -1
  282. package/types/fields/nativeDateTime/sheetKeyboard.d.ts +1 -1
  283. package/types/fields/nativeDateTime/sheetKeyboard.d.ts.map +1 -1
  284. package/types/index.d.ts +1 -1
  285. package/types/index.d.ts.map +1 -1
  286. package/dist/cjs/Button/mediaPlate.native.js.map +0 -1
  287. package/dist/esm/Button/mediaPlate.mjs +0 -6
  288. package/dist/esm/Button/mediaPlate.mjs.map +0 -1
  289. package/dist/esm/Button/mediaPlate.native.js +0 -6
  290. package/dist/esm/Button/mediaPlate.native.js.map +0 -1
  291. package/dist/jsx/Button/mediaPlate.mjs +0 -6
  292. package/dist/jsx/Button/mediaPlate.mjs.map +0 -1
  293. package/dist/jsx/Button/mediaPlate.native.js.map +0 -1
  294. package/src/Button/mediaPlate.ts +0 -9
  295. package/types/Button/mediaPlate.d.ts +0 -10
  296. package/types/Button/mediaPlate.d.ts.map +0 -1
@@ -123,6 +123,12 @@ import { RegularPanel } from "./RegularPanel";
123
123
  import { panelViewportPadding, resolvePanelWidthMode } from "./useFloatingPanel";
124
124
  import { TrailingActionOverlay, TriggerWithTrailingAction } from "./trailingAction";
125
125
  import { iconEchoElement, iconEchoOverlays, type IconEcho } from "./iconEcho";
126
+ import { echoInCover, iconCoverRect } from "./coverRect";
127
+ import {
128
+ IconButtonGroupSurfaceContext,
129
+ useIconButtonGroupSurface,
130
+ type IconButtonGroupPlate,
131
+ } from "../Button/iconButtonGroupContext";
126
132
 
127
133
  // Re-export the hook + constants so Select / Combobox / external consumers
128
134
  // resolve identically on native (their floating cores are unused on device
@@ -189,10 +195,13 @@ export function FloatingPanel({
189
195
  const { knobProps } = useResolvedKnobs({ painted: false });
190
196
  const triggerSurface = SurfaceContext.useStyledContext();
191
197
  const windowZIndex = useOverlayZIndex("sheet");
198
+ // A grouped trigger's capsule is its own: a panel's content never covers it.
192
199
  const children = (
193
- <Surface painted={false} elevated>
194
- {panelChildren}
195
- </Surface>
200
+ <IconButtonGroupSurfaceContext.Provider value={null}>
201
+ <Surface painted={false} elevated>
202
+ {panelChildren}
203
+ </Surface>
204
+ </IconButtonGroupSurfaceContext.Provider>
196
205
  );
197
206
  const slideTransition = knobProps.motion.slide.transition;
198
207
  const enterDuration = motionDurationMs(slideTransition);
@@ -250,6 +259,10 @@ export function FloatingPanel({
250
259
  const anchored = asMenu || asRegular;
251
260
  const triggerRef = useRef<RNView | null>(null);
252
261
  const [anchor, setAnchor] = useState<TriggerRect | null>(null);
262
+ // LC-112 (FZ-8550): a trigger in a bar's glass capsule covers the capsule.
263
+ const groupSurface = useIconButtonGroupSurface();
264
+ // `undefined` while the capsule is still being measured.
265
+ const [group, setGroup] = useState<IconButtonGroupPlate | null | undefined>(null);
253
266
  const [triggerRadius, setTriggerRadius] = useState<number | null>(null);
254
267
  const reportTriggerRadius = useCallback(
255
268
  (radius: number) => setTriggerRadius((prev) => (prev === radius ? prev : radius)),
@@ -260,6 +273,7 @@ export function FloatingPanel({
260
273
  useLayoutEffect(() => {
261
274
  if (!anchored || !open) {
262
275
  setAnchor(null);
276
+ setGroup(null);
263
277
  return;
264
278
  }
265
279
  const node = triggerRef.current as
@@ -275,10 +289,18 @@ export function FloatingPanel({
275
289
  } else {
276
290
  setAnchor({ x: 0, y: 0, width: 0, height: 0 });
277
291
  }
292
+ if (iconEcho && anchorMode !== "attach" && groupSurface) {
293
+ setGroup(undefined);
294
+ groupSurface.measure((plate) => {
295
+ if (live) setGroup(plate);
296
+ });
297
+ } else {
298
+ setGroup(null);
299
+ }
278
300
  return () => {
279
301
  live = false;
280
302
  };
281
- }, [anchored, open]);
303
+ }, [anchored, open, iconEcho, anchorMode, groupSurface]);
282
304
 
283
305
  const handleBackdropPress = useCallback(() => {
284
306
  if (backdropArmed) close();
@@ -636,7 +658,7 @@ export function FloatingPanel({
636
658
  <TriggerMenuPanel
637
659
  // An attached menu (an icon trigger, no echo row) opens under its
638
660
  // trigger instead of over it, the UIMenu pull-down (LC-262).
639
- anchor={anchor}
661
+ anchor={group === undefined ? null : anchor}
640
662
  attach={anchorMode === "attach"}
641
663
  bounds={{
642
664
  top: topInset + panelViewportPadding,
@@ -655,6 +677,7 @@ export function FloatingPanel({
655
677
  : null
656
678
  }
657
679
  iconEchoKind={iconEcho}
680
+ group={iconEcho && anchorMode !== "attach" ? (group ?? null) : null}
658
681
  coverAlign={anchorMode === "attach" ? "start" : coverAlign}
659
682
  panelWidth={panelWidth}
660
683
  triggerRadius={anchorMode === "attach" ? null : triggerRadius}
@@ -824,6 +847,7 @@ function TriggerMenuPanel({
824
847
  echoLabel,
825
848
  iconEcho = null,
826
849
  iconEchoKind = "menu",
850
+ group = null,
827
851
  coverAlign,
828
852
  panelWidth,
829
853
  triggerRadius,
@@ -841,6 +865,8 @@ function TriggerMenuPanel({
841
865
  /** LC-112 (FZ-8480): the icon trigger drawn again in its own spot. */
842
866
  iconEcho?: ReactNode;
843
867
  iconEchoKind?: IconEcho;
868
+ /** LC-112 (FZ-8550): the glass capsule a grouped trigger sits on. */
869
+ group?: IconButtonGroupPlate | null;
844
870
  coverAlign: "start" | "auto";
845
871
  panelWidth: PanelWidth;
846
872
  triggerRadius: number | null;
@@ -852,6 +878,11 @@ function TriggerMenuPanel({
852
878
  }) {
853
879
  const { knobProps } = useResolvedKnobs({ painted: false });
854
880
  const [size, setSize] = useState<{ width: number; height: number } | null>(null);
881
+ // LC-112 (FZ-8480, FZ-8550): an icon trigger's panel covers the surface the
882
+ // trigger is seen on, its capsule or its press target, and the X keeps the
883
+ // trigger's own box inside it.
884
+ const cover = anchor && iconEcho != null ? iconCoverRect(anchor, group) : anchor;
885
+ const echoCover = anchor && cover && iconEcho != null ? echoInCover(anchor, cover) : null;
855
886
  // LC-112 width setting (FZ-8438 r3). `"content"` keeps the iOS pull-down's
856
887
  // 250pt floor (FZ-8220), and no panel is narrower than an echoed trigger.
857
888
  const room =
@@ -871,12 +902,12 @@ function TriggerMenuPanel({
871
902
  trigger: anchor?.width ?? 0,
872
903
  available: room.right - room.left,
873
904
  echo: echo != null,
874
- floor: Math.max(MENU_MIN_WIDTH, anchor?.width ?? 0),
905
+ floor: Math.max(MENU_MIN_WIDTH, cover?.width ?? 0),
875
906
  });
876
907
  const placement =
877
- anchor && size
908
+ anchor && cover && size
878
909
  ? (attach ? placeBesideTrigger : placeOverTrigger)({
879
- trigger: anchor,
910
+ trigger: cover,
880
911
  width: size.width,
881
912
  height: size.height,
882
913
  bounds: room,
@@ -888,13 +919,15 @@ function TriggerMenuPanel({
888
919
  // LC-112 seamless cover: the menu is its trigger grown open, so it draws the
889
920
  // trigger's corner arc at all four corners. A field reports its radius; an
890
921
  // icon button with no echo is a circle or capsule, half its short side.
891
- const half = anchor ? Math.min(anchor.width, anchor.height) / 2 : undefined;
922
+ const half = cover ? Math.min(cover.width, cover.height) / 2 : undefined;
892
923
  const radius =
893
- triggerRadius != null && half != null
894
- ? Math.min(triggerRadius, half)
895
- : coverAlign === "auto" && half
896
- ? half
897
- : undefined;
924
+ group != null && iconEcho != null
925
+ ? Math.min(group.radius, half ?? group.radius)
926
+ : triggerRadius != null && half != null
927
+ ? Math.min(triggerRadius, half)
928
+ : coverAlign === "auto" && half
929
+ ? half
930
+ : undefined;
898
931
  const borderWidth = Number(knobProps.elevatedSurface.borderWidth) || 0;
899
932
  const echoOverlays = iconEchoOverlays(iconEchoKind, placement?.up ?? false);
900
933
  // A panel slid along the screen (a `"screen"` or fixed width, or one pushed
@@ -981,7 +1014,13 @@ function TriggerMenuPanel({
981
1014
  </Pressable>
982
1015
  ) : null}
983
1016
  {iconEcho != null && anchor && !placement?.up && !echoOverlays ? (
984
- <IconEchoBand anchor={anchor} shift={echoShift} borderWidth={borderWidth} edge="top">
1017
+ <IconEchoBand
1018
+ anchor={anchor}
1019
+ cover={echoCover}
1020
+ shift={echoShift}
1021
+ borderWidth={borderWidth}
1022
+ edge="top"
1023
+ >
985
1024
  {iconEcho}
986
1025
  </IconEchoBand>
987
1026
  ) : null}
@@ -996,7 +1035,7 @@ function TriggerMenuPanel({
996
1035
  ? echoHeaderBox({
997
1036
  offset: echoShift,
998
1037
  width: anchor.width,
999
- height: anchor.height,
1038
+ height: echoCover?.height ?? anchor.height,
1000
1039
  panelWidth: placement.width,
1001
1040
  rtl: I18nManager.isRTL,
1002
1041
  })
@@ -1010,6 +1049,7 @@ function TriggerMenuPanel({
1010
1049
  {iconEcho != null && anchor && (placement?.up || echoOverlays) ? (
1011
1050
  <IconEchoBand
1012
1051
  anchor={anchor}
1052
+ cover={echoCover}
1013
1053
  shift={echoShift}
1014
1054
  borderWidth={borderWidth}
1015
1055
  edge={echoOverlays ? "overlay" : "bottom"}
@@ -1032,18 +1072,22 @@ function TriggerMenuPanel({
1032
1072
  */
1033
1073
  function IconEchoBand({
1034
1074
  anchor,
1075
+ cover = null,
1035
1076
  shift,
1036
1077
  borderWidth,
1037
1078
  edge,
1038
1079
  children,
1039
1080
  }: {
1040
1081
  anchor: TriggerRect;
1082
+ /** The trigger's offset in, and the height of, a cover larger than it. */
1083
+ cover?: { top: number; height: number } | null;
1041
1084
  shift: number;
1042
1085
  borderWidth: number;
1043
1086
  /** `"overlay"`: an X laid over the content's top corner, taking no height. */
1044
1087
  edge: "top" | "bottom" | "overlay";
1045
1088
  children: ReactNode;
1046
1089
  }) {
1090
+ const bandHeight = cover?.height ?? anchor.height;
1047
1091
  return (
1048
1092
  <RNView
1049
1093
  testID="floating-panel-icon-echo-band"
@@ -1055,11 +1099,11 @@ function IconEchoBand({
1055
1099
  top: -borderWidth,
1056
1100
  start: -borderWidth,
1057
1101
  end: -borderWidth,
1058
- height: anchor.height,
1102
+ height: bandHeight,
1059
1103
  zIndex: 1,
1060
1104
  }
1061
1105
  : {
1062
- height: anchor.height,
1106
+ height: bandHeight,
1063
1107
  marginHorizontal: -borderWidth,
1064
1108
  ...(edge === "top" ? { marginTop: -borderWidth } : { marginBottom: -borderWidth }),
1065
1109
  }
@@ -1070,6 +1114,7 @@ function IconEchoBand({
1070
1114
  width: anchor.width,
1071
1115
  height: anchor.height,
1072
1116
  marginStart: shift,
1117
+ marginTop: cover?.top ?? 0,
1073
1118
  alignItems: "flex-start",
1074
1119
  justifyContent: "flex-start",
1075
1120
  }}
@@ -48,6 +48,7 @@ import {
48
48
  type PanelIconEchoBox,
49
49
  } from "./echoContext";
50
50
  import { iconEchoElement, iconEchoOverlays, type IconEcho } from "./iconEcho";
51
+ import { echoInCover, iconCoverRect } from "./coverRect";
51
52
  import { groupSurfaceOf, paintedTriggerBox } from "./paintedBox";
52
53
  import {
53
54
  contextMenuPoint,
@@ -566,8 +567,9 @@ function placeIconEcho(
566
567
  up: boolean;
567
568
  overlay: boolean;
568
569
  /**
569
- * The shared plate a grouped trigger sits on: the band covers all of it,
570
- * and only the pressed trigger shows, at its own spot in the plate.
570
+ * The surface the panel covers when it is not the trigger's own box (a
571
+ * grouped capsule, or the 44pt target of a smaller avatar): the band
572
+ * covers all of it, and only the pressed trigger shows, at its own spot.
571
573
  */
572
574
  group?: { top: number; height: number } | null;
573
575
  },
@@ -1065,22 +1067,43 @@ function FloatingPanelWeb({
1065
1067
  measured.height + firstActionHeight + bodyPadding + totalBorderWidth,
1066
1068
  )
1067
1069
  : totalContentHeight;
1070
+ // LC-112 (FZ-8480): an icon trigger's panel covers the surface the
1071
+ // trigger is seen on: its capsule, else its press target.
1072
+ const painted = showIconEcho ? paintedTriggerBox(trig, measured) : null;
1073
+ const coverRect = painted
1074
+ ? iconCoverRect(
1075
+ {
1076
+ x: painted.left,
1077
+ y: measured.top,
1078
+ width: painted.right - painted.left,
1079
+ height: measured.height,
1080
+ },
1081
+ groupBox
1082
+ ? {
1083
+ x: groupBox.left,
1084
+ y: groupBox.top,
1085
+ width: groupBox.width,
1086
+ height: groupBox.height,
1087
+ }
1088
+ : null,
1089
+ )
1090
+ : null;
1091
+ const echoCover = coverRect
1092
+ ? echoInCover(
1093
+ { x: measured.left, y: measured.top, width: measured.width, height: measured.height },
1094
+ coverRect,
1095
+ )
1096
+ : null;
1068
1097
  let placedTop = measured.top;
1069
1098
  let placedLeft = left;
1070
1099
  let placedMaxHeight: number | null = null;
1071
1100
  iconEchoUpRef.current = false;
1072
- if (showIconEcho && measured.width > 0 && measured.height > 0) {
1101
+ if (coverRect && measured.width > 0 && measured.height > 0) {
1073
1102
  // LC-112 (FZ-8480): the button is drawn again in its own spot, so the
1074
1103
  // panel stays on it: down from its top edge, or up from its bottom.
1075
1104
  // It covers the button's plate and press box where they reach past it.
1076
- const painted = groupBox ?? paintedTriggerBox(trig, measured);
1077
1105
  const placed = placeOverTrigger({
1078
- trigger: {
1079
- x: painted.left,
1080
- y: groupBox ? groupBox.top : measured.top,
1081
- width: painted.right - painted.left,
1082
- height: groupBox ? groupBox.height : measured.height,
1083
- },
1106
+ trigger: coverRect,
1084
1107
  width,
1085
1108
  height: totalContentHeight,
1086
1109
  bounds: {
@@ -1152,7 +1175,7 @@ function FloatingPanelWeb({
1152
1175
  ? echoHeaderBox({
1153
1176
  offset: echoStart,
1154
1177
  width: measured.width,
1155
- height: measured.height,
1178
+ height: echoCover?.height ?? measured.height,
1156
1179
  panelWidth: width,
1157
1180
  rtl,
1158
1181
  })
@@ -1164,10 +1187,7 @@ function FloatingPanelWeb({
1164
1187
  height: measured.height,
1165
1188
  up: iconEchoUpRef.current,
1166
1189
  overlay: echoOverlays,
1167
- group:
1168
- groupSurface && groupBox
1169
- ? { top: measured.top - groupBox.top, height: groupBox.height }
1170
- : null,
1190
+ group: echoCover,
1171
1191
  });
1172
1192
  placeEchoOverTrigger(
1173
1193
  dropdownElement,
@@ -34,6 +34,7 @@ import {
34
34
  styled,
35
35
  useGetThemedIcon,
36
36
  useTheme,
37
+ useThemeName,
37
38
  } from "tamagui";
38
39
  import {
39
40
  ensureCompositeFocusRing,
@@ -64,6 +65,7 @@ import { pressSlopProps } from "../shared/pressSlopProps";
64
65
  import { useFieldRow } from "../shared/fieldRowContext";
65
66
  import { useIsInTableCell } from "../shared/tableCellContext";
66
67
  import { useInPanelEcho, usePanelTriggerRadiusReporter } from "../FloatingPanel/echoContext";
68
+ import { keyboardAppearanceFor } from "./keyboardAppearance";
67
69
  import { useFieldA11y, useFieldDescribedBy } from "../fieldLayout";
68
70
 
69
71
  /**
@@ -857,6 +859,7 @@ const InputAreaImpl = InputFrame.styleable<{
857
859
  // field given `size` directly (no Surface) must not type at `$true`.
858
860
  const { knobProps } = useResolvedKnobs({ size: size as SizeTokens });
859
861
  const theme = useTheme();
862
+ const themeName = useThemeName();
860
863
  const fieldDescribedBy = useFieldDescribedBy();
861
864
  const nativeA11yProps = useNativeFieldA11yProps();
862
865
  const inTableCell = useIsInTableCell();
@@ -929,6 +932,9 @@ const InputAreaImpl = InputFrame.styleable<{
929
932
  paddingHorizontal={0}
930
933
  paddingVertical={0}
931
934
  keyboardType={resolvedKeyboardType}
935
+ // The iOS keyboard follows the OS appearance unless told, so a light
936
+ // app on a dark phone typed on a dark keypad (FZ-8111).
937
+ {...(isWeb ? undefined : { keyboardAppearance: keyboardAppearanceFor(themeName) })}
932
938
  textContentType={textContentType}
933
939
  inputMode={resolvedInputMode}
934
940
  autoComplete={
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { keyboardAppearanceFor } from "./keyboardAppearance";
3
+
4
+ describe("keyboardAppearanceFor (FZ-8111)", () => {
5
+ it("follows the app's scheme, not the phone's", () => {
6
+ expect(keyboardAppearanceFor("light")).toBe("light");
7
+ expect(keyboardAppearanceFor("light_accent")).toBe("light");
8
+ expect(keyboardAppearanceFor("dark")).toBe("dark");
9
+ expect(keyboardAppearanceFor("dark_accent")).toBe("dark");
10
+ expect(keyboardAppearanceFor(undefined)).toBe("light");
11
+ });
12
+ });
@@ -0,0 +1,4 @@
1
+ /** The iOS keypad's appearance from the active theme, not the phone's setting. */
2
+ export function keyboardAppearanceFor(themeName: string | undefined): "light" | "dark" {
3
+ return String(themeName ?? "").startsWith("dark") ? "dark" : "light";
4
+ }
@@ -104,14 +104,14 @@ describe("Duration", () => {
104
104
 
105
105
  it("supplies a default clock affordance", () => {
106
106
  const result = renderWithProviders(<Duration label="Clock" name="c" value={3600} />);
107
- expect(result.container.querySelector("svg")).toBeTruthy();
107
+ expect(result.container.querySelector("[data-duration-box] svg")).toBeTruthy();
108
108
  });
109
109
 
110
110
  it("hides the clock when clockIcon is null", () => {
111
111
  const result = renderWithProviders(
112
112
  <Duration label="No clock" name="c" value={3600} clockIcon={null} />,
113
113
  );
114
- expect(result.container.querySelector("svg")).toBeNull();
114
+ expect(result.container.querySelector("[data-duration-box] svg")).toBeNull();
115
115
  });
116
116
 
117
117
  it("normalises a negative value in the hours segment", () => {
@@ -193,7 +193,7 @@ describe("Duration", () => {
193
193
  expect(DURATION_PRESET_SIZE).toBe("$2");
194
194
  expect(sizeRecipeForToken(DURATION_PRESET_SIZE).height).toBe(26);
195
195
  const result = renderWithProviders(<Duration label="P" value={0} />);
196
- const chips = result.container.querySelectorAll("[data-duration-preset]");
196
+ const chips = result.container.querySelectorAll("[data-duration-preset] [data-mp-chip]");
197
197
  expect(chips).toHaveLength(6);
198
198
  for (const chip of chips) {
199
199
  const el = chip as HTMLElement;
@@ -222,8 +222,8 @@ describe("Duration", () => {
222
222
  it("clock glyph follows the icon recipe, not a hardcoded 16 (LC-75)", () => {
223
223
  const small = renderWithProviders(<Duration label="S" size="$2" value={3600} />);
224
224
  const large = renderWithProviders(<Duration label="L" size="$6" value={3600} />);
225
- const sSvg = small.container.querySelector("svg");
226
- const lSvg = large.container.querySelector("svg");
225
+ const sSvg = small.container.querySelector("[data-duration-box] svg");
226
+ const lSvg = large.container.querySelector("[data-duration-box] svg");
227
227
  const sSize = Number(sSvg?.getAttribute("width") ?? sSvg?.getAttribute("height"));
228
228
  const lSize = Number(lSvg?.getAttribute("width") ?? lSvg?.getAttribute("height"));
229
229
  expect(sSize).toBe(sizeRecipeForToken("$2").iconSize);
@@ -244,13 +244,22 @@ describe("Duration", () => {
244
244
  it("fires onChange with seconds (LC-14) from a preset", () => {
245
245
  const onChange = vi.fn();
246
246
  const result = renderWithProviders(<Duration label="P" value={0} onChange={onChange} />);
247
- const chip = result.container.querySelector(
248
- "[data-duration-preset], [data-duration-presets] button, [data-duration-presets] [role='button']",
249
- );
247
+ const chip = result.container.querySelector("[data-duration-preset='1h'] [role='button']");
250
248
  expect(chip).toBeTruthy();
251
249
  fireEvent.click(chip as HTMLElement);
252
- expect(onChange).toHaveBeenCalled();
253
- expect(typeof onChange.mock.calls[0]?.[0]).toBe("number");
250
+ expect(onChange).toHaveBeenCalledWith(3600);
251
+ });
252
+
253
+ it("presets are MPO chips, the current value's chip selected (FZ-8111)", () => {
254
+ const result = renderWithProviders(<Duration label="P" value={3600} />);
255
+ const selected = result.container.querySelectorAll(
256
+ "[data-duration-preset] [data-mp-chip][data-selected='true']",
257
+ );
258
+ expect(selected).toHaveLength(1);
259
+ expect(
260
+ selected[0]!.closest("[data-duration-preset]")?.getAttribute("data-duration-preset"),
261
+ ).toBe("1h");
262
+ expect(result.container.querySelector("[data-duration-presets] button")).toBeNull();
254
263
  });
255
264
 
256
265
  it("arrow-up on hours commits via onChange", () => {
@@ -0,0 +1,98 @@
1
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
2
+ import { act, fireEvent } from "@testing-library/react";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ const picker = vi.hoisted(() => ({ available: true, props: [] as Record<string, unknown>[] }));
6
+
7
+ vi.mock("react-native", async (importOriginal) => {
8
+ const actual = await importOriginal<typeof import("react-native")>();
9
+ return { ...actual, Platform: { ...actual.Platform, OS: "ios" } };
10
+ });
11
+
12
+ vi.mock("@multiplatform.one/platform", async (importOriginal) => {
13
+ const actual = (await importOriginal()) as Record<string, unknown>;
14
+ const real = actual.platform as Record<string, unknown>;
15
+ return {
16
+ ...actual,
17
+ platform: new Proxy(
18
+ {},
19
+ {
20
+ get(_target, prop) {
21
+ if (prop === "isIos") return true;
22
+ if (prop === "isAndroid") return false;
23
+ return real[prop as string];
24
+ },
25
+ },
26
+ ),
27
+ };
28
+ });
29
+
30
+ vi.mock("./DurationWheel", async () => await import("./DurationWheel.native"));
31
+
32
+ vi.mock("../nativeDateTime/dateTimePickerLoader", () => ({
33
+ loadDateTimePicker: () =>
34
+ picker.available
35
+ ? (props: Record<string, unknown>) => {
36
+ picker.props.push(props);
37
+ return <div data-testid="os-picker" />;
38
+ }
39
+ : null,
40
+ }));
41
+
42
+ const { Duration } = await import("./index");
43
+
44
+ const at = (h: number, m: number) => {
45
+ const d = new Date(2026, 9, 29);
46
+ d.setHours(h, m, 0, 0);
47
+ return d;
48
+ };
49
+
50
+ describe("Duration on a phone (FZ-8111, LC-266)", () => {
51
+ beforeEach(() => {
52
+ picker.available = true;
53
+ picker.props = [];
54
+ });
55
+
56
+ it("shows the value on a trigger, not typed segments", () => {
57
+ const result = renderWithProviders(<Duration label="Duration" value={3720} />);
58
+ expect(result.getByTestId("duration-wheel-trigger")).toBeTruthy();
59
+ expect(result.container.querySelector("[data-duration-value]")?.textContent).toBe("1 h 2 m");
60
+ expect(result.container.querySelector('input[aria-label="Hours"]')).toBeNull();
61
+ });
62
+
63
+ it("opens the countdown wheel at the value and writes the pick back as seconds", () => {
64
+ const onChange = vi.fn();
65
+ const result = renderWithProviders(
66
+ <Duration label="Duration" value={3720} hideDays onChange={onChange} />,
67
+ );
68
+ fireEvent.click(result.getByTestId("duration-wheel-trigger"));
69
+ const props = picker.props.at(-1)!;
70
+ expect(props.mode).toBe("countdown");
71
+ expect([(props.value as Date).getHours(), (props.value as Date).getMinutes()]).toEqual([1, 2]);
72
+ act(() => {
73
+ (props.onChange as (e: unknown, d: Date) => void)({ type: "set" }, at(2, 30));
74
+ });
75
+ fireEvent.click(result.getByRole("button", { name: "Done" }));
76
+ expect(onChange).toHaveBeenCalledWith(9000);
77
+ });
78
+
79
+ it("keeps the typed segments when the binary has no picker module", () => {
80
+ picker.available = false;
81
+ const result = renderWithProviders(<Duration label="Duration" value={3720} />);
82
+ expect(result.queryByTestId("duration-wheel-trigger")).toBeNull();
83
+ expect(result.container.querySelector('input[aria-label="Hours"]')).toBeTruthy();
84
+ });
85
+
86
+ it("keeps the typed segments for a field that shows seconds", () => {
87
+ const result = renderWithProviders(<Duration label="Duration" value={3725} showSeconds />);
88
+ expect(result.queryByTestId("duration-wheel-trigger")).toBeNull();
89
+ expect(result.container.querySelector('input[aria-label="Hours"]')).toBeTruthy();
90
+ });
91
+
92
+ it("keeps the MPO chip presets under the wheel trigger", () => {
93
+ const result = renderWithProviders(<Duration label="Duration" value={0} />);
94
+ expect(result.container.querySelectorAll("[data-duration-preset] [data-mp-chip]")).toHaveLength(
95
+ 6,
96
+ );
97
+ });
98
+ });
@@ -0,0 +1,101 @@
1
+ import { sizeRecipeForToken } from "@multiplatform.one/theme";
2
+ import { ClockIcon } from "@phosphor-icons/react";
3
+ import { useState } from "react";
4
+ import { Pressable as RNPressable } from "react-native";
5
+ import type { SizeTokens } from "tamagui";
6
+ import { Text } from "tamagui";
7
+ import { Input as InputParts } from "../../InputParts";
8
+ import { formCommonColors } from "../../shared/colorRamps";
9
+ import { t } from "../../shared/t";
10
+ import { loadDateTimePicker } from "../nativeDateTime/dateTimePickerLoader";
11
+ import { OsDateTimeSheet } from "../nativeDateTime/OsDateTimeSheet";
12
+ import {
13
+ durationToWheelDate,
14
+ formatDurationParts,
15
+ secondsToParts,
16
+ wheelDateToDuration,
17
+ } from "./durationMath";
18
+ import type { DurationWheelFieldProps } from "./durationWheelTypes";
19
+
20
+ export type { DurationWheelFieldProps };
21
+
22
+ export function durationWheelAvailable(): boolean {
23
+ return loadDateTimePicker() != null;
24
+ }
25
+
26
+ export function DurationWheelField({
27
+ seconds,
28
+ onChange,
29
+ disabled,
30
+ readOnly,
31
+ foldDays,
32
+ clockIcon,
33
+ hasError,
34
+ id,
35
+ hasLabel,
36
+ required,
37
+ size,
38
+ knobProps,
39
+ }: DurationWheelFieldProps) {
40
+ const [open, setOpen] = useState(false);
41
+ const sizeToken = String(size || knobProps.sizeToken);
42
+ const recipe = sizeRecipeForToken(sizeToken);
43
+ const resolvedClock = clockIcon === undefined ? <ClockIcon size={recipe.iconSize} /> : clockIcon;
44
+ const shown = formatDurationParts(secondsToParts(seconds, foldDays), foldDays, {
45
+ d: t("d"),
46
+ h: t("h"),
47
+ m: t("m"),
48
+ });
49
+ const interactive = !disabled && !readOnly;
50
+
51
+ return (
52
+ <InputParts size={size || knobProps.sizeToken} {...(hasError ? { theme: "error" } : undefined)}>
53
+ <RNPressable
54
+ nativeID={id}
55
+ onPress={interactive ? () => setOpen(true) : undefined}
56
+ disabled={!interactive}
57
+ accessibilityRole="button"
58
+ accessibilityLabel={hasLabel ? undefined : t("Duration")}
59
+ accessibilityValue={{ text: shown }}
60
+ accessibilityState={{ disabled: !!disabled }}
61
+ aria-required={required || undefined}
62
+ aria-invalid={hasError || undefined}
63
+ style={{ width: "100%" }}
64
+ testID="duration-wheel-trigger"
65
+ >
66
+ <InputParts.Box
67
+ disabled={disabled}
68
+ data-duration-box=""
69
+ data-duration-wheel=""
70
+ justifyContent="flex-start"
71
+ size={sizeToken as SizeTokens}
72
+ pointerEvents="none"
73
+ >
74
+ {resolvedClock != null ? (
75
+ <InputParts.Icon adornment="leading">{resolvedClock}</InputParts.Icon>
76
+ ) : null}
77
+ <Text
78
+ {...knobProps.body}
79
+ {...knobProps.controlType}
80
+ fontSize={recipe.fontSize}
81
+ color={formCommonColors.text}
82
+ numberOfLines={1}
83
+ data-duration-value=""
84
+ >
85
+ {shown}
86
+ </Text>
87
+ </InputParts.Box>
88
+ </RNPressable>
89
+ <OsDateTimeSheet
90
+ open={open}
91
+ value={durationToWheelDate(seconds)}
92
+ mode="countdown"
93
+ onCancel={() => setOpen(false)}
94
+ onConfirm={(picked) => {
95
+ setOpen(false);
96
+ onChange(wheelDateToDuration(picked, seconds));
97
+ }}
98
+ />
99
+ </InputParts>
100
+ );
101
+ }
@@ -0,0 +1,11 @@
1
+ import type { DurationWheelFieldProps } from "./durationWheelTypes";
2
+
3
+ export type { DurationWheelFieldProps };
4
+
5
+ export function durationWheelAvailable(): boolean {
6
+ return false;
7
+ }
8
+
9
+ export function DurationWheelField(_props: DurationWheelFieldProps) {
10
+ return null;
11
+ }