@duro-app/ui 5.11.0 → 5.13.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 (331) hide show
  1. package/dist/Table-Bms2ftt3.js +1694 -0
  2. package/dist/Table-Bms2ftt3.js.map +1 -0
  3. package/dist/compiler-runtime-BuugS4D0.js.map +1 -1
  4. package/dist/components/Alert/Alert.d.ts.map +1 -1
  5. package/dist/components/AppShell/native.css.d.ts +53 -0
  6. package/dist/components/AppShell/native.css.d.ts.map +1 -0
  7. package/dist/components/Button/Button.d.ts +2 -1
  8. package/dist/components/Button/Button.d.ts.map +1 -1
  9. package/dist/components/Button/styles.css.d.ts +1 -0
  10. package/dist/components/Button/styles.css.d.ts.map +1 -1
  11. package/dist/components/Callout/Callout.d.ts.map +1 -1
  12. package/dist/components/Callout/styles.css.d.ts +3 -0
  13. package/dist/components/Callout/styles.css.d.ts.map +1 -1
  14. package/dist/components/Checkbox/Checkbox.d.ts.map +1 -1
  15. package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -1
  16. package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -1
  17. package/dist/components/Dialog/Dialog.d.ts +2 -17
  18. package/dist/components/Dialog/Dialog.d.ts.map +1 -1
  19. package/dist/components/Dialog/Dialog.stories.d.ts +4 -0
  20. package/dist/components/Dialog/Dialog.stories.d.ts.map +1 -1
  21. package/dist/components/Dialog/native.css.d.ts +82 -0
  22. package/dist/components/Dialog/native.css.d.ts.map +1 -0
  23. package/dist/components/Dialog/parts.css.d.ts +82 -0
  24. package/dist/components/Dialog/parts.css.d.ts.map +1 -0
  25. package/dist/components/Dialog/parts.d.ts +35 -0
  26. package/dist/components/Dialog/parts.d.ts.map +1 -0
  27. package/dist/components/Dialog/styles.css.d.ts +1 -74
  28. package/dist/components/Dialog/styles.css.d.ts.map +1 -1
  29. package/dist/components/Drawer/Drawer.d.ts +7 -35
  30. package/dist/components/Drawer/Drawer.d.ts.map +1 -1
  31. package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
  32. package/dist/components/Drawer/DrawerScroll.stories.d.ts.map +1 -1
  33. package/dist/components/Drawer/DrawerSnap.stories.d.ts +20 -0
  34. package/dist/components/Drawer/DrawerSnap.stories.d.ts.map +1 -0
  35. package/dist/components/Drawer/native.css.d.ts +55 -0
  36. package/dist/components/Drawer/native.css.d.ts.map +1 -0
  37. package/dist/components/Drawer/parts.css.d.ts +96 -0
  38. package/dist/components/Drawer/parts.css.d.ts.map +1 -0
  39. package/dist/components/Drawer/parts.d.ts +56 -0
  40. package/dist/components/Drawer/parts.d.ts.map +1 -0
  41. package/dist/components/Drawer/sheetPhysics.d.ts +56 -0
  42. package/dist/components/Drawer/sheetPhysics.d.ts.map +1 -0
  43. package/dist/components/Drawer/styles.css.d.ts +15 -58
  44. package/dist/components/Drawer/styles.css.d.ts.map +1 -1
  45. package/dist/components/Drawer/touchInput.d.ts +35 -0
  46. package/dist/components/Drawer/touchInput.d.ts.map +1 -0
  47. package/dist/components/Drawer/useDrawerRoot.d.ts +33 -0
  48. package/dist/components/Drawer/useDrawerRoot.d.ts.map +1 -0
  49. package/dist/components/Drawer/useSheetHeights.d.ts +7 -0
  50. package/dist/components/Drawer/useSheetHeights.d.ts.map +1 -0
  51. package/dist/components/Drawer/useSwipeDismiss.d.ts +13 -2
  52. package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -1
  53. package/dist/components/Grid/styles.css.d.ts +1 -0
  54. package/dist/components/Grid/styles.css.d.ts.map +1 -1
  55. package/dist/components/Icon/Icon.d.ts +3 -9
  56. package/dist/components/Icon/Icon.d.ts.map +1 -1
  57. package/dist/components/Icon/geometry.d.ts +60 -0
  58. package/dist/components/Icon/geometry.d.ts.map +1 -0
  59. package/dist/components/Icon/types.d.ts +15 -0
  60. package/dist/components/Icon/types.d.ts.map +1 -0
  61. package/dist/components/Input/Input.d.ts +5 -1
  62. package/dist/components/Input/Input.d.ts.map +1 -1
  63. package/dist/components/Input/styles.css.d.ts +1 -0
  64. package/dist/components/Input/styles.css.d.ts.map +1 -1
  65. package/dist/components/LinkButton/styles.css.d.ts +1 -0
  66. package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
  67. package/dist/components/Menu/Menu.d.ts +7 -1
  68. package/dist/components/Menu/Menu.d.ts.map +1 -1
  69. package/dist/components/Menu/native.css.d.ts +14 -0
  70. package/dist/components/Menu/native.css.d.ts.map +1 -0
  71. package/dist/components/Menu/useMenuOpen.d.ts +16 -0
  72. package/dist/components/Menu/useMenuOpen.d.ts.map +1 -0
  73. package/dist/components/Menu/useMenuRoot.d.ts +3 -6
  74. package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
  75. package/dist/components/Meter/styles.css.d.ts +1 -0
  76. package/dist/components/Meter/styles.css.d.ts.map +1 -1
  77. package/dist/components/PageShell/styles.css.d.ts +2 -0
  78. package/dist/components/PageShell/styles.css.d.ts.map +1 -1
  79. package/dist/components/Popover/native.css.d.ts +11 -0
  80. package/dist/components/Popover/native.css.d.ts.map +1 -0
  81. package/dist/components/RadioGroup/RadioGroup.d.ts +1 -17
  82. package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  83. package/dist/components/RadioGroup/RadioGroup.stories.d.ts +3 -0
  84. package/dist/components/RadioGroup/RadioGroup.stories.d.ts.map +1 -1
  85. package/dist/components/RadioGroup/group.d.ts +31 -0
  86. package/dist/components/RadioGroup/group.d.ts.map +1 -0
  87. package/dist/components/RadioGroup/styles.css.d.ts +10 -0
  88. package/dist/components/RadioGroup/styles.css.d.ts.map +1 -1
  89. package/dist/components/ScrollArea/styles.css.d.ts +1 -0
  90. package/dist/components/ScrollArea/styles.css.d.ts.map +1 -1
  91. package/dist/components/SideNav/Chevron.d.ts +4 -0
  92. package/dist/components/SideNav/Chevron.d.ts.map +1 -0
  93. package/dist/components/SideNav/SideNav.d.ts.map +1 -1
  94. package/dist/components/SideNav/styles.css.d.ts +6 -0
  95. package/dist/components/SideNav/styles.css.d.ts.map +1 -1
  96. package/dist/components/SideNav/types.d.ts +6 -0
  97. package/dist/components/SideNav/types.d.ts.map +1 -0
  98. package/dist/components/Spinner/Spinner.d.ts +2 -6
  99. package/dist/components/Spinner/Spinner.d.ts.map +1 -1
  100. package/dist/components/Spinner/Spinner.types.d.ts +6 -0
  101. package/dist/components/Spinner/Spinner.types.d.ts.map +1 -0
  102. package/dist/components/Spinner/ring.css.d.ts +39 -0
  103. package/dist/components/Spinner/ring.css.d.ts.map +1 -0
  104. package/dist/components/Spinner/styles.css.d.ts +1 -28
  105. package/dist/components/Spinner/styles.css.d.ts.map +1 -1
  106. package/dist/components/SplitPane/SplitPane.d.ts +38 -0
  107. package/dist/components/SplitPane/SplitPane.d.ts.map +1 -0
  108. package/dist/components/SplitPane/SplitPane.meta.d.ts +3 -0
  109. package/dist/components/SplitPane/SplitPane.meta.d.ts.map +1 -0
  110. package/dist/components/SplitPane/SplitPane.stories.d.ts +17 -0
  111. package/dist/components/SplitPane/SplitPane.stories.d.ts.map +1 -0
  112. package/dist/components/SplitPane/styles.css.d.ts +78 -0
  113. package/dist/components/SplitPane/styles.css.d.ts.map +1 -0
  114. package/dist/components/StatusIcon/StatusIcon.d.ts.map +1 -1
  115. package/dist/components/StatusIcon/styles.css.d.ts +3 -0
  116. package/dist/components/StatusIcon/styles.css.d.ts.map +1 -1
  117. package/dist/components/Switch/Switch.d.ts +2 -18
  118. package/dist/components/Switch/Switch.d.ts.map +1 -1
  119. package/dist/components/Switch/Switch.types.d.ts +18 -0
  120. package/dist/components/Switch/Switch.types.d.ts.map +1 -0
  121. package/dist/components/TableOfContents/styles.css.d.ts +1 -0
  122. package/dist/components/TableOfContents/styles.css.d.ts.map +1 -1
  123. package/dist/components/Tabs/TabLayout.d.ts +7 -0
  124. package/dist/components/Tabs/TabLayout.d.ts.map +1 -0
  125. package/dist/components/Tabs/Tabs.d.ts.map +1 -1
  126. package/dist/components/Tabs/indicator.d.ts +28 -0
  127. package/dist/components/Tabs/indicator.d.ts.map +1 -0
  128. package/dist/components/Tabs/styles.css.d.ts +4 -0
  129. package/dist/components/Tabs/styles.css.d.ts.map +1 -1
  130. package/dist/components/Tabs/useTabIndicator.d.ts +4 -0
  131. package/dist/components/Tabs/useTabIndicator.d.ts.map +1 -0
  132. package/dist/components/Tabs/useTabKeys.d.ts +13 -0
  133. package/dist/components/Tabs/useTabKeys.d.ts.map +1 -0
  134. package/dist/components/Tag/RemoveGlyph.d.ts +4 -0
  135. package/dist/components/Tag/RemoveGlyph.d.ts.map +1 -0
  136. package/dist/components/Tag/Tag.d.ts +2 -24
  137. package/dist/components/Tag/Tag.d.ts.map +1 -1
  138. package/dist/components/Tag/styles.css.d.ts +8 -0
  139. package/dist/components/Tag/styles.css.d.ts.map +1 -1
  140. package/dist/components/Tag/types.d.ts +39 -0
  141. package/dist/components/Tag/types.d.ts.map +1 -0
  142. package/dist/components/Textarea/styles.css.d.ts +1 -0
  143. package/dist/components/Textarea/styles.css.d.ts.map +1 -1
  144. package/dist/components/Toast/ToastProvider.d.ts +2 -21
  145. package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
  146. package/dist/components/Toast/native.css.d.ts +81 -0
  147. package/dist/components/Toast/native.css.d.ts.map +1 -0
  148. package/dist/components/Toast/store.d.ts +47 -0
  149. package/dist/components/Toast/store.d.ts.map +1 -0
  150. package/dist/components/Toggle/Toggle.d.ts.map +1 -1
  151. package/dist/components/Toggle/styles.css.d.ts +11 -2
  152. package/dist/components/Toggle/styles.css.d.ts.map +1 -1
  153. package/dist/components/Tooltip/TooltipHint.d.ts +9 -0
  154. package/dist/components/Tooltip/TooltipHint.d.ts.map +1 -0
  155. package/dist/components/Tree/Tree.d.ts.map +1 -1
  156. package/dist/components/Tree/styles.css.d.ts +6 -0
  157. package/dist/components/Tree/styles.css.d.ts.map +1 -1
  158. package/dist/components/Tree/useTreeKeyboard.d.ts +19 -0
  159. package/dist/components/Tree/useTreeKeyboard.d.ts.map +1 -0
  160. package/dist/components/VirtualList/VirtualList.d.ts +33 -0
  161. package/dist/components/VirtualList/VirtualList.d.ts.map +1 -0
  162. package/dist/components/VirtualList/VirtualList.meta.d.ts +3 -0
  163. package/dist/components/VirtualList/VirtualList.meta.d.ts.map +1 -0
  164. package/dist/components/VirtualList/VirtualList.stories.d.ts +15 -0
  165. package/dist/components/VirtualList/VirtualList.stories.d.ts.map +1 -0
  166. package/dist/components/VirtualList/styles.css.d.ts +28 -0
  167. package/dist/components/VirtualList/styles.css.d.ts.map +1 -0
  168. package/dist/components/VirtualTable/styles.css.d.ts +6 -0
  169. package/dist/components/VirtualTable/styles.css.d.ts.map +1 -1
  170. package/dist/index.css +1 -1
  171. package/dist/index.d.ts +3 -1
  172. package/dist/index.d.ts.map +1 -1
  173. package/dist/index.js +6202 -4794
  174. package/dist/index.js.map +1 -1
  175. package/dist/shared/ButtonHost.d.ts +11 -0
  176. package/dist/shared/ButtonHost.d.ts.map +1 -0
  177. package/dist/shared/portal/hostRegistry.d.ts +33 -0
  178. package/dist/shared/portal/hostRegistry.d.ts.map +1 -0
  179. package/dist/shared/pressableChild.d.ts +14 -0
  180. package/dist/shared/pressableChild.d.ts.map +1 -0
  181. package/dist/shared/useOverlayOpenState.d.ts +40 -0
  182. package/dist/shared/useOverlayOpenState.d.ts.map +1 -0
  183. package/dist/table.js +141 -135
  184. package/dist/table.js.map +1 -1
  185. package/package.json +34 -4
  186. package/src/components/Alert/Alert.tsx +14 -2
  187. package/src/components/AppShell/AppShell.meta.ts +1 -1
  188. package/src/components/AppShell/AppShell.native.tsx +174 -0
  189. package/src/components/AppShell/native.css.ts +56 -0
  190. package/src/components/Button/Button.tsx +6 -5
  191. package/src/components/Button/styles.css.ts +2 -0
  192. package/src/components/Callout/Callout.tsx +17 -3
  193. package/src/components/Callout/styles.css.ts +5 -0
  194. package/src/components/Checkbox/Checkbox.native.tsx +67 -0
  195. package/src/components/Checkbox/Checkbox.tsx +1 -50
  196. package/src/components/ConfirmDialog/ConfirmDialog.tsx +3 -0
  197. package/src/components/DetailPanel/DetailPanel.tsx +9 -45
  198. package/src/components/Dialog/Dialog.native.tsx +296 -0
  199. package/src/components/Dialog/Dialog.stories.tsx +71 -0
  200. package/src/components/Dialog/Dialog.tsx +41 -130
  201. package/src/components/Dialog/native.css.ts +78 -0
  202. package/src/components/Dialog/parts.css.ts +101 -0
  203. package/src/components/Dialog/parts.tsx +61 -0
  204. package/src/components/Dialog/styles.css.ts +2 -89
  205. package/src/components/Drawer/Drawer.meta.ts +2 -1
  206. package/src/components/Drawer/Drawer.native.tsx +469 -0
  207. package/src/components/Drawer/Drawer.tsx +63 -179
  208. package/src/components/Drawer/DrawerScroll.stories.tsx +4 -54
  209. package/src/components/Drawer/DrawerSnap.stories.tsx +205 -0
  210. package/src/components/Drawer/native.css.ts +58 -0
  211. package/src/components/Drawer/parts.css.ts +125 -0
  212. package/src/components/Drawer/parts.tsx +110 -0
  213. package/src/components/Drawer/sheetPhysics.ts +103 -0
  214. package/src/components/Drawer/styles.css.ts +19 -74
  215. package/src/components/Drawer/touchInput.ts +81 -0
  216. package/src/components/Drawer/useDrawerRoot.ts +90 -0
  217. package/src/components/Drawer/useSheetHeights.ts +42 -0
  218. package/src/components/Drawer/useSwipeDismiss.ts +146 -23
  219. package/src/components/Grid/styles.css.ts +2 -0
  220. package/src/components/Icon/Icon.native.tsx +54 -0
  221. package/src/components/Icon/Icon.tsx +19 -430
  222. package/src/components/Icon/geometry.ts +456 -0
  223. package/src/components/Icon/types.ts +74 -0
  224. package/src/components/Input/Input.tsx +8 -0
  225. package/src/components/Input/styles.css.ts +2 -0
  226. package/src/components/LinkButton/styles.css.ts +2 -0
  227. package/src/components/Menu/Menu.meta.ts +1 -1
  228. package/src/components/Menu/Menu.native.tsx +268 -0
  229. package/src/components/Menu/Menu.tsx +11 -2
  230. package/src/components/Menu/native.css.ts +18 -0
  231. package/src/components/Menu/useMenuOpen.ts +33 -0
  232. package/src/components/Menu/useMenuRoot.ts +7 -26
  233. package/src/components/Meter/styles.css.ts +2 -0
  234. package/src/components/PageShell/styles.css.ts +4 -0
  235. package/src/components/Popover/Popover.meta.ts +1 -1
  236. package/src/components/Popover/Popover.native.tsx +205 -0
  237. package/src/components/Popover/native.css.ts +13 -0
  238. package/src/components/RadioGroup/RadioGroup.meta.ts +1 -1
  239. package/src/components/RadioGroup/RadioGroup.native.tsx +48 -0
  240. package/src/components/RadioGroup/RadioGroup.stories.tsx +64 -1
  241. package/src/components/RadioGroup/RadioGroup.tsx +8 -83
  242. package/src/components/RadioGroup/group.tsx +85 -0
  243. package/src/components/RadioGroup/styles.css.ts +13 -0
  244. package/src/components/ScrollArea/styles.css.ts +2 -0
  245. package/src/components/SideNav/Chevron.native.tsx +20 -0
  246. package/src/components/SideNav/Chevron.tsx +20 -0
  247. package/src/components/SideNav/SideNav.meta.ts +1 -1
  248. package/src/components/SideNav/SideNav.tsx +57 -41
  249. package/src/components/SideNav/styles.css.ts +8 -0
  250. package/src/components/SideNav/types.ts +6 -0
  251. package/src/components/Spinner/Spinner.native.tsx +37 -0
  252. package/src/components/Spinner/Spinner.tsx +6 -15
  253. package/src/components/Spinner/Spinner.types.ts +6 -0
  254. package/src/components/Spinner/ring.css.ts +45 -0
  255. package/src/components/Spinner/styles.css.ts +2 -32
  256. package/src/components/SplitPane/SplitPane.meta.ts +40 -0
  257. package/src/components/SplitPane/SplitPane.native.tsx +47 -0
  258. package/src/components/SplitPane/SplitPane.stories.tsx +148 -0
  259. package/src/components/SplitPane/SplitPane.tsx +218 -0
  260. package/src/components/SplitPane/styles.css.ts +81 -0
  261. package/src/components/StatusIcon/StatusIcon.tsx +13 -2
  262. package/src/components/StatusIcon/styles.css.ts +5 -0
  263. package/src/components/Switch/Switch.native.tsx +45 -0
  264. package/src/components/Switch/Switch.tsx +22 -44
  265. package/src/components/Switch/Switch.types.ts +18 -0
  266. package/src/components/TableOfContents/styles.css.ts +2 -0
  267. package/src/components/Tabs/TabLayout.native.tsx +9 -0
  268. package/src/components/Tabs/TabLayout.tsx +6 -0
  269. package/src/components/Tabs/Tabs.meta.ts +1 -1
  270. package/src/components/Tabs/Tabs.tsx +38 -136
  271. package/src/components/Tabs/indicator.ts +36 -0
  272. package/src/components/Tabs/styles.css.ts +7 -0
  273. package/src/components/Tabs/useTabIndicator.native.ts +35 -0
  274. package/src/components/Tabs/useTabIndicator.ts +49 -0
  275. package/src/components/Tabs/useTabKeys.native.ts +2 -0
  276. package/src/components/Tabs/useTabKeys.ts +99 -0
  277. package/src/components/Tag/RemoveGlyph.native.tsx +17 -0
  278. package/src/components/Tag/RemoveGlyph.tsx +10 -0
  279. package/src/components/Tag/Tag.meta.ts +1 -1
  280. package/src/components/Tag/Tag.native.tsx +70 -0
  281. package/src/components/Tag/Tag.tsx +9 -37
  282. package/src/components/Tag/styles.css.ts +9 -0
  283. package/src/components/Tag/types.ts +43 -0
  284. package/src/components/Textarea/styles.css.ts +2 -0
  285. package/src/components/ThemeProvider/ThemeProvider.native.tsx +70 -10
  286. package/src/components/Toast/Toast.meta.ts +1 -1
  287. package/src/components/Toast/ToastProvider.native.tsx +226 -0
  288. package/src/components/Toast/ToastProvider.tsx +22 -84
  289. package/src/components/Toast/native.css.ts +78 -0
  290. package/src/components/Toast/store.ts +96 -0
  291. package/src/components/Toggle/Toggle.tsx +5 -3
  292. package/src/components/Toggle/styles.css.ts +14 -2
  293. package/src/components/Tooltip/Tooltip.meta.ts +1 -1
  294. package/src/components/Tooltip/Tooltip.native.tsx +36 -0
  295. package/src/components/Tooltip/TooltipHint.ts +13 -0
  296. package/src/components/Tree/Tree.meta.ts +1 -1
  297. package/src/components/Tree/Tree.tsx +23 -135
  298. package/src/components/Tree/styles.css.ts +6 -0
  299. package/src/components/Tree/useTreeKeyboard.native.ts +5 -0
  300. package/src/components/Tree/useTreeKeyboard.ts +152 -0
  301. package/src/components/VirtualList/VirtualList.meta.ts +39 -0
  302. package/src/components/VirtualList/VirtualList.stories.tsx +95 -0
  303. package/src/components/VirtualList/VirtualList.tsx +103 -0
  304. package/src/components/VirtualList/styles.css.ts +31 -0
  305. package/src/components/VirtualTable/styles.css.ts +12 -0
  306. package/src/hooks/useContainerBelow.native.ts +30 -0
  307. package/src/index.native.ts +61 -1
  308. package/src/index.ts +8 -1
  309. package/src/native-guard/globals.d.ts +10 -0
  310. package/src/native-guard/react-dom.d.ts +3 -0
  311. package/src/shared/ButtonHost.native.tsx +70 -0
  312. package/src/shared/ButtonHost.tsx +12 -0
  313. package/src/shared/accessibilityFocus.native.ts +23 -0
  314. package/src/shared/portal/AnchoredOverlay.native.tsx +117 -0
  315. package/src/shared/portal/OverlayModal.native.tsx +51 -0
  316. package/src/shared/portal/PortalHost.native.tsx +102 -0
  317. package/src/shared/portal/hostRegistry.ts +76 -0
  318. package/src/shared/portal/themeScope.native.ts +18 -0
  319. package/src/shared/pressableChild.ts +33 -0
  320. package/src/shared/safeArea.native.ts +22 -0
  321. package/src/shared/useKeyboardHeight.native.ts +18 -0
  322. package/src/shared/useOverlayOpenState.ts +128 -0
  323. package/src/shared/useScreenReaderEnabled.native.ts +19 -0
  324. package/dist/Table-ColfpgKV.js +0 -1552
  325. package/dist/Table-ColfpgKV.js.map +0 -1
  326. package/dist/components/ThemeProvider/ThemeProvider.native.d.ts +0 -10
  327. package/dist/components/ThemeProvider/ThemeProvider.native.d.ts.map +0 -1
  328. package/dist/index.native.d.ts +0 -33
  329. package/dist/index.native.d.ts.map +0 -1
  330. package/dist/platform.native.d.ts +0 -2
  331. package/dist/platform.native.d.ts.map +0 -1
