@multiplatform.one/forms 7.77.0 → 7.77.2

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 (429) hide show
  1. package/dist/cjs/Button/IconButtonGroup.cjs +45 -18
  2. package/dist/cjs/Button/IconButtonGroup.native.js +51 -20
  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/iconButtonGroupContext.cjs +7 -1
  8. package/dist/cjs/Button/iconButtonGroupContext.native.js +7 -1
  9. package/dist/cjs/Button/iconButtonGroupContext.native.js.map +1 -1
  10. package/dist/cjs/Button/index.cjs +3 -11
  11. package/dist/cjs/Button/index.native.js +3 -12
  12. package/dist/cjs/Button/index.native.js.map +1 -1
  13. package/dist/cjs/FloatingPanel/coverRect.cjs +50 -0
  14. package/dist/cjs/FloatingPanel/coverRect.native.js +54 -0
  15. package/dist/cjs/FloatingPanel/coverRect.native.js.map +1 -0
  16. package/dist/cjs/FloatingPanel/iconEcho.cjs +3 -1
  17. package/dist/cjs/FloatingPanel/iconEcho.native.js +3 -1
  18. package/dist/cjs/FloatingPanel/iconEcho.native.js.map +1 -1
  19. package/dist/cjs/FloatingPanel/index.cjs +27 -29
  20. package/dist/cjs/FloatingPanel/index.native.js +41 -15
  21. package/dist/cjs/FloatingPanel/index.native.js.map +1 -1
  22. package/dist/cjs/InputParts/index.cjs +5 -0
  23. package/dist/cjs/InputParts/index.native.js +7 -0
  24. package/dist/cjs/InputParts/index.native.js.map +1 -1
  25. package/dist/cjs/InputParts/keyboardAppearance.cjs +30 -0
  26. package/dist/cjs/InputParts/keyboardAppearance.native.js +33 -0
  27. package/dist/cjs/InputParts/keyboardAppearance.native.js.map +1 -0
  28. package/dist/cjs/fields/Duration/DurationWheel.cjs +34 -0
  29. package/dist/cjs/fields/Duration/DurationWheel.native.js +132 -0
  30. package/dist/cjs/fields/Duration/DurationWheel.native.js.map +1 -0
  31. package/dist/cjs/fields/Duration/durationMath.cjs +19 -1
  32. package/dist/cjs/fields/Duration/durationMath.native.js +19 -1
  33. package/dist/cjs/fields/Duration/durationMath.native.js.map +1 -1
  34. package/dist/cjs/fields/Duration/durationWheelTypes.cjs +18 -0
  35. package/dist/cjs/fields/Duration/durationWheelTypes.native.js +21 -0
  36. package/dist/cjs/fields/Duration/durationWheelTypes.native.js.map +1 -0
  37. package/dist/cjs/fields/Duration/index.cjs +30 -10
  38. package/dist/cjs/fields/Duration/index.native.js +32 -12
  39. package/dist/cjs/fields/Duration/index.native.js.map +1 -1
  40. package/dist/cjs/fields/Select/index.cjs +98 -30
  41. package/dist/cjs/fields/Select/index.native.js +122 -54
  42. package/dist/cjs/fields/Select/index.native.js.map +1 -1
  43. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.cjs +8 -7
  44. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.native.js +8 -7
  45. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  46. package/dist/cjs/fields/nativeDateTime/osDateTimeIosDisplay.cjs +1 -1
  47. package/dist/cjs/fields/nativeDateTime/osDateTimeIosDisplay.native.js +1 -1
  48. package/dist/cjs/fields/nativeDateTime/osDateTimeIosDisplay.native.js.map +1 -1
  49. package/dist/cjs/fields/nativeDateTime/sheetKeyboard.cjs +1 -1
  50. package/dist/cjs/fields/nativeDateTime/sheetKeyboard.native.js +1 -1
  51. package/dist/cjs/fields/nativeDateTime/sheetKeyboard.native.js.map +1 -1
  52. package/dist/cjs/shared/floatingList.cjs +2 -3
  53. package/dist/cjs/shared/floatingList.native.js +2 -3
  54. package/dist/cjs/shared/floatingList.native.js.map +1 -1
  55. package/dist/esm/AdaptivePopup/index.native.js.map +1 -1
  56. package/dist/esm/Button/IconButtonGroup.mjs +47 -20
  57. package/dist/esm/Button/IconButtonGroup.mjs.map +1 -1
  58. package/dist/esm/Button/IconButtonGroup.native.js +53 -22
  59. package/dist/esm/Button/IconButtonGroup.native.js.map +1 -1
  60. package/dist/esm/Button/circleGlyph.mjs +9 -1
  61. package/dist/esm/Button/circleGlyph.mjs.map +1 -1
  62. package/dist/esm/Button/circleGlyph.native.js +10 -1
  63. package/dist/esm/Button/circleGlyph.native.js.map +1 -1
  64. package/dist/esm/Button/iconButtonGroupContext.mjs +5 -1
  65. package/dist/esm/Button/iconButtonGroupContext.mjs.map +1 -1
  66. package/dist/esm/Button/iconButtonGroupContext.native.js +5 -1
  67. package/dist/esm/Button/iconButtonGroupContext.native.js.map +1 -1
  68. package/dist/esm/Button/index.mjs +1 -9
  69. package/dist/esm/Button/index.mjs.map +1 -1
  70. package/dist/esm/Button/index.native.js +1 -10
  71. package/dist/esm/Button/index.native.js.map +1 -1
  72. package/dist/esm/Button/mediaPlate.native.js.map +1 -1
  73. package/dist/esm/Button/webGlass.native.js.map +1 -1
  74. package/dist/esm/Chip/index.native.js.map +1 -1
  75. package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
  76. package/dist/esm/ContextualSaveBar/clientKnownIdentity.native.js.map +1 -1
  77. package/dist/esm/ContextualSaveBar/commitImmediateField.native.js.map +1 -1
  78. package/dist/esm/ContextualSaveBar/index.native.js.map +1 -1
  79. package/dist/esm/ContextualSaveBar/useUnsavedChangesGuard.native.js.map +1 -1
  80. package/dist/esm/ControlGroup/index.native.js.map +1 -1
  81. package/dist/esm/DescriptionList/index.native.js.map +1 -1
  82. package/dist/esm/ErrorSummary.native.js.map +1 -1
  83. package/dist/esm/FieldDisplay/editMode.native.js.map +1 -1
  84. package/dist/esm/FieldDisplay/formatters.native.js.map +1 -1
  85. package/dist/esm/FieldDisplay/index.native.js.map +1 -1
  86. package/dist/esm/FieldGroup.native.js.map +1 -1
  87. package/dist/esm/FloatingPanel/PanelHeader.native.js.map +1 -1
  88. package/dist/esm/FloatingPanel/RegularPanel.native.js.map +1 -1
  89. package/dist/esm/FloatingPanel/compactFace.native.js.map +1 -1
  90. package/dist/esm/FloatingPanel/containingBlock.native.js.map +1 -1
  91. package/dist/esm/FloatingPanel/coverRect.mjs +23 -0
  92. package/dist/esm/FloatingPanel/coverRect.mjs.map +1 -0
  93. package/dist/esm/FloatingPanel/coverRect.native.js +24 -0
  94. package/dist/esm/FloatingPanel/coverRect.native.js.map +1 -0
  95. package/dist/esm/FloatingPanel/echoContext.native.js.map +1 -1
  96. package/dist/esm/FloatingPanel/iconEcho.mjs +4 -2
  97. package/dist/esm/FloatingPanel/iconEcho.mjs.map +1 -1
  98. package/dist/esm/FloatingPanel/iconEcho.native.js +4 -2
  99. package/dist/esm/FloatingPanel/iconEcho.native.js.map +1 -1
  100. package/dist/esm/FloatingPanel/index.mjs +27 -29
  101. package/dist/esm/FloatingPanel/index.mjs.map +1 -1
  102. package/dist/esm/FloatingPanel/index.native.js +41 -15
  103. package/dist/esm/FloatingPanel/index.native.js.map +1 -1
  104. package/dist/esm/FloatingPanel/nestedAction.native.js.map +1 -1
  105. package/dist/esm/FloatingPanel/paintedBox.native.js.map +1 -1
  106. package/dist/esm/FloatingPanel/panelWidth.native.js.map +1 -1
  107. package/dist/esm/FloatingPanel/pointAnchor.native.js.map +1 -1
  108. package/dist/esm/FloatingPanel/regularPlacement.native.js.map +1 -1
  109. package/dist/esm/FloatingPanel/trailingAction.native.js.map +1 -1
  110. package/dist/esm/FloatingPanel/useFloatingPanel.native.js.map +1 -1
  111. package/dist/esm/Form/index.native.js.map +1 -1
  112. package/dist/esm/Form/submissionController.native.js.map +1 -1
  113. package/dist/esm/Form/validateOn.native.js.map +1 -1
  114. package/dist/esm/FormGrid.native.js.map +1 -1
  115. package/dist/esm/FormSection.native.js.map +1 -1
  116. package/dist/esm/InputParts/PasswordReveal.native.js.map +1 -1
  117. package/dist/esm/InputParts/index.mjs +6 -1
  118. package/dist/esm/InputParts/index.mjs.map +1 -1
  119. package/dist/esm/InputParts/index.native.js +8 -1
  120. package/dist/esm/InputParts/index.native.js.map +1 -1
  121. package/dist/esm/InputParts/keyboardAppearance.mjs +5 -0
  122. package/dist/esm/InputParts/keyboardAppearance.mjs.map +1 -0
  123. package/dist/esm/InputParts/keyboardAppearance.native.js +5 -0
  124. package/dist/esm/InputParts/keyboardAppearance.native.js.map +1 -0
  125. package/dist/esm/Label/index.native.js.map +1 -1
  126. package/dist/esm/MediaPicker/base64.native.js.map +1 -1
  127. package/dist/esm/MediaPicker/payload.native.js.map +1 -1
  128. package/dist/esm/MediaPicker/types.native.js.map +1 -1
  129. package/dist/esm/MediaPicker/useMediaPicker.native.js.map +1 -1
  130. package/dist/esm/Skeleton/index.native.js.map +1 -1
  131. package/dist/esm/Spinner/SpinnerGlyph.native.js.map +1 -1
  132. package/dist/esm/Spinner/spinnerSize.native.js.map +1 -1
  133. package/dist/esm/Table/index.native.js.map +1 -1
  134. package/dist/esm/Wheel/haptic.native.js.map +1 -1
  135. package/dist/esm/Wheel/index.native.js.map +1 -1
  136. package/dist/esm/Wheel/wheelShared.native.js.map +1 -1
  137. package/dist/esm/actionLabel.native.js.map +1 -1
  138. package/dist/esm/fieldDefaults.native.js.map +1 -1
  139. package/dist/esm/fieldLayout.native.js.map +1 -1
  140. package/dist/esm/fields/Barcode/BarcodePreview.native.js.map +1 -1
  141. package/dist/esm/fields/Barcode/code128.native.js.map +1 -1
  142. package/dist/esm/fields/Barcode/index.native.js.map +1 -1
  143. package/dist/esm/fields/Barcode/qr.native.js.map +1 -1
  144. package/dist/esm/fields/Barcode/scannerTypes.native.js.map +1 -1
  145. package/dist/esm/fields/Checkbox/Checkbox.native.js.map +1 -1
  146. package/dist/esm/fields/Checkbox/CheckboxBox.native.js.map +1 -1
  147. package/dist/esm/fields/Checkbox/CheckboxCard.native.js.map +1 -1
  148. package/dist/esm/fields/Checkbox/CheckboxFieldFrame.native.js.map +1 -1
  149. package/dist/esm/fields/Checkbox/CheckboxGroup.native.js.map +1 -1
  150. package/dist/esm/fields/Checkbox/checkboxGroupContext.native.js.map +1 -1
  151. package/dist/esm/fields/Checkbox/index.native.js.map +1 -1
  152. package/dist/esm/fields/ColorPicker/SystemColorField.native.js.map +1 -1
  153. package/dist/esm/fields/ColorPicker/index.native.js.map +1 -1
  154. package/dist/esm/fields/ColorPicker/parts.native.js.map +1 -1
  155. package/dist/esm/fields/ColorPicker/systemColorWell.gnome.native.js.map +1 -1
  156. package/dist/esm/fields/Combobox/authorOptions.native.js.map +1 -1
  157. package/dist/esm/fields/Combobox/index.native.js.map +1 -1
  158. package/dist/esm/fields/DatePicker/Calendar.native.js.map +1 -1
  159. package/dist/esm/fields/DatePicker/DateRangePicker.native.js.map +1 -1
  160. package/dist/esm/fields/DatePicker/DatetimePicker.catalog.native.js.map +1 -1
  161. package/dist/esm/fields/DatePicker/MonthPicker.native.js.map +1 -1
  162. package/dist/esm/fields/DatePicker/MultiDatePicker.native.js.map +1 -1
  163. package/dist/esm/fields/DatePicker/catalog.native.js.map +1 -1
  164. package/dist/esm/fields/DatePicker/fieldValueSync.native.js.map +1 -1
  165. package/dist/esm/fields/DatePicker/parts.native.js.map +1 -1
  166. package/dist/esm/fields/DatePicker/utils.native.js.map +1 -1
  167. package/dist/esm/fields/Duration/DurationWheel.mjs +8 -0
  168. package/dist/esm/fields/Duration/DurationWheel.mjs.map +1 -0
  169. package/dist/esm/fields/Duration/DurationWheel.native.js +103 -0
  170. package/dist/esm/fields/Duration/DurationWheel.native.js.map +1 -0
  171. package/dist/esm/fields/Duration/durationMath.mjs +16 -1
  172. package/dist/esm/fields/Duration/durationMath.mjs.map +1 -1
  173. package/dist/esm/fields/Duration/durationMath.native.js +16 -1
  174. package/dist/esm/fields/Duration/durationMath.native.js.map +1 -1
  175. package/dist/esm/fields/Duration/durationWheelTypes.mjs +2 -0
  176. package/dist/esm/fields/Duration/durationWheelTypes.mjs.map +1 -0
  177. package/dist/esm/fields/Duration/durationWheelTypes.native.js +2 -0
  178. package/dist/esm/fields/Duration/durationWheelTypes.native.js.map +1 -0
  179. package/dist/esm/fields/Duration/index.mjs +28 -11
  180. package/dist/esm/fields/Duration/index.mjs.map +1 -1
  181. package/dist/esm/fields/Duration/index.native.js +30 -13
  182. package/dist/esm/fields/Duration/index.native.js.map +1 -1
  183. package/dist/esm/fields/FileUpload/filePickerTypes.native.js.map +1 -1
  184. package/dist/esm/fields/FileUpload/index.native.js.map +1 -1
  185. package/dist/esm/fields/FileUpload/nativePickerLoader.native.js.map +1 -1
  186. package/dist/esm/fields/Geolocation/expoMapsLoader.native.js.map +1 -1
  187. package/dist/esm/fields/Geolocation/geoValue.native.js.map +1 -1
  188. package/dist/esm/fields/Geolocation/geocoder.native.js.map +1 -1
  189. package/dist/esm/fields/Geolocation/geocoderContext.native.js.map +1 -1
  190. package/dist/esm/fields/Geolocation/index.native.js.map +1 -1
  191. package/dist/esm/fields/Geolocation/mapMath.native.js.map +1 -1
  192. package/dist/esm/fields/Geolocation/mapTypes.native.js.map +1 -1
  193. package/dist/esm/fields/Geolocation/nativeSearchFocus.native.js.map +1 -1
  194. package/dist/esm/fields/Geolocation/photonGeocoder.native.js.map +1 -1
  195. package/dist/esm/fields/Icon/drawnSet.native.js.map +1 -1
  196. package/dist/esm/fields/Icon/index.native.js.map +1 -1
  197. package/dist/esm/fields/ImageField/index.native.js.map +1 -1
  198. package/dist/esm/fields/Input/index.native.js.map +1 -1
  199. package/dist/esm/fields/MentionInput/index.native.js.map +1 -1
  200. package/dist/esm/fields/OTPInput/index.native.js.map +1 -1
  201. package/dist/esm/fields/OTPInput/otpCellCss.native.js.map +1 -1
  202. package/dist/esm/fields/PhoneInput/dialCodes.native.js.map +1 -1
  203. package/dist/esm/fields/PhoneInput/index.native.js.map +1 -1
  204. package/dist/esm/fields/PhoneInput/phone.native.js.map +1 -1
  205. package/dist/esm/fields/PhoneInput/regionNames.native.js.map +1 -1
  206. package/dist/esm/fields/Progress/index.native.js.map +1 -1
  207. package/dist/esm/fields/RadioGroup/index.native.js.map +1 -1
  208. package/dist/esm/fields/Rating/index.native.js.map +1 -1
  209. package/dist/esm/fields/Rating/ratingMath.native.js.map +1 -1
  210. package/dist/esm/fields/Rating/types.native.js.map +1 -1
  211. package/dist/esm/fields/SearchInput/index.native.js.map +1 -1
  212. package/dist/esm/fields/Select/InlineChoices.native.js.map +1 -1
  213. package/dist/esm/fields/Select/SheetPickerRow.native.js.map +1 -1
  214. package/dist/esm/fields/Select/index.mjs +98 -30
  215. package/dist/esm/fields/Select/index.mjs.map +1 -1
  216. package/dist/esm/fields/Select/index.native.js +122 -54
  217. package/dist/esm/fields/Select/index.native.js.map +1 -1
  218. package/dist/esm/fields/Select/shouldInlineChoices.native.js.map +1 -1
  219. package/dist/esm/fields/Signature/index.native.js.map +1 -1
  220. package/dist/esm/fields/Slider/index.native.js.map +1 -1
  221. package/dist/esm/fields/Slider/jumpMotion.native.js.map +1 -1
  222. package/dist/esm/fields/Stepper/index.native.js.map +1 -1
  223. package/dist/esm/fields/Stepper/resolveStepperPlacement.native.js.map +1 -1
  224. package/dist/esm/fields/Switch/footprint.native.js.map +1 -1
  225. package/dist/esm/fields/Switch/index.native.js.map +1 -1
  226. package/dist/esm/fields/TextArea/index.native.js.map +1 -1
  227. package/dist/esm/fields/TimePicker/catalog.native.js.map +1 -1
  228. package/dist/esm/fields/TimePicker/parts.native.js.map +1 -1
  229. package/dist/esm/fields/TimeRangeScrubber/geometry.native.js.map +1 -1
  230. package/dist/esm/fields/TimeRangeScrubber/index.native.js.map +1 -1
  231. package/dist/esm/fields/ToggleGroup/index.native.js.map +1 -1
  232. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.mjs +8 -7
  233. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.mjs.map +1 -1
  234. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.native.js +8 -7
  235. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  236. package/dist/esm/fields/nativeDateTime/dateTimePickerLoader.native.js.map +1 -1
  237. package/dist/esm/fields/nativeDateTime/osDateTimeIosDisplay.mjs +1 -1
  238. package/dist/esm/fields/nativeDateTime/osDateTimeIosDisplay.mjs.map +1 -1
  239. package/dist/esm/fields/nativeDateTime/osDateTimeIosDisplay.native.js +1 -1
  240. package/dist/esm/fields/nativeDateTime/osDateTimeIosDisplay.native.js.map +1 -1
  241. package/dist/esm/fields/nativeDateTime/sheetKeyboard.mjs +1 -1
  242. package/dist/esm/fields/nativeDateTime/sheetKeyboard.mjs.map +1 -1
  243. package/dist/esm/fields/nativeDateTime/sheetKeyboard.native.js +1 -1
  244. package/dist/esm/fields/nativeDateTime/sheetKeyboard.native.js.map +1 -1
  245. package/dist/esm/fields/nativeSelect.native.js.map +1 -1
  246. package/dist/esm/fields/selectedOrder.native.js.map +1 -1
  247. package/dist/esm/formSpacing.native.js.map +1 -1
  248. package/dist/esm/hooks/useFocusManagement.native.js.map +1 -1
  249. package/dist/esm/index.native.js.map +1 -1
  250. package/dist/esm/keyboard/chain.native.js.map +1 -1
  251. package/dist/esm/keyboard/index.native.js.map +1 -1
  252. package/dist/esm/keyboard/types.native.js.map +1 -1
  253. package/dist/esm/requiredMarking.native.js.map +1 -1
  254. package/dist/esm/shared/TimeColumn.native.js.map +1 -1
  255. package/dist/esm/shared/capabilityPreflight.native.js.map +1 -1
  256. package/dist/esm/shared/choiceRowPress.native.js.map +1 -1
  257. package/dist/esm/shared/colorRamps.native.js.map +1 -1
  258. package/dist/esm/shared/devWarn.native.js.map +1 -1
  259. package/dist/esm/shared/fieldRowContext.native.js.map +1 -1
  260. package/dist/esm/shared/floatingList.mjs +2 -3
  261. package/dist/esm/shared/floatingList.mjs.map +1 -1
  262. package/dist/esm/shared/floatingList.native.js +2 -3
  263. package/dist/esm/shared/floatingList.native.js.map +1 -1
  264. package/dist/esm/shared/focusManagement.native.js.map +1 -1
  265. package/dist/esm/shared/groupContext.native.js.map +1 -1
  266. package/dist/esm/shared/i18n.native.js.map +1 -1
  267. package/dist/esm/shared/instantMotion.native.js.map +1 -1
  268. package/dist/esm/shared/nativeA11y.native.js.map +1 -1
  269. package/dist/esm/shared/nativeAutoFocus.native.js.map +1 -1
  270. package/dist/esm/shared/overlaySizeClass.native.js.map +1 -1
  271. package/dist/esm/shared/overlayStack.native.js.map +1 -1
  272. package/dist/esm/shared/panelPortal.native.js.map +1 -1
  273. package/dist/esm/shared/pickerTriggerContract.native.js.map +1 -1
  274. package/dist/esm/shared/pressSlopProps.native.js.map +1 -1
  275. package/dist/esm/shared/restoreRetries.native.js.map +1 -1
  276. package/dist/esm/shared/sheetNavigation.native.js.map +1 -1
  277. package/dist/esm/shared/systemControlModules.gnome.native.js.map +1 -1
  278. package/dist/esm/shared/systemControls.native.js.map +1 -1
  279. package/dist/esm/shared/t.native.js.map +1 -1
  280. package/dist/esm/shared/tableCellContext.native.js.map +1 -1
  281. package/dist/esm/shared/useDragScroll.native.js.map +1 -1
  282. package/dist/esm/shared/useKeyboardInset.native.js.map +1 -1
  283. package/dist/esm/shared/useSheetEscapeDismiss.native.js.map +1 -1
  284. package/dist/esm/shared/useWheelIncrement.native.js.map +1 -1
  285. package/dist/esm/shared/utils.native.js.map +1 -1
  286. package/dist/esm/shared/zIndex.native.js.map +1 -1
  287. package/dist/esm/standardActions.native.js.map +1 -1
  288. package/dist/esm/themedPrimitives.native.js.map +1 -1
  289. package/dist/esm/types.native.js.map +1 -1
  290. package/dist/esm/utils/LinkCell.native.js.map +1 -1
  291. package/dist/jsx/Button/IconButtonGroup.mjs +47 -20
  292. package/dist/jsx/Button/IconButtonGroup.mjs.map +1 -1
  293. package/dist/jsx/Button/IconButtonGroup.native.js +51 -20
  294. package/dist/jsx/Button/IconButtonGroup.native.js.map +1 -1
  295. package/dist/jsx/Button/circleGlyph.mjs +9 -1
  296. package/dist/jsx/Button/circleGlyph.mjs.map +1 -1
  297. package/dist/jsx/Button/circleGlyph.native.js +12 -1
  298. package/dist/jsx/Button/circleGlyph.native.js.map +1 -1
  299. package/dist/jsx/Button/iconButtonGroupContext.mjs +5 -1
  300. package/dist/jsx/Button/iconButtonGroupContext.mjs.map +1 -1
  301. package/dist/jsx/Button/iconButtonGroupContext.native.js +7 -1
  302. package/dist/jsx/Button/iconButtonGroupContext.native.js.map +1 -1
  303. package/dist/jsx/Button/index.mjs +1 -9
  304. package/dist/jsx/Button/index.mjs.map +1 -1
  305. package/dist/jsx/Button/index.native.js +3 -12
  306. package/dist/jsx/Button/index.native.js.map +1 -1
  307. package/dist/jsx/FloatingPanel/coverRect.mjs +23 -0
  308. package/dist/jsx/FloatingPanel/coverRect.mjs.map +1 -0
  309. package/dist/jsx/FloatingPanel/coverRect.native.js +54 -0
  310. package/dist/jsx/FloatingPanel/coverRect.native.js.map +1 -0
  311. package/dist/jsx/FloatingPanel/iconEcho.mjs +4 -2
  312. package/dist/jsx/FloatingPanel/iconEcho.mjs.map +1 -1
  313. package/dist/jsx/FloatingPanel/iconEcho.native.js +3 -1
  314. package/dist/jsx/FloatingPanel/iconEcho.native.js.map +1 -1
  315. package/dist/jsx/FloatingPanel/index.mjs +27 -29
  316. package/dist/jsx/FloatingPanel/index.mjs.map +1 -1
  317. package/dist/jsx/FloatingPanel/index.native.js +41 -15
  318. package/dist/jsx/FloatingPanel/index.native.js.map +1 -1
  319. package/dist/jsx/InputParts/index.mjs +6 -1
  320. package/dist/jsx/InputParts/index.mjs.map +1 -1
  321. package/dist/jsx/InputParts/index.native.js +7 -0
  322. package/dist/jsx/InputParts/index.native.js.map +1 -1
  323. package/dist/jsx/InputParts/keyboardAppearance.mjs +5 -0
  324. package/dist/jsx/InputParts/keyboardAppearance.mjs.map +1 -0
  325. package/dist/jsx/InputParts/keyboardAppearance.native.js +33 -0
  326. package/dist/jsx/InputParts/keyboardAppearance.native.js.map +1 -0
  327. package/dist/jsx/fields/Duration/DurationWheel.mjs +8 -0
  328. package/dist/jsx/fields/Duration/DurationWheel.mjs.map +1 -0
  329. package/dist/jsx/fields/Duration/DurationWheel.native.js +132 -0
  330. package/dist/jsx/fields/Duration/DurationWheel.native.js.map +1 -0
  331. package/dist/jsx/fields/Duration/durationMath.mjs +16 -1
  332. package/dist/jsx/fields/Duration/durationMath.mjs.map +1 -1
  333. package/dist/jsx/fields/Duration/durationMath.native.js +19 -1
  334. package/dist/jsx/fields/Duration/durationMath.native.js.map +1 -1
  335. package/dist/jsx/fields/Duration/durationWheelTypes.mjs +2 -0
  336. package/dist/jsx/fields/Duration/durationWheelTypes.mjs.map +1 -0
  337. package/dist/jsx/fields/Duration/durationWheelTypes.native.js +21 -0
  338. package/dist/jsx/fields/Duration/durationWheelTypes.native.js.map +1 -0
  339. package/dist/jsx/fields/Duration/index.mjs +28 -11
  340. package/dist/jsx/fields/Duration/index.mjs.map +1 -1
  341. package/dist/jsx/fields/Duration/index.native.js +32 -12
  342. package/dist/jsx/fields/Duration/index.native.js.map +1 -1
  343. package/dist/jsx/fields/Select/index.mjs +98 -30
  344. package/dist/jsx/fields/Select/index.mjs.map +1 -1
  345. package/dist/jsx/fields/Select/index.native.js +122 -54
  346. package/dist/jsx/fields/Select/index.native.js.map +1 -1
  347. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.mjs +8 -7
  348. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.mjs.map +1 -1
  349. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.native.js +8 -7
  350. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  351. package/dist/jsx/fields/nativeDateTime/osDateTimeIosDisplay.mjs +1 -1
  352. package/dist/jsx/fields/nativeDateTime/osDateTimeIosDisplay.mjs.map +1 -1
  353. package/dist/jsx/fields/nativeDateTime/osDateTimeIosDisplay.native.js +1 -1
  354. package/dist/jsx/fields/nativeDateTime/sheetKeyboard.mjs +1 -1
  355. package/dist/jsx/fields/nativeDateTime/sheetKeyboard.mjs.map +1 -1
  356. package/dist/jsx/fields/nativeDateTime/sheetKeyboard.native.js +1 -1
  357. package/dist/jsx/fields/nativeDateTime/sheetKeyboard.native.js.map +1 -1
  358. package/dist/jsx/index.native.js.map +1 -1
  359. package/dist/jsx/shared/floatingList.mjs +2 -3
  360. package/dist/jsx/shared/floatingList.mjs.map +1 -1
  361. package/dist/jsx/shared/floatingList.native.js +2 -3
  362. package/dist/jsx/shared/floatingList.native.js.map +1 -1
  363. package/package.json +8 -8
  364. package/src/Button/IconButtonGroup.spec.tsx +20 -0
  365. package/src/Button/IconButtonGroup.tsx +60 -25
  366. package/src/Button/circleGlyph.ts +18 -0
  367. package/src/Button/iconButtonGroupContext.ts +25 -0
  368. package/src/Button/index.tsx +2 -20
  369. package/src/FloatingPanel/FloatingPanel.iconEcho.spec.tsx +61 -1
  370. package/src/FloatingPanel/FloatingPanel.menuFace.native.spec.tsx +104 -0
  371. package/src/FloatingPanel/coverRect.ts +42 -0
  372. package/src/FloatingPanel/iconEcho.tsx +9 -3
  373. package/src/FloatingPanel/index.native.tsx +63 -18
  374. package/src/FloatingPanel/index.tsx +38 -35
  375. package/src/InputParts/index.tsx +6 -0
  376. package/src/InputParts/keyboardAppearance.spec.ts +12 -0
  377. package/src/InputParts/keyboardAppearance.ts +4 -0
  378. package/src/fields/Duration/Duration.spec.tsx +19 -10
  379. package/src/fields/Duration/Duration.wheel.native.spec.tsx +98 -0
  380. package/src/fields/Duration/DurationWheel.native.tsx +101 -0
  381. package/src/fields/Duration/DurationWheel.tsx +11 -0
  382. package/src/fields/Duration/durationMath.spec.ts +43 -0
  383. package/src/fields/Duration/durationMath.ts +31 -0
  384. package/src/fields/Duration/durationWheelTypes.ts +18 -0
  385. package/src/fields/Duration/index.tsx +58 -27
  386. package/src/fields/Select/Select.containingBlock.spec.tsx +88 -12
  387. package/src/fields/Select/Select.stories.tsx +74 -0
  388. package/src/fields/Select/index.tsx +113 -44
  389. package/src/fields/nativeDateTime/OsDateTimeSheet.countdown.native.spec.tsx +121 -0
  390. package/src/fields/nativeDateTime/OsDateTimeSheet.spec.ts +4 -0
  391. package/src/fields/nativeDateTime/OsDateTimeSheet.tsx +14 -9
  392. package/src/fields/nativeDateTime/osDateTimeIosDisplay.ts +3 -3
  393. package/src/fields/nativeDateTime/sheetKeyboard.spec.ts +6 -0
  394. package/src/fields/nativeDateTime/sheetKeyboard.ts +2 -2
  395. package/src/shared/floatingList.tsx +1 -4
  396. package/types/Button/IconButtonGroup.d.ts.map +1 -1
  397. package/types/Button/circleGlyph.d.ts +12 -0
  398. package/types/Button/circleGlyph.d.ts.map +1 -1
  399. package/types/Button/iconButtonGroupContext.d.ts +19 -0
  400. package/types/Button/iconButtonGroupContext.d.ts.map +1 -1
  401. package/types/Button/index.d.ts +2 -9
  402. package/types/Button/index.d.ts.map +1 -1
  403. package/types/FloatingPanel/coverRect.d.ts +25 -0
  404. package/types/FloatingPanel/coverRect.d.ts.map +1 -0
  405. package/types/FloatingPanel/index.d.ts +1 -6
  406. package/types/FloatingPanel/index.d.ts.map +1 -1
  407. package/types/FloatingPanel/index.native.d.ts.map +1 -1
  408. package/types/InputParts/index.d.ts.map +1 -1
  409. package/types/InputParts/keyboardAppearance.d.ts +3 -0
  410. package/types/InputParts/keyboardAppearance.d.ts.map +1 -0
  411. package/types/fields/Duration/DurationWheel.d.ts +5 -0
  412. package/types/fields/Duration/DurationWheel.d.ts.map +1 -0
  413. package/types/fields/Duration/DurationWheel.native.d.ts +5 -0
  414. package/types/fields/Duration/DurationWheel.native.d.ts.map +1 -0
  415. package/types/fields/Duration/durationMath.d.ts +13 -0
  416. package/types/fields/Duration/durationMath.d.ts.map +1 -1
  417. package/types/fields/Duration/durationWheelTypes.d.ts +18 -0
  418. package/types/fields/Duration/durationWheelTypes.d.ts.map +1 -0
  419. package/types/fields/Duration/index.d.ts +8 -3
  420. package/types/fields/Duration/index.d.ts.map +1 -1
  421. package/types/fields/Select/index.d.ts.map +1 -1
  422. package/types/fields/nativeDateTime/OsDateTimeSheet.d.ts +1 -1
  423. package/types/fields/nativeDateTime/OsDateTimeSheet.d.ts.map +1 -1
  424. package/types/fields/nativeDateTime/osDateTimeIosDisplay.d.ts +2 -2
  425. package/types/fields/nativeDateTime/osDateTimeIosDisplay.d.ts.map +1 -1
  426. package/types/fields/nativeDateTime/sheetKeyboard.d.ts +1 -1
  427. package/types/fields/nativeDateTime/sheetKeyboard.d.ts.map +1 -1
  428. package/types/shared/floatingList.d.ts +1 -3
  429. package/types/shared/floatingList.d.ts.map +1 -1
