@remix-run/ui 0.1.2 → 0.3.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 (318) hide show
  1. package/README.md +9 -7
  2. package/dist/animation/demos/animation.demo.d.ts +5 -0
  3. package/dist/animation/demos/animation.demo.js +92 -0
  4. package/dist/animation/demos/animation.demo.js.map +1 -0
  5. package/dist/animation/demos/aspect-ratio.d.ts +2 -0
  6. package/dist/animation/demos/aspect-ratio.js +69 -0
  7. package/dist/animation/demos/aspect-ratio.js.map +1 -0
  8. package/dist/animation/demos/bouncy-switch.d.ts +2 -0
  9. package/dist/animation/demos/bouncy-switch.js +36 -0
  10. package/dist/animation/demos/bouncy-switch.js.map +1 -0
  11. package/dist/animation/demos/color-interpolation.d.ts +1 -0
  12. package/dist/animation/demos/color-interpolation.js +53 -0
  13. package/dist/animation/demos/color-interpolation.js.map +1 -0
  14. package/dist/animation/demos/cube.d.ts +2 -0
  15. package/dist/animation/demos/cube.js +86 -0
  16. package/dist/animation/demos/cube.js.map +1 -0
  17. package/dist/animation/demos/default-animate.d.ts +2 -0
  18. package/dist/animation/demos/default-animate.js +89 -0
  19. package/dist/animation/demos/default-animate.js.map +1 -0
  20. package/dist/animation/demos/drag-release.d.ts +24 -0
  21. package/dist/animation/demos/drag-release.js +96 -0
  22. package/dist/animation/demos/drag-release.js.map +1 -0
  23. package/dist/animation/demos/enter.d.ts +1 -0
  24. package/dist/animation/demos/enter.js +19 -0
  25. package/dist/animation/demos/enter.js.map +1 -0
  26. package/dist/animation/demos/exit.d.ts +2 -0
  27. package/dist/animation/demos/exit.js +58 -0
  28. package/dist/animation/demos/exit.js.map +1 -0
  29. package/dist/animation/demos/flip-toggle.d.ts +2 -0
  30. package/dist/animation/demos/flip-toggle.js +37 -0
  31. package/dist/animation/demos/flip-toggle.js.map +1 -0
  32. package/dist/animation/demos/hold-to-confirm.d.ts +13 -0
  33. package/dist/animation/demos/hold-to-confirm.js +211 -0
  34. package/dist/animation/demos/hold-to-confirm.js.map +1 -0
  35. package/dist/animation/demos/html-content.d.ts +2 -0
  36. package/dist/animation/demos/html-content.js +30 -0
  37. package/dist/animation/demos/html-content.js.map +1 -0
  38. package/dist/animation/demos/interruptible-keyframes.d.ts +1 -0
  39. package/dist/animation/demos/interruptible-keyframes.js +48 -0
  40. package/dist/animation/demos/interruptible-keyframes.js.map +1 -0
  41. package/dist/animation/demos/keyframes.d.ts +1 -0
  42. package/dist/animation/demos/keyframes.js +24 -0
  43. package/dist/animation/demos/keyframes.js.map +1 -0
  44. package/dist/animation/demos/material-ripple.d.ts +2 -0
  45. package/dist/animation/demos/material-ripple.js +127 -0
  46. package/dist/animation/demos/material-ripple.js.map +1 -0
  47. package/dist/animation/demos/mixin-presence-list.d.ts +2 -0
  48. package/dist/animation/demos/mixin-presence-list.js +88 -0
  49. package/dist/animation/demos/mixin-presence-list.js.map +1 -0
  50. package/dist/animation/demos/mixin-reclaim.d.ts +2 -0
  51. package/dist/animation/demos/mixin-reclaim.js +90 -0
  52. package/dist/animation/demos/mixin-reclaim.js.map +1 -0
  53. package/dist/animation/demos/multi-state-badge.d.ts +2 -0
  54. package/dist/animation/demos/multi-state-badge.js +231 -0
  55. package/dist/animation/demos/multi-state-badge.js.map +1 -0
  56. package/dist/animation/demos/press.d.ts +2 -0
  57. package/dist/animation/demos/press.js +63 -0
  58. package/dist/animation/demos/press.js.map +1 -0
  59. package/dist/animation/demos/reordering.d.ts +2 -0
  60. package/dist/animation/demos/reordering.js +50 -0
  61. package/dist/animation/demos/reordering.js.map +1 -0
  62. package/dist/animation/demos/rolling-square.d.ts +2 -0
  63. package/dist/animation/demos/rolling-square.js +42 -0
  64. package/dist/animation/demos/rolling-square.js.map +1 -0
  65. package/dist/animation/demos/rotate.d.ts +1 -0
  66. package/dist/animation/demos/rotate.js +18 -0
  67. package/dist/animation/demos/rotate.js.map +1 -0
  68. package/dist/animation/demos/shared-layout.d.ts +2 -0
  69. package/dist/animation/demos/shared-layout.js +114 -0
  70. package/dist/animation/demos/shared-layout.js.map +1 -0
  71. package/dist/animation/demos/spring.demo.d.ts +6 -0
  72. package/dist/animation/demos/spring.demo.js +304 -0
  73. package/dist/animation/demos/spring.demo.js.map +1 -0
  74. package/dist/animation/demos/transition-options.d.ts +1 -0
  75. package/dist/animation/demos/transition-options.js +21 -0
  76. package/dist/animation/demos/transition-options.js.map +1 -0
  77. package/dist/animation/spring.d.ts +24 -31
  78. package/dist/animation/spring.js +31 -8
  79. package/dist/animation/spring.js.map +1 -1
  80. package/dist/components/accordion/accordion.d.ts +6 -6
  81. package/dist/components/accordion/accordion.js.map +1 -1
  82. package/dist/components/accordion/demos/card.demo.d.ts +1 -0
  83. package/dist/components/accordion/demos/card.demo.js +1 -0
  84. package/dist/components/accordion/demos/card.demo.js.map +1 -1
  85. package/dist/components/accordion/demos/multiple.demo.d.ts +1 -0
  86. package/dist/components/accordion/demos/multiple.demo.js +1 -0
  87. package/dist/components/accordion/demos/multiple.demo.js.map +1 -1
  88. package/dist/components/accordion/demos/overview.demo.d.ts +1 -0
  89. package/dist/components/accordion/demos/overview.demo.js +1 -0
  90. package/dist/components/accordion/demos/overview.demo.js.map +1 -1
  91. package/dist/components/anchor/anchor.d.ts +8 -1
  92. package/dist/components/anchor/anchor.js +30 -11
  93. package/dist/components/anchor/anchor.js.map +1 -1
  94. package/dist/components/breadcrumbs/{demos/basic.demo.d.ts → basic.demo.d.ts} +1 -0
  95. package/dist/components/breadcrumbs/{demos/basic.demo.js → basic.demo.js} +1 -0
  96. package/dist/components/breadcrumbs/basic.demo.js.map +1 -0
  97. package/dist/components/breadcrumbs/breadcrumbs.d.ts +1 -1
  98. package/dist/components/breadcrumbs/{demos/separator.demo.d.ts → separator.demo.d.ts} +1 -0
  99. package/dist/components/breadcrumbs/{demos/separator.demo.js → separator.demo.js} +1 -0
  100. package/dist/components/breadcrumbs/separator.demo.js.map +1 -0
  101. package/dist/components/button/button.d.ts +4 -4
  102. package/dist/components/button/button.js +3 -1
  103. package/dist/components/button/button.js.map +1 -1
  104. package/dist/components/button/demos/aliases.demo.d.ts +1 -0
  105. package/dist/components/button/demos/aliases.demo.js +1 -0
  106. package/dist/components/button/demos/aliases.demo.js.map +1 -1
  107. package/dist/components/button/demos/basic.demo.d.ts +1 -0
  108. package/dist/components/button/demos/basic.demo.js +1 -0
  109. package/dist/components/button/demos/basic.demo.js.map +1 -1
  110. package/dist/components/button/demos/states.demo.d.ts +1 -0
  111. package/dist/components/button/demos/states.demo.js +1 -0
  112. package/dist/components/button/demos/states.demo.js.map +1 -1
  113. package/dist/components/combobox/combobox.d.ts +10 -10
  114. package/dist/components/combobox/combobox.js.map +1 -1
  115. package/dist/components/combobox/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
  116. package/dist/components/combobox/{demos/overview.demo.js → overview.demo.js} +1 -0
  117. package/dist/components/combobox/overview.demo.js.map +1 -0
  118. package/dist/components/listbox/listbox.d.ts +3 -3
  119. package/dist/components/listbox/listbox.js.map +1 -1
  120. package/dist/components/listbox/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
  121. package/dist/components/listbox/{demos/overview.demo.js → overview.demo.js} +1 -0
  122. package/dist/components/listbox/overview.demo.js.map +1 -0
  123. package/dist/components/menu/demos/bubbling.demo.d.ts +1 -0
  124. package/dist/components/menu/demos/bubbling.demo.js +1 -0
  125. package/dist/components/menu/demos/bubbling.demo.js.map +1 -1
  126. package/dist/components/menu/demos/context-trigger.demo.d.ts +7 -0
  127. package/dist/components/menu/demos/context-trigger.demo.js +87 -0
  128. package/dist/components/menu/demos/context-trigger.demo.js.map +1 -0
  129. package/dist/components/menu/demos/overview.demo.d.ts +1 -0
  130. package/dist/components/menu/demos/overview.demo.js +1 -0
  131. package/dist/components/menu/demos/overview.demo.js.map +1 -1
  132. package/dist/components/menu/menu.d.ts +15 -12
  133. package/dist/components/menu/menu.js +55 -0
  134. package/dist/components/menu/menu.js.map +1 -1
  135. package/dist/components/popover/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
  136. package/dist/components/popover/{demos/overview.demo.js → overview.demo.js} +1 -0
  137. package/dist/components/popover/overview.demo.js.map +1 -0
  138. package/dist/components/popover/popover.d.ts +7 -7
  139. package/dist/components/popover/popover.js +11 -4
  140. package/dist/components/popover/popover.js.map +1 -1
  141. package/dist/components/select/{demos/deconstructed.demo.d.ts → deconstructed.demo.d.ts} +1 -0
  142. package/dist/components/select/{demos/deconstructed.demo.js → deconstructed.demo.js} +1 -0
  143. package/dist/components/select/deconstructed.demo.js.map +1 -0
  144. package/dist/components/select/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
  145. package/dist/components/select/{demos/overview.demo.js → overview.demo.js} +1 -0
  146. package/dist/components/select/overview.demo.js.map +1 -0
  147. package/dist/components/select/select.d.ts +10 -10
  148. package/dist/components/select/select.js +1 -1
  149. package/dist/components/select/select.js.map +1 -1
  150. package/dist/components/tabs/tabs.d.ts +3 -3
  151. package/dist/index.d.ts +1 -1
  152. package/dist/index.js.map +1 -1
  153. package/dist/interactions/keydown/keydown.d.ts +1 -1
  154. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
  155. package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
  156. package/dist/runtime/client-entries.d.ts +1 -2
  157. package/dist/runtime/client-entries.js.map +1 -1
  158. package/dist/runtime/component.d.ts +20 -17
  159. package/dist/runtime/component.js +15 -6
  160. package/dist/runtime/component.js.map +1 -1
  161. package/dist/runtime/core/attributes.d.ts +3 -1
  162. package/dist/runtime/core/attributes.js +19 -1
  163. package/dist/runtime/core/attributes.js.map +1 -1
  164. package/dist/runtime/core/props.js +6 -5
  165. package/dist/runtime/core/props.js.map +1 -1
  166. package/dist/runtime/demos/basic.demo.d.ts +6 -0
  167. package/dist/runtime/demos/basic.demo.js +16 -0
  168. package/dist/runtime/demos/basic.demo.js.map +1 -0
  169. package/dist/runtime/demos/controlled-uncontrolled-values.demo.d.ts +6 -0
  170. package/dist/runtime/demos/controlled-uncontrolled-values.demo.js +77 -0
  171. package/dist/runtime/demos/controlled-uncontrolled-values.demo.js.map +1 -0
  172. package/dist/runtime/demos/keyed-list.demo.d.ts +6 -0
  173. package/dist/runtime/demos/keyed-list.demo.js +62 -0
  174. package/dist/runtime/demos/keyed-list.demo.js.map +1 -0
  175. package/dist/runtime/demos/readme.demo.d.ts +5 -0
  176. package/dist/runtime/demos/readme.demo.js +396 -0
  177. package/dist/runtime/demos/readme.demo.js.map +1 -0
  178. package/dist/runtime/diff-dom.js +194 -21
  179. package/dist/runtime/diff-dom.js.map +1 -1
  180. package/dist/runtime/dom.d.ts +16 -9
  181. package/dist/runtime/frame.d.ts +7 -0
  182. package/dist/runtime/frame.js +176 -41
  183. package/dist/runtime/frame.js.map +1 -1
  184. package/dist/runtime/jsx.d.ts +3 -4
  185. package/dist/runtime/jsx.js.map +1 -1
  186. package/dist/runtime/mixins/link-mixin.d.ts +5 -1
  187. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  188. package/dist/runtime/mixins/mixin.d.ts +2 -1
  189. package/dist/runtime/mixins/mixin.js.map +1 -1
  190. package/dist/runtime/mixins/ref-mixin.d.ts +2 -1
  191. package/dist/runtime/mixins/ref-mixin.js.map +1 -1
  192. package/dist/runtime/reconcile.d.ts +1 -1
  193. package/dist/runtime/reconcile.js +19 -5
  194. package/dist/runtime/reconcile.js.map +1 -1
  195. package/dist/runtime/scheduler.d.ts +10 -10
  196. package/dist/runtime/scheduler.js +1 -1
  197. package/dist/runtime/scheduler.js.map +1 -1
  198. package/dist/runtime/svg-attributes.js +1 -0
  199. package/dist/runtime/svg-attributes.js.map +1 -1
  200. package/dist/runtime/to-vnode.js +2 -2
  201. package/dist/runtime/to-vnode.js.map +1 -1
  202. package/dist/runtime/vnode.d.ts +6 -1
  203. package/dist/runtime/vnode.js +4 -0
  204. package/dist/runtime/vnode.js.map +1 -1
  205. package/dist/server/stream.d.ts +2 -0
  206. package/dist/server/stream.js +82 -11
  207. package/dist/server/stream.js.map +1 -1
  208. package/dist/style/css-mixin.d.ts +2 -1
  209. package/dist/style/css-mixin.js.map +1 -1
  210. package/dist/style/index.d.ts +1 -1
  211. package/dist/style/style.js +27 -10
  212. package/dist/style/style.js.map +1 -1
  213. package/dist/style/stylesheet.d.ts +13 -13
  214. package/dist/style/stylesheet.js.map +1 -1
  215. package/dist/theme/contract.d.ts +24 -222
  216. package/dist/theme/contract.js +2 -2
  217. package/dist/theme/contract.js.map +1 -1
  218. package/dist/theme/glyph-contract.d.ts +1 -3
  219. package/dist/theme/glyph-contract.js.map +1 -1
  220. package/dist/theme/presets/rmx-01/index.d.ts +4 -2
  221. package/dist/theme/presets/rmx-01/index.js.map +1 -1
  222. package/dist/utils/scroll-lock.d.ts +3 -3
  223. package/dist/utils/scroll-lock.js.map +1 -1
  224. package/package.json +4 -4
  225. package/src/animation/README.md +7 -5
  226. package/src/animation/demos/animation.demo.tsx +194 -0
  227. package/src/animation/demos/aspect-ratio.tsx +120 -0
  228. package/src/animation/demos/bouncy-switch.tsx +47 -0
  229. package/src/animation/demos/color-interpolation.tsx +73 -0
  230. package/src/animation/demos/cube.tsx +116 -0
  231. package/src/animation/demos/default-animate.tsx +129 -0
  232. package/src/animation/demos/drag-release.ts +118 -0
  233. package/src/animation/demos/enter.tsx +22 -0
  234. package/src/animation/demos/exit.tsx +79 -0
  235. package/src/animation/demos/flip-toggle.tsx +46 -0
  236. package/src/animation/demos/hold-to-confirm.tsx +317 -0
  237. package/src/animation/demos/html-content.tsx +41 -0
  238. package/src/animation/demos/interruptible-keyframes.tsx +62 -0
  239. package/src/animation/demos/keyframes.tsx +29 -0
  240. package/src/animation/demos/material-ripple.tsx +158 -0
  241. package/src/animation/demos/mixin-presence-list.tsx +121 -0
  242. package/src/animation/demos/mixin-reclaim.tsx +125 -0
  243. package/src/animation/demos/multi-state-badge.tsx +426 -0
  244. package/src/animation/demos/press.tsx +67 -0
  245. package/src/animation/demos/reordering.tsx +65 -0
  246. package/src/animation/demos/rolling-square.tsx +57 -0
  247. package/src/animation/demos/rotate.tsx +21 -0
  248. package/src/animation/demos/shared-layout.tsx +211 -0
  249. package/src/animation/demos/spring.demo.tsx +385 -0
  250. package/src/animation/demos/transition-options.tsx +24 -0
  251. package/src/animation/spring.ts +43 -9
  252. package/src/components/accordion/accordion.tsx +7 -5
  253. package/src/components/accordion/demos/card.demo.tsx +1 -0
  254. package/src/components/accordion/demos/multiple.demo.tsx +1 -0
  255. package/src/components/accordion/demos/overview.demo.tsx +1 -0
  256. package/src/components/anchor/README.md +11 -3
  257. package/src/components/anchor/anchor.ts +54 -13
  258. package/src/components/breadcrumbs/{demos/basic.demo.tsx → basic.demo.tsx} +1 -0
  259. package/src/components/breadcrumbs/breadcrumbs.tsx +1 -1
  260. package/src/components/breadcrumbs/{demos/separator.demo.tsx → separator.demo.tsx} +1 -0
  261. package/src/components/button/button.tsx +30 -18
  262. package/src/components/button/demos/aliases.demo.tsx +1 -0
  263. package/src/components/button/demos/basic.demo.tsx +1 -0
  264. package/src/components/button/demos/states.demo.tsx +1 -0
  265. package/src/components/combobox/README.md +1 -1
  266. package/src/components/combobox/combobox.tsx +30 -12
  267. package/src/components/combobox/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
  268. package/src/components/glyph/glyph.tsx +2 -2
  269. package/src/components/listbox/listbox.ts +88 -83
  270. package/src/components/listbox/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
  271. package/src/components/menu/README.md +26 -2
  272. package/src/components/menu/demos/bubbling.demo.tsx +1 -0
  273. package/src/components/menu/demos/context-trigger.demo.tsx +133 -0
  274. package/src/components/menu/demos/overview.demo.tsx +1 -0
  275. package/src/components/menu/menu.tsx +100 -16
  276. package/src/components/popover/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
  277. package/src/components/popover/popover.ts +101 -81
  278. package/src/components/select/{demos/deconstructed.demo.tsx → deconstructed.demo.tsx} +1 -0
  279. package/src/components/select/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
  280. package/src/components/select/select.tsx +27 -10
  281. package/src/index.ts +1 -0
  282. package/src/runtime/client-entries.ts +1 -5
  283. package/src/runtime/component.ts +37 -15
  284. package/src/runtime/core/attributes.ts +21 -0
  285. package/src/runtime/core/props.ts +6 -4
  286. package/src/runtime/demos/basic.demo.tsx +21 -0
  287. package/src/runtime/demos/controlled-uncontrolled-values.demo.tsx +177 -0
  288. package/src/runtime/demos/keyed-list.demo.tsx +94 -0
  289. package/src/runtime/demos/readme.demo.tsx +702 -0
  290. package/src/runtime/diff-dom.ts +270 -20
  291. package/src/runtime/dom.ts +16 -9
  292. package/src/runtime/frame.ts +218 -41
  293. package/src/runtime/jsx.ts +7 -11
  294. package/src/runtime/mixins/link-mixin.ts +88 -86
  295. package/src/runtime/mixins/mixin.ts +9 -1
  296. package/src/runtime/mixins/ref-mixin.ts +4 -5
  297. package/src/runtime/reconcile.ts +23 -14
  298. package/src/runtime/scheduler.ts +20 -20
  299. package/src/runtime/svg-attributes.ts +1 -0
  300. package/src/runtime/to-vnode.ts +2 -2
  301. package/src/runtime/vnode.ts +10 -0
  302. package/src/server/README.md +4 -2
  303. package/src/server/stream.ts +84 -10
  304. package/src/style/css-mixin.ts +6 -1
  305. package/src/style/index.ts +1 -2
  306. package/src/style/style.ts +29 -10
  307. package/src/style/stylesheet.ts +14 -2
  308. package/src/theme/contract.ts +37 -5
  309. package/src/theme/glyph-contract.ts +3 -1
  310. package/src/theme/presets/rmx-01/index.ts +4 -2
  311. package/src/utils/scroll-lock.ts +7 -3
  312. package/dist/components/breadcrumbs/demos/basic.demo.js.map +0 -1
  313. package/dist/components/breadcrumbs/demos/separator.demo.js.map +0 -1
  314. package/dist/components/combobox/demos/overview.demo.js.map +0 -1
  315. package/dist/components/listbox/demos/overview.demo.js.map +0 -1
  316. package/dist/components/popover/demos/overview.demo.js.map +0 -1
  317. package/dist/components/select/demos/deconstructed.demo.js.map +0 -1
  318. package/dist/components/select/demos/overview.demo.js.map +0 -1