@@ -1,27 +1,29 @@
1
1
  import {createPortal} from 'react-dom'
2
- import {
3
- type ReactNode,
4
- type RefObject,
5
- createContext,
6
- useContext,
7
- useState,
8
- useEffect,
9
- useCallback,
10
- useId,
11
- useRef,
12
- } from 'react'
2
+ import {type ReactNode, useState, useEffect, useCallback} from 'react'
13
3
  import {html} from 'react-strict-dom'
14
- import {DURATION_MS, type DurationToken} from '@duro-app/tokens/keys'
4
+ import type {DurationToken} from '@duro-app/tokens/keys'
15
5
  import {styles} from './styles.css'
16
6
  import {usePortalMount} from '../ThemeProvider/ThemeProvider'
17
7
  import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
18
8
  import {ModalContext} from '../../shared/ModalContext'
19
9
  import {useSwipeDismiss} from './useSwipeDismiss'
10
+ import {useSheetHeights} from './useSheetHeights'
11
+ import {cycleDetent} from './sheetPhysics'
12
+ import {partStyles} from './parts.css'
13
+ import {
14
+ DEFAULT_GRABBER_LABELS,
15
+ Description,
16
+ DrawerContext,
17
+ Footer,
18
+ Header,
19
+ Title,
20
+ useDrawer,
21
+ type DrawerSize,
22
+ type GrabberLabels,
23
+ } from './parts'
24
+ import {useDrawerRoot, type DrawerRootProps} from './useDrawerRoot'
20
25
 