@@ -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
+ }
@@ -2,10 +2,13 @@ import { describe, expect, it } from "vitest";
2
2
  import { sizeRecipeForToken } from "@multiplatform.one/theme";
3
3
  import {
4
4
  durationSegmentWidth,
5
+ durationToWheelDate,
6
+ formatDurationParts,
5
7
  normalizeSeconds,
6
8
  parseDurationText,
7
9
  partsToSeconds,
8
10
  secondsToParts,
11
+ wheelDateToDuration,
9
12
  } from "./durationMath";
10
13
 
11
14
  describe("durationMath", () => {
@@ -64,4 +67,44 @@ describe("durationMath", () => {
64
67
  expect(durationSegmentWidth("$2", 2)).not.toBe(durationSegmentWidth("$6", 2));
65
68
  expect(durationSegmentWidth("$2", 2)).toBeLessThan(durationSegmentWidth("$4", 2));
66
69
  });
70
+
71
+ describe("countdown wheel (FZ-8111)", () => {
72
+ const at = (h: number, m: number) => {
73
+ const d = new Date(2026, 9, 29);
74
+ d.setHours(h, m, 0, 0);
75
+ return d;
76
+ };
77
+
78
+ it("reads hours and minutes within the day onto the wheel", () => {
79
+ const date = durationToWheelDate(3720);
80
+ expect([date.getHours(), date.getMinutes(), date.getSeconds()]).toEqual([1, 2, 0]);
81
+ const late = durationToWheelDate(23 * 3600 + 59 * 60);
82
+ expect([late.getHours(), late.getMinutes()]).toEqual([23, 59]);
83
+ });
84
+
85
+ it("round-trips every wheel position back to the same seconds", () => {
86
+ for (const seconds of [0, 60, 900, 3720, 9000, 86340]) {
87
+ expect(wheelDateToDuration(durationToWheelDate(seconds), 0)).toBe(seconds);
88
+ }
89
+ });
90
+
91
+ it("sets seconds from a wheel pick", () => {
92
+ expect(wheelDateToDuration(at(2, 30), 0)).toBe(9000);
93
+ expect(wheelDateToDuration(at(0, 15), 3720)).toBe(900);
94
+ });
95
+
96
+ it("carries whole days and stray seconds the wheel cannot show", () => {
97
+ expect(wheelDateToDuration(at(2, 30), 86400 + 3600 + 7)).toBe(86400 + 9000 + 7);
98
+ const wheel = durationToWheelDate(90000);
99
+ expect([wheel.getHours(), wheel.getMinutes()]).toEqual([1, 0]);
100
+ });
101
+
102
+ it("formats the trigger text with or without days", () => {
103
+ const units = { d: "d", h: "h", m: "m" };
104
+ expect(formatDurationParts(secondsToParts(3720, true), true, units)).toBe("1 h 2 m");
105
+ expect(formatDurationParts(secondsToParts(90000, true), true, units)).toBe("25 h 0 m");
106
+ expect(formatDurationParts(secondsToParts(93784, false), false, units)).toBe("1 d 2 h 3 m");
107
+ expect(formatDurationParts(secondsToParts(3720, false), false, units)).toBe("1 h 2 m");
108
+ });
109
+ });
67
110
  });