@@ -4,10 +4,16 @@ import {
4
4
  css,
5
5
  on,
6
6
  type CSSMixinDescriptor,
7
+ type ElementProps,
7
8
  type Handle,
9
+ type MixinFactory,
8
10
  type RemixNode,
9
11
  } from '@remix-run/ui'
10
- import { anchor as positionAnchor, type AnchorOptions } from '../anchor/anchor.ts'
12
+ import {
13
+ anchor as positionAnchor,
14
+ type AnchorOptions,
15
+ type AnchorTarget,
16
+ } from '../anchor/anchor.ts'
11
17
  import { onOutsideClick } from '../../interactions/outside-click/outside-click-mixin.ts'
12
18
  import { theme } from '../../theme/theme.ts'
13
19
  import { lockScroll } from '../../utils/scroll-lock.ts'
@@ -24,7 +30,7 @@ export interface PopoverProps {
24
30
  }
25
31
 
26
32
  interface AnchorRef {
27
- node: HTMLElement
33
+ target: AnchorTarget
28
34
  options: AnchorOptions
29
35
  }
30
36
 
@@ -75,7 +81,7 @@ const popoverSurfaceCss: CSSMixinDescriptor = css({
75
81
  export const contentStyle = popoverContentCss
76
82
  export const surfaceStyle = [popoverSurfaceCss, popoverSurfaceTransitionCss] as const
77
83
 
78
- function PopoverProvider(handle: Handle<PopoverProps, PopoverContext>) {
84
+ function PopoverProvider(handle: Handle<PopoverProps, PopoverContext>): () => RemixNode {
79
85
  handle.context.set({
80
86
  hideFocusTarget: null,
81
87
  showFocusTarget: null,
@@ -99,100 +105,114 @@ export interface PopoverHideRequest {
99
105
  target?: Node | null
100
106
  }
101
107
 
102
- const anchorMixin = createMixin<HTMLElement, [options: AnchorOptions]>((handle) => {
108
+ const anchorMixin: MixinFactory<HTMLElement, [options: AnchorOptions], ElementProps> = createMixin<
109
+ HTMLElement,
110
+ [options: AnchorOptions]
111
+ >((handle) => {
103
112
  let context = handle.context.get(PopoverProvider)
104
113
  return (options) => {
105
114
  handle.queueTask((node) => {
106
- context.anchor = { node, options }
115
+ context.anchor = { target: node, options }
107
116
  })
108
117
  }
109
118
  })
110
119
 
111
- const surfaceMixin = createMixin<HTMLElement, [options: PopoverSurfaceOptions]>((handle) => {
112
- let openProp = false
113
- let cleanupAnchor = () => {}
114
- let unlockScroll = () => {}
115
- let context = handle.context.get(PopoverProvider)
116
-
117
- handle.queueTask((node, signal) => {
118
- context.surface = node
120
+ function anchorContains(anchor: AnchorRef | null, target: Node) {
121
+ return anchor?.target instanceof HTMLElement && anchor.target.contains(target)
122
+ }
119
123
 
120
- signal.addEventListener('abort', () => {
121
- if (context.surface === node) {
122
- context.surface = null
123
- }
124
- })
125
- })
124
+ const surfaceMixin: MixinFactory<HTMLElement, [options: PopoverSurfaceOptions], ElementProps> =
125
+ createMixin<HTMLElement, [options: PopoverSurfaceOptions]>((handle) => {
126
+ let openProp = false
127
+ let cleanupAnchor = () => {}
128
+ let unlockScroll = () => {}
129
+ let context = handle.context.get(PopoverProvider)
126
130
 
127
- return (options) => {
128
- let wasOpen = openProp
129
- openProp = options.open
130
-
131
- handle.queueTask(async (node) => {
132
- if (openProp && !wasOpen) {
133
- node.showPopover()
134
- } else if (!openProp && wasOpen) {
135
- node.hidePopover()
136
- }
137
- })
131
+ handle.queueTask((node, signal) => {
132
+ context.surface = node
138
133
 
139
- return [
140
- attrs({ popover: 'manual' }),
141
-
142
- on('beforetoggle', (event) => {
143
- if (event.newState === 'open') {
144
- cleanupAnchor = positionAnchor(
145
- event.currentTarget,
146
- context.anchor!.node,
147
- context.anchor!.options,
148
- )
149
- unlockScroll = lockScroll()
150
- } else if (event.newState === 'closed') {
151
- cleanupAnchor()
152
- unlockScroll()
134
+ signal.addEventListener('abort', () => {
135
+ if (context.surface === node) {
136
+ context.surface = null
153
137
  }
154
- }),
138
+ })
139
+ })
155
140
 
156
- on('toggle', async (event) => {
157
- if (event.newState === 'open') {
158
- context.showFocusTarget?.focus()
159
- } else if (event.newState === 'closed' && options.restoreFocusOnHide !== false) {
160
- context.hideFocusTarget?.focus()
161
- }
162
- }),
141
+ return (options) => {
142
+ let wasOpen = openProp
143
+ openProp = options.open
163
144
 
164
- on('keydown', (event) => {
165
- if (event.key === 'Escape') {
166
- event.preventDefault()
167
- options.onHide({ reason: 'escape-key' })
145
+ handle.queueTask(async (node) => {
146
+ if (openProp && !wasOpen) {
147
+ node.showPopover()
148
+ } else if (!openProp && wasOpen) {
149
+ node.hidePopover()
168
150
  }
169
- }),
170
-
171
- onOutsideClick(
172
- openProp,
173
- (target) => {
174
- options.onHide({ reason: 'outside-click', target })
175
- },
176
- (target) => options.closeOnAnchorClick === false && !!context.anchor?.node.contains(target),
177
- options.stopOutsideClickPropagation ?? true,
178
- ),
179
- ]
180
- }
181
- })
182
-
183
- const focusOnShowMixin = createMixin<HTMLElement, []>((handle) => {
184
- handle.addEventListener('insert', (event) => {
185
- let context = handle.context.get(PopoverProvider)
186
- context.showFocusTarget = event.node
151
+ })
152
+
153
+ return [
154
+ attrs({ popover: 'manual' }),
155
+
156
+ on('beforetoggle', (event) => {
157
+ if (event.newState === 'open') {
158
+ let anchor = context.anchor
159
+ if (!anchor) {
160
+ throw new Error('popover.surface() requires a registered anchor before opening')
161
+ }
162
+
163
+ cleanupAnchor = positionAnchor(event.currentTarget, anchor.target, anchor.options)
164
+ unlockScroll = lockScroll()
165
+ } else if (event.newState === 'closed') {
166
+ cleanupAnchor()
167
+ unlockScroll()
168
+ }
169
+ }),
170
+
171
+ on('toggle', async (event) => {
172
+ if (event.newState === 'open') {
173
+ context.showFocusTarget?.focus()
174
+ } else if (event.newState === 'closed' && options.restoreFocusOnHide !== false) {
175
+ context.hideFocusTarget?.focus()
176
+ }
177
+ }),
178
+
179
+ on('keydown', (event) => {
180
+ if (event.key === 'Escape') {
181
+ event.preventDefault()
182
+ options.onHide({ reason: 'escape-key' })
183
+ }
184
+ }),
185
+
186
+ onOutsideClick(
187
+ openProp,
188
+ (target) => {
189
+ options.onHide({ reason: 'outside-click', target })
190
+ },
191
+ (target) =>
192
+ options.closeOnAnchorClick === false && anchorContains(context.anchor, target),
193
+ options.stopOutsideClickPropagation ?? true,
194
+ ),
195
+ ]
196
+ }
187
197
  })
188
- })
189
198
 
190
- const focusOnHideMixin = createMixin<HTMLElement, []>((handle) => {
191
- handle.addEventListener('insert', (event) => {
192
- let context = handle.context.get(PopoverProvider)
193
- context.hideFocusTarget = event.node
194
- })
195
- })
199
+ const focusOnShowMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<HTMLElement, []>(
200
+ (handle) => {
201
+ handle.addEventListener('insert', (event) => {
202
+ let context = handle.context.get(PopoverProvider)
203
+ context.showFocusTarget = event.node
204
+ })
205
+ },
206
+ )
207
+
208
+ const focusOnHideMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<HTMLElement, []>(
209
+ (handle) => {
210
+ handle.addEventListener('insert', (event) => {
211
+ let context = handle.context.get(PopoverProvider)
212
+ context.hideFocusTarget = event.node
213
+ })
214
+ },
215
+ )
196
216
 
197
217
  export const Context = PopoverProvider
198
218
  export const anchor = anchorMixin
@@ -9,6 +9,7 @@ import { theme } from '@remix-run/ui/theme'
9
9
  /**
10
10
  * @name Select Deconstructed
11
11
  * @description Build a fully custom select by composing the select, popover, and listbox primitives directly.
12
+ * @layout center
12
13
  */
13
14
  export default function Example(handle: Handle) {
14
15
  let label = 'Local'
@@ -5,6 +5,7 @@ import { theme } from '@remix-run/ui/theme'
5
5
  /**
6
6
  * @name Select Overview
7
7
  * @description A styled select control with a searchable dropdown and accessible label.
8
+ * @layout center
8
9
  */
9
10
  export default function Example() {
10
11
  return () => (
@@ -10,6 +10,7 @@ import {
10
10
  type ElementProps,
11
11
  type Handle,
12
12
  type MixinHandle,
13
+ type MixinFactory,
13
14
  type Props,
14
15
  type RemixNode,
15
16
  } from '@remix-run/ui'
@@ -106,7 +107,7 @@ interface SelectContextValue {
106
107
  unregisterTrigger: (node: HTMLButtonElement) => void
107
108
  }
108
109
 
109
- function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>) {
110
+ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>): () => RemixNode {
110
111
  let triggerRef: HTMLButtonElement | undefined
111
112
  let listboxRef: listbox.ListboxRef | undefined
112
113
  let surfaceRef: HTMLElement | undefined
@@ -155,7 +156,7 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
155
156
  popoverContextRef.hideFocusTarget = triggerRef ?? null
156
157
  popoverContextRef.anchor = triggerRef
157
158
  ? {
158
- node: triggerRef,
159
+ target: triggerRef,
159
160
  options: getPopoverAnchorOptions(),
160
161
  }
161
162
  : null
@@ -396,7 +397,11 @@ function getSelectContext(handle: Handle | MixinHandle) {
396
397
  return handle.context.get(SelectProvider)
397
398
  }
398
399
 
399
- const triggerMixin = createMixin<HTMLButtonElement, [], ElementProps>((handle) => {
400
+ const triggerMixin: MixinFactory<HTMLButtonElement, [], ElementProps> = createMixin<
401
+ HTMLButtonElement,
402
+ [],
403
+ ElementProps
404
+ >((handle) => {
400
405
  let context = getSelectContext(handle)
401
406
 
402
407
  return (props) => [
@@ -428,7 +433,11 @@ const triggerMixin = createMixin<HTMLButtonElement, [], ElementProps>((handle) =
428
433
  ]
429
434
  })
430
435
 
431
- const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
436
+ const popoverMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
437
+ HTMLElement,
438
+ [],
439
+ ElementProps
440
+ >((handle) => {
432
441
  let context = getSelectContext(handle)
433
442
  let popoverState = handle.context.get(popover.Context)
434
443
 
@@ -453,7 +462,11 @@ const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
453
462
  ]
454
463
  })
455
464
 
456
- const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
465
+ const listMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
466
+ HTMLElement,
467
+ [],
468
+ ElementProps
469
+ >((handle) => {
457
470
  let context = getSelectContext(handle)
458
471
 
459
472
  return () => [
@@ -466,7 +479,11 @@ const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
466
479
  ]
467
480
  })
468
481
 
469
- const hiddenInputMixin = createMixin<HTMLInputElement, [], ElementProps>((handle) => {
482
+ const hiddenInputMixin: MixinFactory<HTMLInputElement, [], ElementProps> = createMixin<
483
+ HTMLInputElement,
484
+ [],
485
+ ElementProps
486
+ >((handle) => {
470
487
  let context = getSelectContext(handle)
471
488
 
472
489
  return () =>
@@ -532,17 +549,17 @@ const select = {
532
549
  export function onSelectChange<target extends HTMLElement>(
533
550
  handler: SelectChangeHandler<target>,
534
551
  captureBoolean?: boolean,
535
- ) {
552
+ ): ReturnType<typeof on<target, typeof SELECT_CHANGE_EVENT>> {
536
553
  return on(SELECT_CHANGE_EVENT, handler, captureBoolean)
537
554
  }
538
555
 
539
- function SelectLabel(handle: Handle) {
556
+ function SelectLabel(handle: Handle): () => RemixNode {
540
557
  let context = getSelectContext(handle)
541
558
 
542
559
  return () => <span mix={button.labelStyle}>{context.displayedLabel}</span>
543
560
  }
544
561
 
545
- export function Select(handle: Handle<SelectProps>) {
562
+ export function Select(handle: Handle<SelectProps>): () => RemixNode {
546
563
  return () => {
547
564
  let { children, defaultLabel, defaultValue, disabled, name, mix, ...buttonProps } = handle.props
548
565
 
@@ -568,7 +585,7 @@ export function Select(handle: Handle<SelectProps>) {
568
585
  }
569
586
  }
570
587
 
571
- export function Option(handle: Handle<SelectOptionProps>) {
588
+ export function Option(handle: Handle<SelectOptionProps>): () => RemixNode {
572
589
  return () => {
573
590
  let { label, value, disabled, textValue, children, mix, ...divProps } = handle.props
574
591
 
package/src/index.ts CHANGED
@@ -51,6 +51,7 @@ export type { HostProps, LayoutAnimationConfig } from './runtime/dom.ts'
51
51
  export { createMixin } from './runtime/mixins/mixin.ts'
52
52
  export type {
53
53
  MixinDescriptor,
54
+ MixinFactory,
54
55
  MixinHandle,
55
56
  MixinType,
56
57
  MixInput,
@@ -47,7 +47,7 @@ export type EntryMetadata = {
47
47
  */
48
48
  export type EntryComponent<props extends SerializableProps = {}, context = NoContext> = ((
49
49
  handle: Handle<props, context>,
50
- ) => RenderFn<props>) &
50
+ ) => RenderFn) &
51
51
  EntryMetadata
52
52
 
53
53
  /**
@@ -86,10 +86,6 @@ export function clientEntry<props extends SerializableProps = {}, context = NoCo
86
86
  entryId: string,
87
87
  component: (handle: Handle<props, context>) => RenderFn,
88
88
  ): EntryComponent<props, context>
89
- export function clientEntry<props extends SerializableProps = {}, context = NoContext>(
90
- entryId: string,
91
- component: (handle: Handle<Record<string, never>, context>) => RenderFn<props>,
92
- ): EntryComponent<props, context>
93
89
 
94
90
  // Implementation
95
91
  export function clientEntry(entryId: string, component: any): any {
@@ -118,13 +118,18 @@ export type ContextFrom<ComponentType> =
118
118
 
119
119
  /**
120
120
  * Context storage API exposed on component handles.
121
+ *
122
+ * Context values are keyed by provider component identity. `get(Component)`
123
+ * reads the nearest ancestor instance whose component function is exactly
124
+ * `Component`, so nested instances of the same provider shadow outer instances
125
+ * while different component types remain independent.
121
126
  */
122
127
  export interface Context<C> {
123
128
  /** Replaces the current context value for this component instance. */
124
129
  set(values: C): void
125
- /** Reads the context value associated with the given component type. */
130
+ /** Reads the context value from the nearest ancestor instance of the given component type. */
126
131
  get<ComponentType>(component: ComponentType): ContextFrom<ComponentType>
127
- /** Reads the context value associated with the given component key. */
132
+ /** Reads an unknown context value for an untyped lookup. */
128
133
  get(component: ElementType | symbol): unknown | undefined
129
134
  }
130
135
 
@@ -174,9 +179,9 @@ export type ComponentFn<Props = Record<string, never>, ContextValue = NoContext>
174
179
  ) => RenderFn
175
180
 
176
181
  /**
177
- * Render function returned by a component factory.
182
+ * Zero-argument render function returned by a component factory.
178
183
  */
179
- export type RenderFn<Props = ElementProps> = (props: Props) => RemixNode
184
+ export type RenderFn = () => RemixNode
180
185
 
181
186
  export type { RemixNode } from './jsx.ts'
182
187
 
@@ -218,7 +223,14 @@ type ComponentConfig = {
218
223
  /**
219
224
  * Runtime handle returned by {@link createComponent}.
220
225
  */
221
- export type ComponentHandle = ReturnType<typeof createComponent>
226
+ export interface ComponentHandle<C = NoContext> {
227
+ frame: FrameHandle
228
+ render(nextProps: ElementProps): [RemixNode, Array<() => void>]
229
+ remove(): Array<() => void>
230
+ setScheduleUpdate(nextScheduleUpdate: () => void): void
231
+ getContextValue(): C | undefined
232
+ isRemoved(): boolean
233
+ }
222
234
 
223
235
  /**
224
236
  * Creates the internal runtime wrapper for a component instance.
@@ -226,11 +238,11 @@ export type ComponentHandle = ReturnType<typeof createComponent>
226
238
  * @param config Component runtime configuration.
227
239
  * @returns Component runtime helpers used by the reconciler.
228
240
  */
229
- export function createComponent<C = NoContext>(config: ComponentConfig) {
241
+ export function createComponent<C = NoContext>(config: ComponentConfig): ComponentHandle<C> {
230
242
  return new ComponentRuntime<C>(config)
231
243
  }
232
244
 
233
- class ComponentRuntime<C = NoContext> {
245
+ class ComponentRuntime<C = NoContext> implements ComponentHandle<C> {
234
246
  frame: FrameHandle
235
247
 
236
248
  #config: ComponentConfig
@@ -240,6 +252,7 @@ class ComponentRuntime<C = NoContext> {
240
252
  #props = {} as ElementProps
241
253
  #renderController: AbortController | undefined
242
254
  #renderFn: RenderFn | undefined
255
+ #removed = false
243
256
  #scheduleUpdate: () => void = () => {
244
257
  throw new Error('scheduleUpdate not implemented')
245
258
  }
@@ -252,7 +265,7 @@ class ComponentRuntime<C = NoContext> {
252
265
  }
253
266
 
254
267
  render = (nextProps: ElementProps): [RemixNode, Array<() => void>] => {
255
- if (this.#connectedController?.signal.aborted) {
268
+ if (this.#removed) {
256
269
  console.warn('render called after component was removed, potential application memory leak')
257
270
  return [null, []]
258
271
  }
@@ -274,13 +287,15 @@ class ComponentRuntime<C = NoContext> {
274
287
  this.#renderFn = renderFn
275
288
  }
276
289
 
277
- return [renderFn(this.#props), this.#dequeueTasks()]
290
+ return [renderFn(), this.#dequeueTasks()]
278
291
  }
279
292
 
280
293
  remove = (): Array<() => void> => {
294
+ if (this.#removed) return []
295
+ this.#removed = true
281
296
  this.#connectedController?.abort()
282
297
  this.#abortRenderSignal()
283
- return this.#dequeueTasks()
298
+ return this.#dequeueTasks(AbortSignal.abort())
284
299
  }
285
300
 
286
301
  setScheduleUpdate = (nextScheduleUpdate: () => void): void => {
@@ -289,6 +304,8 @@ class ComponentRuntime<C = NoContext> {
289
304
 
290
305
  getContextValue = (): C | undefined => this.#contextValue
291
306
 
307
+ isRemoved = (): boolean => this.#removed
308
+
292
309
  #createHandle(): Handle<ElementProps, C> {
293
310
  let component = this
294
311
  let context: Context<C> = {
@@ -303,6 +320,11 @@ class ComponentRuntime<C = NoContext> {
303
320
  props: this.#props,
304
321
  update: () =>
305
322
  new Promise((resolve) => {
323
+ if (component.#removed) {
324
+ resolve(AbortSignal.abort())
325
+ return
326
+ }
327
+
306
328
  this.#tasks.push((signal) => resolve(signal))
307
329
  this.#scheduleUpdate()
308
330
  }),
@@ -335,14 +357,14 @@ class ComponentRuntime<C = NoContext> {
335
357
  this.#renderController = undefined
336
358
  }
337
359
 
338
- #dequeueTasks(): Array<() => void> {
339
- let needsSignal = this.#tasks.some((task) => task.length >= 1)
360
+ #dequeueTasks(signal?: AbortSignal): Array<() => void> {
361
+ let needsSignal = signal === undefined && this.#tasks.some((task) => task.length >= 1)
340
362
 
341
363
  if (needsSignal) {
342
364
  this.#renderController ??= new AbortController()
343
365
  }
344
366
 
345
- let signal = this.#renderController?.signal
367
+ signal ??= this.#renderController?.signal
346
368
  let tasks = this.#tasks.splice(0, this.#tasks.length)
347
369
  return tasks.map((task) => () => task(signal!))
348
370
  }
@@ -366,7 +388,7 @@ function syncProps(target: ElementProps, next: ElementProps): void {
366
388
  * @param handle Component handle for the frame instance.
367
389
  * @returns A placeholder render function handled by the reconciler.
368
390
  */
369
- export function Frame(handle: Handle<FrameProps, FrameHandle>) {
391
+ export function Frame(handle: Handle<FrameProps, FrameHandle>): RenderFn {
370
392
  void handle
371
393
  return () => null // reconciler renders
372
394
  }
@@ -377,7 +399,7 @@ export function Frame(handle: Handle<FrameProps, FrameHandle>) {
377
399
  * @param handle Component handle for the fragment instance.
378
400
  * @returns A placeholder render function handled by the reconciler.
379
401
  */
380
- export function Fragment(handle: Handle<FragmentProps>) {
402
+ export function Fragment(handle: Handle<FragmentProps>): RenderFn {
381
403
  void handle
382
404
  return () => null // reconciler renders
383
405
  }
@@ -14,6 +14,17 @@ const ATTRIBUTE_FALLBACK_NAMES = new Set([
14
14
  'colSpan',
15
15
  'role',
16
16
  'popover',
17
+ 'translate',
18
+ ])
19
+
20
+ const BOOLEANISH_STRING_ATTRIBUTES = new Set([
21
+ 'autoReverse',
22
+ 'contenteditable',
23
+ 'draggable',
24
+ 'externalResourcesRequired',
25
+ 'focusable',
26
+ 'preserveAlpha',
27
+ 'spellcheck',
17
28
  ])
18
29
 
19
30
  export const FRAMEWORK_PROPS = new Set(['children', 'mix', 'key', 'animate', 'innerHTML', 'on'])
@@ -43,9 +54,11 @@ export function canUseProperty(
43
54
  element: Element,
44
55
  name: string,
45
56
  isSvg: boolean,
57
+ attr: string,
46
58
  ): element is Element & Record<string, unknown> {
47
59
  if (isSvg) return false
48
60
  if (ATTRIBUTE_FALLBACK_NAMES.has(name)) return false
61
+ if (isBooleanishStringAttribute(attr)) return false
49
62
  return name in element
50
63
  }
51
64
 
@@ -68,6 +81,14 @@ export function normalizeAttributeName(
68
81
  return normalizeSvgAttribute(name)
69
82
  }
70
83
 
84
+ export function isBooleanishStringAttribute(name: string): boolean {
85
+ return BOOLEANISH_STRING_ATTRIBUTES.has(name)
86
+ }
87
+
88
+ export function shouldStringifyBooleanAttribute(name: string): boolean {
89
+ return isBooleanishStringAttribute(name) || name === 'value'
90
+ }
91
+
71
92
  export function serializeStyleObject(style: Record<string, unknown>): string {
72
93
  let parts: string[] = []
73
94
 
@@ -2,6 +2,7 @@ import type { ElementProps } from '../jsx.ts'
2
2
  import {
3
3
  canUseProperty,
4
4
  getMergedClassName,
5
+ isBooleanishStringAttribute,
5
6
  normalizeAttributeName,
6
7
  serializeStyleObject,
7
8
  toKebabCase,
@@ -68,11 +69,11 @@ export function patchHostProps(curr: ElementProps, next: ElementProps, dom: Elem
68
69
  if (name === 'class' || name === 'className') continue
69
70
  if (name in next && next[name] != null) continue
70
71
 
71
- if (canUseProperty(dom, name, isSvg)) {
72
+ let { ns, attr } = normalizeAttributeName(name, isSvg)
73
+ if (canUseProperty(dom, name, isSvg, attr)) {
72
74
  clearRuntimePropertyOnRemoval(dom, name)
73
75
  }
74
76
 
75
- let { ns, attr } = normalizeAttributeName(name, isSvg)
76
77
  if (ns) dom.removeAttributeNS(ns, toLocalName(attr))
77
78
  else dom.removeAttribute(attr)
78
79
  }
@@ -114,7 +115,7 @@ function patchHostProp(dom: Element, name: string, value: unknown, isSvg: boolea
114
115
  return
115
116
  }
116
117
 
117
- if (canUseProperty(dom, name, isSvg)) {
118
+ if (canUseProperty(dom, name, isSvg, attr)) {
118
119
  try {
119
120
  dom[name] = value == null ? '' : value
120
121
  return
@@ -124,7 +125,8 @@ function patchHostProp(dom: Element, name: string, value: unknown, isSvg: boolea
124
125
  if (typeof value === 'function') return
125
126
 
126
127
  let isAriaOrData = name.startsWith('aria-') || name.startsWith('data-')
127
- if (value != null && (value !== false || isAriaOrData)) {
128
+ let isBooleanishString = isBooleanishStringAttribute(attr)
129
+ if (value != null && (value !== false || isAriaOrData || isBooleanishString)) {
128
130
  let attrValue = name === 'popover' && value === true ? '' : String(value)
129
131
  if (ns) dom.setAttributeNS(ns, attr, attrValue)
130
132
  else dom.setAttribute(attr, attrValue)
@@ -0,0 +1,21 @@
1
+ import { on, type Handle } from '@remix-run/ui'
2
+
3
+ /**
4
+ * @name Basic Counter
5
+ * @description The smallest interactive Remix UI counter from the standalone demos.
6
+ */
7
+ export default function App(handle: Handle) {
8
+ let count = 0
9
+ return () => (
10
+ <button
11
+ mix={[
12
+ on('click', () => {
13
+ count++
14
+ handle.update()
15
+ }),
16
+ ]}
17
+ >
18
+ Ye ol' counter: {count}
19
+ </button>
20
+ )
21
+ }