21
- // --- Types ---
22
-
23
- export type DrawerAnchor = 'right' | 'left' | 'bottom'
24
- export type DrawerSize = 'sm' | 'md' | 'lg'
26
+ export type {DrawerAnchor, DrawerSize, SnapPoint} from './parts'
25
27
 
26
28
  const closeDurationMap = {
27
29
  instant: styles.closeDurationInstant,
@@ -33,113 +35,15 @@ const closeDurationMap = {
33
35
  slow: styles.closeDurationSlow,
34
36
  } as const satisfies Record<DurationToken, unknown>
35
37
 
36
- // --- Context ---
37
-
38
- interface DrawerContextValue {
39
- open: boolean
40
- closing: boolean
41
- closeDuration: DurationToken
42
- anchor: DrawerAnchor
43
- dismissable: boolean
44
- swipeDismiss: boolean
45
- requestOpen: () => void
46
- requestClose: () => void
47
- /** Close immediately without CSS animation (used by swipe dismiss which handles its own exit animation) */
48
- requestCloseImmediate: () => void
49
- titleId: string
50
- descriptionId: string
51
- panelRef: RefObject<HTMLDivElement | null>
52
- }
53
-
54
- const DrawerContext = createContext<DrawerContextValue | null>(null)
55
-
56
- function useDrawer() {
57
- const ctx = useContext(DrawerContext)
58
- if (!ctx) throw new Error('Drawer compound components must be used within Drawer.Root')
59
- return ctx
60
- }
61
-
62
38
  // --- Root ---
63
39
 
64
- interface RootProps {
65
- children: ReactNode
66
- /** Controlled open state */
67
- open?: boolean
68
- /** Default open state (uncontrolled) */
69
- defaultOpen?: boolean
70
- /** Called when drawer open state changes */
71
- onOpenChange?: (open: boolean) => void
72
- /** Which edge the drawer slides from. Default: 'right' */
73
- anchor?: DrawerAnchor
74
- /** Whether clicking backdrop or pressing Escape closes the drawer. Default: true */
75
- dismissable?: boolean
76
- /** Whether swipe gesture dismisses the drawer. Default: true */
77
- swipeDismiss?: boolean
78
- /** Motion token for the close animation. Default: 'fast' (150ms) */
79
- closeAnimationDuration?: DurationToken
80
- }
81
-
82
- function Root({
83
- children,
84
- open: controlledOpen,
85
- defaultOpen = false,
86
- onOpenChange,
87
- anchor = 'right',
88
- dismissable = true,
89
- swipeDismiss = true,
90
- closeAnimationDuration = 'fast',
91
- }: RootProps) {
92
- const isControlled = controlledOpen !== undefined
93
- const [internalOpen, setInternalOpen] = useState(defaultOpen)
94
- const [closing, setClosing] = useState(false)
95
- const closingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
96
- const panelRef = useRef<HTMLDivElement>(null)
97
-
98
- const isOpen = isControlled ? controlledOpen : internalOpen
99
-
100
- const titleId = useId()
101
- const descriptionId = useId()
102
-
103
- const requestOpen = useCallback(() => {
104
- if (closingTimerRef.current) {
105
- clearTimeout(closingTimerRef.current)
106
- closingTimerRef.current = null
107
- }
108
- setClosing(false)
109
- if (!isControlled) setInternalOpen(true)
110
- onOpenChange?.(true)
111
- }, [isControlled, onOpenChange])
112
-
113
- const requestClose = useCallback(() => {
114
- setClosing(true)
115
- closingTimerRef.current = setTimeout(() => {
116
- setClosing(false)
117
- closingTimerRef.current = null
118
- if (!isControlled) setInternalOpen(false)
119
- onOpenChange?.(false)
120
- }, DURATION_MS[closeAnimationDuration])
121
- }, [isControlled, onOpenChange, closeAnimationDuration])
122
-
123
- const requestCloseImmediate = useCallback(() => {
124
- if (closingTimerRef.current) {
125
- clearTimeout(closingTimerRef.current)
126
- closingTimerRef.current = null
127
- }
128
- setClosing(false)
129
- if (!isControlled) setInternalOpen(false)
130
- onOpenChange?.(false)
131
- }, [isControlled, onOpenChange])
132
-
133
- // Clean up timer on unmount
134
- useEffect(() => {
135
- return () => {
136
- if (closingTimerRef.current) clearTimeout(closingTimerRef.current)
137
- }
138
- }, [])
40
+ function Root({children, ...props}: DrawerRootProps) {
41
+ const ctx = useDrawerRoot(props)
42
+ const {open: present, dismissable, requestClose} = ctx
139
43
 
140
44
  // Handle Escape key
141
45
  useEffect(() => {
142
- if (!isOpen && !closing) return
46
+ if (!present) return
143
47
  if (!dismissable) return
144
48
 
145
49
  function onKeyDown(e: KeyboardEvent) {
@@ -150,39 +54,20 @@ function Root({
150
54
  }
151
55
  document.addEventListener('keydown', onKeyDown)
152
56
  return () => document.removeEventListener('keydown', onKeyDown)
153
- }, [isOpen, closing, dismissable, requestClose])
57
+ }, [present, dismissable, requestClose])
154
58
 
155
59
  // Lock body scroll when open
156
60
  useEffect(() => {
157
- if (isOpen || closing) {
61
+ if (present) {
158
62
  const prev = document.body.style.overflow
159
63
  document.body.style.overflow = 'hidden'
160
64
  return () => {
161
65
  document.body.style.overflow = prev
162
66
  }
163
67
  }
164
- }, [isOpen, closing])
68
+ }, [present])
165
69
 
166
- return (
167
- <DrawerContext.Provider
168
- value={{
169
- open: isOpen || closing,
170
- closing,
171
- closeDuration: closeAnimationDuration,
172
- anchor,
173
- dismissable,
174
- swipeDismiss,
175
- requestOpen,
176
- requestClose,
177
- requestCloseImmediate,
178
- titleId,
179
- descriptionId,
180
- panelRef,
181
- }}
182
- >
183
- {children}
184
- </DrawerContext.Provider>
185
- )
70
+ return <DrawerContext.Provider value={ctx}>{children}</DrawerContext.Provider>
186
71
  }
187
72
 
188
73
  // --- Trigger ---
@@ -191,7 +76,7 @@ function Trigger({children}: {children: ReactNode}) {
191
76
  const {requestOpen} = useDrawer()
192
77
 
193
78
  return (
194
- <html.div onClick={requestOpen} style={styles.inlineWrapper}>
79
+ <html.div onClick={requestOpen} style={partStyles.inlineWrapper}>
195
80
  {children}
196
81
  </html.div>
197
82
  )
@@ -202,6 +87,8 @@ function Trigger({children}: {children: ReactNode}) {
202
87
  interface PortalProps {
203
88
  children: ReactNode
204
89
  size?: DrawerSize
90
+ /** The grabber's accessible names, by what a press does next. Snap sheets only */
91
+ grabberLabels?: GrabberLabels
205
92
  }
206
93
 
207
94
  // Slide animation maps
@@ -223,7 +110,7 @@ const viewportAnchorMap = {
223
110
  bottom: styles.viewportBottom,
224
111
  } as const
225
112
 
226
- function Portal({children, size = 'md'}: PortalProps) {
113
+ function Portal({children, size = 'md', grabberLabels = DEFAULT_GRABBER_LABELS}: PortalProps) {
227
114
  const {
228
115
  open,
229
116
  closing,
@@ -236,8 +123,14 @@ function Portal({children, size = 'md'}: PortalProps) {
236
123
  titleId,
237
124
  descriptionId,
238
125
  panelRef,
126
+ snapPoints,
127
+ snapIndex,
128
+ setSnapIndex,
239
129
  } = useDrawer()
240
130
  const mount = usePortalMount()
131
+ const snapping = snapPoints.length > 0
132
+ const heights = useSheetHeights(snapPoints, snapping && open)
133
+ const sized = snapping && heights.length === snapPoints.length
241
134
  // The shown panel element, tracked as state so swipe listeners follow it
242
135
  // when the portal mount arrives and the panel is remounted into it.
243
136
  const [panel, setPanel] = useState<HTMLDivElement | null>(null)
@@ -261,6 +154,7 @@ function Portal({children, size = 'md'}: PortalProps) {
261
154
  enabled: swipeEnabled && open && !closing,
262
155
  onDismiss: requestCloseImmediate,
263
156
  panel,
157
+ snap: sized ? {heights, index: snapIndex, onSnap: setSnapIndex} : null,
264
158
  })
265
159
 
266
160
  if (!open) return null
@@ -294,12 +188,29 @@ function Portal({children, size = 'md'}: PortalProps) {
294
188
  isHorizontal && styles.panelHorizontal,
295
189
  isHorizontal && styles[size],
296
190
  !isHorizontal && styles.panelVertical,
191
+ sized && styles.panelSnap,
192
+ sized && styles.sheetHeight(heights[snapIndex]),
297
193
  anchor === 'left' && styles.panelLeftBorder,
298
194
  closing && slideOutMap[anchor],
299
195
  closing && closeDurationMap[closeDuration],
300
196
  !closing && slideInMap[anchor],
301
197
  ]}
302
198
  >
199
+ {snapping && (
200
+ <html.button
201
+ type="button"
202
+ data-drawer-grabber=""
203
+ aria-label={
204
+ cycleDetent(snapIndex, snapPoints.length) > snapIndex
205
+ ? grabberLabels.expand
206
+ : grabberLabels.collapse
207
+ }
208
+ onClick={() => setSnapIndex(cycleDetent(snapIndex, snapPoints.length))}
209
+ style={[partStyles.grabberHit, styles.grabberHitWeb]}
210
+ >
211
+ <html.span style={partStyles.grabber} />
212
+ </html.button>
213
+ )}
303
214
  <ModalContext.Provider value={true}>
304
215
  <ControlContextBoundary>{children}</ControlContextBoundary>
305
216
  </ModalContext.Provider>
@@ -316,46 +227,19 @@ function Portal({children, size = 'md'}: PortalProps) {
316
227
  return mount ? createPortal(node, mount) : node
317
228
  }
318
229
 
319
- // --- Header ---
320
-
321
- function Header({children}: {children: ReactNode}) {
322
- return <html.div style={styles.header}>{children}</html.div>
323
- }
324
-
325
- // --- Title ---
326
-
327
- function Title({children}: {children: ReactNode}) {
328
- const {titleId} = useDrawer()
329
- return (
330
- <html.h2 id={titleId} style={styles.title}>
331
- {children}
332
- </html.h2>
333
- )
334
- }
335
-
336
- // --- Description ---
230
+ // --- Body ---
337
231
 
338
- function Description({children}: {children: ReactNode}) {
339
- const {descriptionId} = useDrawer()
232
+ function Body({children}: {children: ReactNode}) {
233
+ const {snapPoints} = useDrawer()
234
+ // A snap sheet's Body is its scroll viewport: a drag there scrolls it, and
235
+ // moves the sheet only from the top edge (or upward, below the largest detent).
340
236
  return (
341
- <html.p id={descriptionId} style={styles.description}>
237
+ <html.div style={styles.body} data-duro-scroll={snapPoints.length > 0 ? '' : undefined}>
342
238
  {children}
343
- </html.p>
239
+ </html.div>
344
240
  )
345
241
  }
346
242
 
347
- // --- Body ---
348
-
349
- function Body({children}: {children: ReactNode}) {
350
- return <html.div style={styles.body}>{children}</html.div>
351
- }
352
-
353
- // --- Footer ---
354
-
355
- function Footer({children}: {children: ReactNode}) {
356
- return <html.div style={styles.footer}>{children}</html.div>
357
- }
358
-
359
243
  // --- Close ---
360
244
 
361
245
  interface CloseProps {
@@ -368,14 +252,14 @@ function Close({children, 'aria-label': ariaLabel = 'Close'}: CloseProps) {
368
252
 
369
253
  if (children) {
370
254
  return (
371
- <html.div onClick={requestClose} style={styles.inlineWrapper}>
255
+ <html.div onClick={requestClose} style={partStyles.inlineWrapper}>
372
256
  {children}
373
257
  </html.div>
374
258
  )
375
259
  }
376
260
 
377
261
  return (
378
- <html.button onClick={requestClose} aria-label={ariaLabel} style={styles.closeButton}>
262
+ <html.button onClick={requestClose} aria-label={ariaLabel} style={partStyles.closeButton}>
379
263
  <svg width={16} height={16} viewBox="0 0 16 16" fill="none">
380
264
  <path
381
265
  d="M4 4l8 8M12 4l-8 8"
@@ -1,11 +1,11 @@
1
1
  import type {Meta, StoryObj} from '@storybook/react'
2
2
  import {expect, waitFor} from 'storybook/test'
3
- import {cdp, page} from 'vitest/browser'
4
3
  import {Drawer} from './Drawer'
5
4
  import {ToggleGroup} from '../ToggleGroup/ToggleGroup'
6
5
  import {Toggle} from '../Toggle/Toggle'
7
6
  import {Icon, type IconName} from '../Icon'
8
7
  import {SIZES_PX} from '@duro-app/tokens/keys'
8
+ import {cdpSend, pause, setViewport, toPage, touchDrag, type Point} from './touchInput'
9
9
 
10
10
  const meta: Meta = {
11
11
  title: 'Components/Drawer/Scroll',
@@ -87,56 +87,6 @@ function Scene({pressed}: {pressed?: string} = {}) {
87
87
  )
88
88
  }
89
89
 
90
- type Point = {x: number; y: number}
91
-
92
- /** `CDPSession` is empty until the provider's typings load; `send` exists at runtime. */
93
- const cdpSend = (method: string, params: object) =>
94
- (cdp() as unknown as {send: (method: string, params: object) => Promise<unknown>}).send(
95
- method,
96
- params,
97
- )
98
-
99
- const pause = (ms: number) => new Promise((r) => setTimeout(r, ms))
100
-
101
- /**
102
- * CDP input lands in top-level page coordinates; the story runs in an iframe
103
- * the test runner offsets and may scale to fit. Map a point up every frame.
104
- */
105
- function toPage(p: Point): Point {
106
- let {x, y} = p
107
- let win: Window = window
108
- while (win.frameElement && win.parent !== win) {
109
- const rect = win.frameElement.getBoundingClientRect()
110
- const scale = rect.width / win.innerWidth
111
- x = rect.left + x * scale
112
- y = rect.top + y * scale
113
- win = win.parent
114
- }
115
- return {x, y}
116
- }
117
-
118
- /**
119
- * A page-coordinate touch point with a 1px radius: Chrome's touch adjustment
120
- * snaps a fat default touch onto the nearest control, which would turn every
121
- * drag in the list into a drag on a toggle.
122
- */
123
- const touchAt = (p: Point) => ({...toPage(p), radiusX: 1, radiusY: 1})
124
-
125
- /** Real touch drag through CDP; synthetic pointer events don't scroll. */
126
- async function touchDrag(from: Point, to: Point) {
127
- const steps = 12
128
- const send = (type: string, touchPoints: Point[]) =>
129
- cdpSend('Input.dispatchTouchEvent', {type, touchPoints: touchPoints.map(touchAt)})
130
- await send('touchStart', [from])
131
- for (let i = 1; i <= steps; i++) {
132
- await send('touchMove', [
133
- {x: from.x + ((to.x - from.x) * i) / steps, y: from.y + ((to.y - from.y) * i) / steps},
134
- ])
135
- await pause(16)
136
- }
137
- await send('touchEnd', [])
138
- }
139
-
140
90
  const getViewport = () => document.querySelector<HTMLElement>('[data-duro-scroll]')!
141
91
  const getDialog = () => document.querySelector('[role="dialog"]')
142
92
 
@@ -158,7 +108,7 @@ function freePoint(el: HTMLElement, fy: number): Point {
158
108
  }
159
109
 
160
110
  async function setup() {
161
- await page.viewport(390, 844)
111
+ await setViewport(390, 844)
162
112
  await cdpSend('Emulation.setTouchEmulationEnabled', {enabled: true, maxTouchPoints: 1})
163
113
  await waitFor(() => expect(getViewport()).toBeTruthy())
164
114
  const viewport = getViewport()
@@ -242,7 +192,7 @@ export const TapOnToggleStillPresses: Story = {
242
192
  export const MountKeepsTheDrawerStill: Story = {
243
193
  // size the page before the render: a drawer mounts at its final width
244
194
  beforeEach: async () => {
245
- await page.viewport(390, 844)
195
+ await setViewport(390, 844)
246
196
  },
247
197
  render: () => <Scene pressed={ICON_NAMES[39]} />,
248
198
  play: async () => {
@@ -278,7 +228,7 @@ export const CoarsePointerRows: Story = {
278
228
  ),
279
229
  play: async () => {
280
230
  // phone width, so 42 icons overflow three rows
281
- await page.viewport(390, 844)
231
+ await setViewport(390, 844)
282
232
  await cdpSend('Emulation.setTouchEmulationEnabled', {enabled: true, maxTouchPoints: 1})
283
233
  await waitFor(() => expect(matchMedia('(pointer: coarse)').matches).toBe(true))
284
234
  const viewport = getViewport()
@@ -0,0 +1,205 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {expect, fn, userEvent, waitFor} from 'storybook/test'
3
+ import {Drawer, type SnapPoint} from './Drawer'
4
+ import {Button} from '../Button/Button'
5
+ import {List} from '../List/List'
6
+ import {cdpSend, centre, pause, setViewport, touchDrag, type Point} from './touchInput'
7
+
8
+ const meta: Meta = {
9
+ title: 'Components/Drawer/Snap points',
10
+ parameters: {layout: 'fullscreen'},
11
+ }
12
+
13
+ export default meta
14
+ type Story = StoryObj
15
+
16
+ const SESSIONS = Array.from({length: 40}, (_, i) => `Session ${i + 1}`)
17
+
18
+ interface SheetArgs {
19
+ snapPoints: SnapPoint[]
20
+ defaultSnapPoint: SnapPoint
21
+ titleAlign: 'start' | 'center'
22
+ onOpenChange?: (open: boolean) => void
23
+ }
24
+
25
+ const DEFAULT_ARGS: SheetArgs = {
26
+ snapPoints: ['medium', 'large'],
27
+ defaultSnapPoint: 'medium',
28
+ titleAlign: 'center',
29
+ }
30
+
31
+ /** The picked B0 direction (docs/mockups/native-sheet/Done.dc.html): grabber, centred title, a text Done. */
32
+ function Sheet(props: Partial<SheetArgs>) {
33
+ const {snapPoints, defaultSnapPoint, titleAlign, onOpenChange} = {...DEFAULT_ARGS, ...props}
34
+ return (
35
+ <Drawer.Root
36
+ anchor="bottom"
37
+ snapPoints={snapPoints}
38
+ defaultSnapPoint={defaultSnapPoint}
39
+ onOpenChange={onOpenChange}
40
+ defaultOpen
41
+ >
42
+ <Drawer.Portal>
43
+ <Drawer.Header titleAlign={titleAlign}>
44
+ <Drawer.Title>Move to project</Drawer.Title>
45
+ <Drawer.Close>
46
+ <Button variant="link">Done</Button>
47
+ </Drawer.Close>
48
+ </Drawer.Header>
49
+ <Drawer.Body>
50
+ <List.Root aria-label="Sessions">
51
+ {SESSIONS.map((s) => (
52
+ <List.Item key={s}>
53
+ <List.Text>{s}</List.Text>
54
+ </List.Item>
55
+ ))}
56
+ </List.Root>
57
+ </Drawer.Body>
58
+ </Drawer.Portal>
59
+ </Drawer.Root>
60
+ )
61
+ }
62
+
63
+ export const SnapSheet: StoryObj<SheetArgs> = {
64
+ args: {...DEFAULT_ARGS, onOpenChange: fn()},
65
+ argTypes: {
66
+ snapPoints: {
67
+ control: 'check',
68
+ options: ['medium', 'large'],
69
+ description: "The detents: 'medium' half the window, 'large' the window less its top inset",
70
+ },
71
+ defaultSnapPoint: {
72
+ control: 'inline-radio',
73
+ options: ['medium', 'large'],
74
+ description: 'The detent the sheet opens at',
75
+ },
76
+ titleAlign: {
77
+ control: 'inline-radio',
78
+ options: ['start', 'center'],
79
+ description: 'Drawer.Header: the title at the start, or centred with the close at the end',
80
+ },
81
+ },
82
+ // Keyed by the opening detent: it seeds state on mount, so a change remounts the sheet.
83
+ render: (args) => (
84
+ <Sheet
85
+ key={args.defaultSnapPoint}
86
+ {...args}
87
+ snapPoints={args.snapPoints.length > 0 ? args.snapPoints : ['medium']}
88
+ />
89
+ ),
90
+ }
91
+
92
+ const getDialog = () => document.querySelector<HTMLElement>('[role="dialog"]')
93
+ const getGrabber = () => document.querySelector<HTMLElement>('[data-drawer-grabber]')!
94
+ const getBody = () => document.querySelector<HTMLElement>('[role="dialog"] [data-duro-scroll]')!
95
+ const sheetHeight = () => getDialog()!.getBoundingClientRect().height
96
+
97
+ const WINDOW_H = 844
98
+ const MEDIUM = WINDOW_H / 2
99
+ const LARGE = WINDOW_H // no top safe-area inset in a desktop browser
100
+
101
+ async function setup() {
102
+ await setViewport(390, WINDOW_H)
103
+ await cdpSend('Emulation.setTouchEmulationEnabled', {enabled: true, maxTouchPoints: 1})
104
+ await waitFor(() => expect(getDialog()).not.toBeNull())
105
+ // let the slide-in finish
106
+ await pause(500)
107
+ }
108
+
109
+ const near = (actual: number, expected: number) =>
110
+ expect(Math.abs(actual - expected)).toBeLessThanOrEqual(2)
111
+
112
+ async function dragGrabber(dy: number) {
113
+ const from: Point = centre(getGrabber())
114
+ await touchDrag(from, {x: from.x, y: from.y + dy})
115
+ await pause(400)
116
+ }
117
+
118
+ // One drag per story: a test that drags three times can outlast the runner's
119
+ // timeout on a loaded machine.
120
+ export const DragUpGrowsToLarge: Story = {
121
+ render: () => <Sheet />,
122
+ play: async () => {
123
+ await setup()
124
+ await waitFor(() => near(sheetHeight(), MEDIUM))
125
+ await dragGrabber(-300)
126
+ await waitFor(() => near(sheetHeight(), LARGE))
127
+ },
128
+ }
129
+
130
+ export const DragDownShrinksToMedium: Story = {
131
+ render: () => <Sheet defaultSnapPoint="large" />,
132
+ play: async () => {
133
+ await setup()
134
+ await waitFor(() => near(sheetHeight(), LARGE))
135
+ await dragGrabber(300)
136
+ await waitFor(() => near(sheetHeight(), MEDIUM))
137
+ },
138
+ }
139
+
140
+ export const DragDownBelowMediumDismisses: Story = {
141
+ render: () => <Sheet />,
142
+ play: async () => {
143
+ await setup()
144
+ await waitFor(() => near(sheetHeight(), MEDIUM))
145
+ await dragGrabber(300)
146
+ await waitFor(() => expect(getDialog()).toBeNull())
147
+ },
148
+ }
149
+
150
+ export const SlowShortDragSnapsBack: Story = {
151
+ render: () => <Sheet />,
152
+ play: async () => {
153
+ await setup()
154
+ const from = centre(getGrabber())
155
+ // 60px over 20 moves (about 0.2 px/ms): well under the fling speed, well
156
+ // short of the next detent
157
+ await touchDrag(from, {x: from.x, y: from.y - 60}, 20)
158
+ await pause(400)
159
+ await waitFor(() => near(sheetHeight(), MEDIUM))
160
+ },
161
+ }
162
+
163
+ export const GrabberTapCyclesDetents: Story = {
164
+ render: () => <Sheet />,
165
+ play: async () => {
166
+ await setup()
167
+ const grabber = getGrabber()
168
+ await expect(grabber).toHaveAccessibleName('Expand sheet')
169
+ await userEvent.click(grabber)
170
+ await waitFor(() => near(sheetHeight(), LARGE))
171
+ await expect(grabber).toHaveAccessibleName('Shrink sheet')
172
+ await userEvent.click(grabber)
173
+ await waitFor(() => near(sheetHeight(), MEDIUM))
174
+ },
175
+ }
176
+
177
+ export const DragUpOnContentGrowsThenScrolls: Story = {
178
+ render: () => <Sheet />,
179
+ play: async () => {
180
+ await setup()
181
+ const from = centre(getBody())
182
+ // At medium, the content's upward drag grows the sheet …
183
+ await touchDrag(from, {x: from.x, y: from.y - 250})
184
+ await pause(400)
185
+ await waitFor(() => near(sheetHeight(), LARGE))
186
+ await expect(getBody().scrollTop).toBe(0)
187
+ // … at the largest detent it scrolls the list.
188
+ const again = centre(getBody())
189
+ await touchDrag(again, {x: again.x, y: again.y - 120})
190
+ await pause(300)
191
+ await expect(getBody().scrollTop).toBeGreaterThan(0)
192
+ near(sheetHeight(), LARGE)
193
+ },
194
+ }
195
+
196
+ export const DoneCloses: Story = {
197
+ render: () => <Sheet />,
198
+ play: async () => {
199
+ await setup()
200
+ await userEvent.click(
201
+ document.querySelector('[role="dialog"] button:not([data-drawer-grabber])')!,
202
+ )
203
+ await waitFor(() => expect(getDialog()).toBeNull())
204
+ },
205
+ }