@@ -76,3 +76,34 @@ export function durationSegmentWidth(sizeToken: string, digits: number): number
76
76
  const recipe = sizeRecipeForToken(sizeToken);
77
77
  return recipe.fontSize * digits;
78
78
  }
79
+
80
+ const DAY = 86400;
81
+
82
+ /** The countdown wheel's reading of a duration: hours and minutes within one day, on today's date. */
83
+ export function durationToWheelDate(total: number): Date {
84
+ const s = normalizeSeconds(total) % DAY;
85
+ const date = new Date();
86
+ date.setHours(Math.floor(s / 3600), Math.floor((s % 3600) / 60), 0, 0);
87
+ return date;
88
+ }
89
+
90
+ /**
91
+ * A wheel pick back to seconds. The wheel spans 0 h 0 m to 23 h 59 m, so the
92
+ * whole days and the stray seconds of the previous value carry over untouched.
93
+ */
94
+ export function wheelDateToDuration(picked: Date, previous: number): number {
95
+ const prev = normalizeSeconds(previous);
96
+ return (
97
+ Math.floor(prev / DAY) * DAY + picked.getHours() * 3600 + picked.getMinutes() * 60 + (prev % 60)
98
+ );
99
+ }
100
+
101
+ /** "1 h 2 m", "1 d 2 h 3 m": the wheel trigger's text, units from the caller. */
102
+ export function formatDurationParts(
103
+ parts: DurationParts,
104
+ hideDays: boolean,
105
+ units: { d: string; h: string; m: string },
106
+ ): string {
107
+ const head = hideDays || parts.days === 0 ? [] : [`${parts.days} ${units.d}`];
108
+ return [...head, `${parts.hours} ${units.h}`, `${parts.minutes} ${units.m}`].join(" ");
109
+ }
@@ -0,0 +1,18 @@
1
+ import type { KnobProps } from "@multiplatform.one/theme";
2
+ import type { ReactNode } from "react";
3
+ import type { SizeTokens } from "tamagui";
4
+
5
+ export interface DurationWheelFieldProps {
6
+ seconds: number;
7
+ onChange: (seconds: number) => void;
8
+ disabled?: boolean;
9
+ readOnly?: boolean;
10
+ foldDays: boolean;
11
+ clockIcon?: ReactNode | null;
12
+ hasError?: boolean;
13
+ id?: string;
14
+ hasLabel?: boolean;
15
+ required?: boolean;
16
+ size?: SizeTokens;
17
+ knobProps: KnobProps;
18
+ }
@@ -5,7 +5,7 @@ import type { ReactNode } from "react";
5
5
  import { useCallback, useState } from "react";
6
6
  import type { LabelProps, SizeTokens } from "tamagui";
7
7
  import { Text, XStack, YStack, isWeb } from "tamagui";
8
- import { Button } from "../../Button";
8
+ import { Chip } from "../../Chip";
9
9
  import { Skeleton } from "../../Skeleton";
10
10
  import { Field, FieldLayout } from "../../fieldLayout";
11
11
  import { Input as InputParts } from "../../InputParts";
@@ -20,22 +20,29 @@ import {
20
20
  useResolvedValidators,
21
21
  } from "../../shared/utils";
22
22
  import type { AnyFormApi } from "../../types";
23
+ import { DurationWheelField, durationWheelAvailable } from "./DurationWheel";
23
24
  import {
24
25
  durationSegmentWidth,
26
+ durationToWheelDate,
27
+ formatDurationParts,
25
28
  normalizeSeconds,
26
29
  parseDurationText,
27
30
  partsToSeconds,
28
31
  secondsToParts,
32
+ wheelDateToDuration,
29
33
  type DurationParts,
30
34
  } from "./durationMath";
31
35
 
32
36
  export type { DurationParts };
33
37
  export {
34
38
  durationSegmentWidth,
39
+ durationToWheelDate,
40
+ formatDurationParts,
35
41
  normalizeSeconds,
36
42
  parseDurationText,
37
43
  partsToSeconds,
38
44
  secondsToParts,
45
+ wheelDateToDuration,
39
46
  };
40
47
 
41
48
  export interface DurationPreset {
@@ -374,16 +381,15 @@ function DurationPresets({
374
381
  {presets.map((preset) => {
375
382
  const selected = seconds === preset.seconds;
376
383
  return (
377
- <Button
378
- key={preset.seconds}
379
- size={DURATION_PRESET_SIZE}
380
- outlined={!selected}
381
- accent={selected}
382
- data-duration-preset={preset.label}
383
- onPress={() => onChange(preset.seconds)}
384
- >
385
- {preset.label}
386
- </Button>
384
+ <XStack key={preset.seconds} data-duration-preset={preset.label}>
385
+ <Chip
386
+ size={DURATION_PRESET_SIZE}
387
+ selected={selected}
388
+ onPress={() => onChange(preset.seconds)}
389
+ >
390
+ {preset.label}
391
+ </Chip>
392
+ </XStack>
387
393
  );
388
394
  })}
389
395
  </XStack>
@@ -425,6 +431,11 @@ export interface DurationProps {
425
431
  clockIcon?: ReactNode | null;
426
432
  /** When true, renders a skeleton placeholder instead of the duration picker */
427
433
  skeleton?: boolean;
434
+ /**
435
+ * On iOS and Android a tap opens the system hours/minutes wheel (LC-266).
436
+ * `false` keeps the typed segments there too. Web always types.
437
+ */
438
+ native?: boolean;
428
439
  }
429
440
 
430
441
  export function Duration({
@@ -450,6 +461,7 @@ export function Duration({
450
461
  placeholder = t("Select duration"),
451
462
  clockIcon,
452
463
  skeleton,
464
+ native,
453
465
  }: DurationProps) {
454
466
  const hydrationTouch = useTouchSurface();
455
467
  const { resolvedForm, knobProps, id } = useFormField({
@@ -489,24 +501,43 @@ export function Duration({
489
501
  );
490
502
  }
491
503
 
504
+ const wheel = native !== false && !showSeconds && durationWheelAvailable();
505
+
492
506
  const fieldBody = (current: number, onValueChange: (s: number) => void, hasError?: boolean) => (
493
507
  <YStack {...knobProps.gap}>
494
- <DurationControl
495
- seconds={current}
496
- onChange={onValueChange}
497
- disabled={disabled}
498
- readOnly={readOnly}
499
- hideDays={hideDays}
500
- showSeconds={showSeconds}
501
- clockIcon={clockIcon}
502
- hasError={hasError}
503
- id={id}
504
- hasLabel={Boolean(label)}
505
- placeholder={placeholder}
506
- required={required}
507
- size={size}
508
- knobProps={knobProps}
509
- />
508
+ {wheel ? (
509
+ <DurationWheelField
510
+ seconds={current}
511
+ onChange={onValueChange}
512
+ disabled={disabled}
513
+ readOnly={readOnly}
514
+ foldDays={hideDays}
515
+ clockIcon={clockIcon}
516
+ hasError={hasError}
517
+ id={id}
518
+ hasLabel={Boolean(label)}
519
+ required={required}
520
+ size={size}
521
+ knobProps={knobProps}
522
+ />
523
+ ) : (
524
+ <DurationControl
525
+ seconds={current}
526
+ onChange={onValueChange}
527
+ disabled={disabled}
528
+ readOnly={readOnly}
529
+ hideDays={hideDays}
530
+ showSeconds={showSeconds}
531
+ clockIcon={clockIcon}
532
+ hasError={hasError}
533
+ id={id}
534
+ hasLabel={Boolean(label)}
535
+ placeholder={placeholder}
536
+ required={required}
537
+ size={size}
538
+ knobProps={knobProps}
539
+ />
540
+ )}
510
541
  {readOnly || disabled || inTableCell ? null : (
511
542
  <DurationPresets
512
543
  seconds={current}
@@ -98,7 +98,7 @@ describe("Select seamless cover (LC-112, FZ-8438)", () => {
98
98
  });
99
99
  });
100
100
 
101
- describe("Select drop-up (LC-112, FZ-8438 r3)", () => {
101
+ describe("Select cover and scroll (LC-112, FZ-8463)", () => {
102
102
  const field = (top: number) => ({
103
103
  x: 16,
104
104
  y: top,
@@ -111,6 +111,7 @@ describe("Select drop-up (LC-112, FZ-8438 r3)", () => {
111
111
  toJSON: () => ({}),
112
112
  });
113
113
  let fieldTop = 700;
114
+ let contentHeight = 300;
114
115
  beforeEach(() => {
115
116
  Object.defineProperty(window, "innerWidth", {
116
117
  configurable: true,
@@ -136,7 +137,7 @@ describe("Select drop-up (LC-112, FZ-8438 r3)", () => {
136
137
  );
137
138
  vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockImplementation(
138
139
  function (this: HTMLElement) {
139
- return this.dataset.testid === "select-dropdown" ? 300 : 0;
140
+ return this.dataset.testid === "select-dropdown" ? contentHeight : 0;
140
141
  },
141
142
  );
142
143
  });
@@ -144,28 +145,103 @@ describe("Select drop-up (LC-112, FZ-8438 r3)", () => {
144
145
  vi.restoreAllMocks();
145
146
  });
146
147
 
147
- it("a field too low for its list opens upward, its bottom edge on the field's", async () => {
148
+ it("a low field's list covers the field with no echo and grows up from its bottom edge", async () => {
148
149
  fieldTop = 700;
150
+ contentHeight = 300;
149
151
  const result = renderWithProviders(<Select options={options} value="hel" />);
150
152
  const { dropdown } = await openTop(result);
151
- // The panel's bottom edge is the field's (740): the 300 list plus the
152
- // frame's borders, above it.
153
- await waitFor(() => expect(parseFloat(dropdown.style.top)).toBeLessThan(700));
154
- expect(parseFloat(dropdown.style.top) + parseFloat(dropdown.style.maxHeight)).toBe(740);
153
+ await waitFor(() => expect(dropdown.querySelector("[data-fp-trigger-row]")).toBeNull());
154
+ // The panel's bottom edge is the field's (740), the whole list above it.
155
+ await waitFor(() =>
156
+ expect(parseFloat(dropdown.style.top) + parseFloat(dropdown.style.maxHeight)).toBe(740),
157
+ );
158
+ expect(parseFloat(dropdown.style.top)).toBeLessThan(700);
155
159
  expect(parseFloat(dropdown.style.maxHeight)).toBeGreaterThanOrEqual(300);
156
- const echo = dropdown.querySelector<HTMLElement>("[data-fp-trigger-row]");
157
- await waitFor(() => expect(echo?.style.order).toBe("1"));
158
- expect(echo?.style.top).toBe("");
159
- expect(echo?.style.bottom).not.toBe("");
160
+ expect(dropdown.textContent?.match(/Helsinki/g)?.length ?? 0).toBe(1);
161
+ });
162
+
163
+ it("a low field's long list stops at the safe top edge and scrolls", async () => {
164
+ fieldTop = 700;
165
+ contentHeight = 2000;
166
+ const result = renderWithProviders(<Select options={options} value="hel" />);
167
+ const { dropdown } = await openTop(result);
168
+ await waitFor(() => expect(dropdown.querySelector("[data-fp-trigger-row]")).toBeNull());
169
+ await waitFor(() => expect(parseFloat(dropdown.style.top)).toBe(10));
170
+ expect(parseFloat(dropdown.style.maxHeight)).toBe(730);
171
+ });
172
+
173
+ it("a high field's long list covers the field and grows down to the safe bottom edge", async () => {
174
+ fieldTop = 120;
175
+ contentHeight = 2000;
176
+ const result = renderWithProviders(<Select options={options} value="hel" />);
177
+ const { dropdown } = await openTop(result);
178
+ await waitFor(() => expect(dropdown.querySelector("[data-fp-trigger-row]")).toBeNull());
179
+ expect(parseFloat(dropdown.style.top)).toBe(120);
180
+ await waitFor(() => expect(parseFloat(dropdown.style.maxHeight)).toBe(768 - 10 - 120));
181
+ });
182
+
183
+ describe("with 36px rows", () => {
184
+ const rowTop = 4;
185
+ const rowHeight = 36;
186
+ beforeEach(() => {
187
+ const rowIndex = (el: HTMLElement) =>
188
+ Array.from(
189
+ el.closest('[data-testid="select-dropdown"]')?.querySelectorAll('[role="option"]') ?? [],
190
+ ).indexOf(el);
191
+ vi.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockImplementation(
192
+ function (this: HTMLElement) {
193
+ return this.getAttribute("role") === "option" ? rowHeight : 0;
194
+ },
195
+ );
196
+ vi.spyOn(HTMLElement.prototype, "offsetTop", "get").mockImplementation(
197
+ function (this: HTMLElement) {
198
+ return this.getAttribute("role") === "option" ? rowTop + rowIndex(this) * rowHeight : 0;
199
+ },
200
+ );
201
+ vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockImplementation(
202
+ function (this: HTMLElement) {
203
+ return this.dataset.testid === "select-dropdown"
204
+ ? parseFloat(this.style.maxHeight) || 0
205
+ : 0;
206
+ },
207
+ );
208
+ });
209
+
210
+ it("growing up, the row on the field is whole: the panel's bottom stays on the field", async () => {
211
+ fieldTop = 700;
212
+ contentHeight = 2000;
213
+ const result = renderWithProviders(<Select options={options} value="hel" />);
214
+ const { dropdown } = await openTop(result);
215
+ // 730 of room above holds 20 whole rows and 6px of a 21st: that sliver goes.
216
+ await waitFor(() => expect(parseFloat(dropdown.style.maxHeight)).toBe(724));
217
+ expect(parseFloat(dropdown.style.top)).toBe(16);
218
+ const visibleBottom = dropdown.scrollTop + parseFloat(dropdown.style.maxHeight);
219
+ expect((visibleBottom - rowTop) % rowHeight).toBe(0);
220
+ });
221
+
222
+ it("growing down, no sliver of a row sits at the screen edge", async () => {
223
+ fieldTop = 120;
224
+ contentHeight = 2000;
225
+ const result = renderWithProviders(<Select options={options} value="hel" />);
226
+ const { dropdown } = await openTop(result);
227
+ expect(parseFloat(dropdown.style.top)).toBe(120);
228
+ await waitFor(() =>
229
+ expect(parseFloat(dropdown.style.maxHeight)).toBeLessThan(768 - 10 - 120),
230
+ );
231
+ const visibleBottom = dropdown.scrollTop + parseFloat(dropdown.style.maxHeight);
232
+ expect((visibleBottom - rowTop) % rowHeight).toBe(0);
233
+ });
160
234
  });
161
235
 
162
236
  it("a field with room below keeps the echo on top, opening down", async () => {
163
237
  fieldTop = 120;
238
+ contentHeight = 300;
164
239
  const result = renderWithProviders(<Select options={options} value="hel" />);
165
240
  const { dropdown, top } = await openTop(result);
166
241
  expect(top).toBe(120);
167
242
  const echo = dropdown.querySelector<HTMLElement>("[data-fp-trigger-row]");
168
- expect(echo?.style.order).toBe("");
243
+ expect(echo).not.toBeNull();
244
+ expect(dropdown.firstElementChild).toBe(echo);
169
245
  });
170
246
  });
171
247