@remix-run/ui 0.3.0 → 0.4.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 (308) hide show
  1. package/README.md +50 -164
  2. package/dist/accordion/index.d.ts +46 -0
  3. package/dist/accordion/index.js +142 -0
  4. package/dist/accordion/index.js.map +1 -0
  5. package/dist/accordion/primitives.d.ts +104 -0
  6. package/dist/accordion/primitives.js +312 -0
  7. package/dist/accordion/primitives.js.map +1 -0
  8. package/dist/{components/anchor/anchor.js → anchor/index.js} +1 -1
  9. package/dist/anchor/index.js.map +1 -0
  10. package/dist/animation/demos/reordering.js +9 -6
  11. package/dist/animation/demos/reordering.js.map +1 -1
  12. package/dist/{components/breadcrumbs/breadcrumbs.js → breadcrumbs/index.js} +19 -19
  13. package/dist/breadcrumbs/index.js.map +1 -0
  14. package/dist/button/index.d.ts +15 -0
  15. package/dist/button/index.js +128 -0
  16. package/dist/button/index.js.map +1 -0
  17. package/dist/checkbox/index.d.ts +14 -0
  18. package/dist/checkbox/index.js +125 -0
  19. package/dist/checkbox/index.js.map +1 -0
  20. package/dist/combobox/index.d.ts +21 -0
  21. package/dist/combobox/index.js +58 -0
  22. package/dist/combobox/index.js.map +1 -0
  23. package/dist/{components/combobox/combobox.d.ts → combobox/primitives.d.ts} +3 -7
  24. package/dist/{components/combobox/combobox.js → combobox/primitives.js} +8 -61
  25. package/dist/combobox/primitives.js.map +1 -0
  26. package/dist/input/index.d.ts +13 -0
  27. package/dist/input/index.js +121 -0
  28. package/dist/input/index.js.map +1 -0
  29. package/dist/listbox/flash-attribute.js.map +1 -0
  30. package/dist/{components/listbox/listbox.d.ts → listbox/index.d.ts} +2 -6
  31. package/dist/{components/listbox/listbox.js → listbox/index.js} +4 -82
  32. package/dist/listbox/index.js.map +1 -0
  33. package/dist/menu/hover-aim.js.map +1 -0
  34. package/dist/menu/index.d.ts +42 -0
  35. package/dist/menu/index.js +250 -0
  36. package/dist/menu/index.js.map +1 -0
  37. package/dist/{components/menu/menu.d.ts → menu/primitives.d.ts} +3 -32
  38. package/dist/{components/menu/menu.js → menu/primitives.js} +97 -314
  39. package/dist/menu/primitives.js.map +1 -0
  40. package/dist/{components/popover/popover.d.ts → popover/index.d.ts} +2 -4
  41. package/dist/{components/popover/popover.js → popover/index.js} +5 -48
  42. package/dist/popover/index.js.map +1 -0
  43. package/dist/{interactions/outside-click/outside-click-mixin.js → popover/outside-click.js} +1 -1
  44. package/dist/popover/outside-click.js.map +1 -0
  45. package/dist/popover/scroll-lock.d.ts +1 -0
  46. package/dist/{utils → popover}/scroll-lock.js +0 -18
  47. package/dist/popover/scroll-lock.js.map +1 -0
  48. package/dist/radio/index.d.ts +12 -0
  49. package/dist/radio/index.js +77 -0
  50. package/dist/radio/index.js.map +1 -0
  51. package/dist/runtime/reconcile.js +11 -5
  52. package/dist/runtime/reconcile.js.map +1 -1
  53. package/dist/runtime/vdom.js +3 -0
  54. package/dist/runtime/vdom.js.map +1 -1
  55. package/dist/select/index.d.ts +19 -0
  56. package/dist/select/index.js +95 -0
  57. package/dist/select/index.js.map +1 -0
  58. package/dist/select/keydown.js.map +1 -0
  59. package/dist/{components/select/select.d.ts → select/primitives.d.ts} +4 -6
  60. package/dist/{components/select/select.js → select/primitives.js} +11 -63
  61. package/dist/select/primitives.js.map +1 -0
  62. package/dist/server/stream.js +2 -2
  63. package/dist/server/stream.js.map +1 -1
  64. package/dist/shared/focus-styles.d.ts +1 -0
  65. package/dist/shared/focus-styles.js +2 -0
  66. package/dist/shared/focus-styles.js.map +1 -0
  67. package/dist/shared/icons.d.ts +9 -0
  68. package/dist/shared/icons.js +42 -0
  69. package/dist/shared/icons.js.map +1 -0
  70. package/dist/shared/listbox-popover-styles.d.ts +6 -0
  71. package/dist/shared/listbox-popover-styles.js +137 -0
  72. package/dist/shared/listbox-popover-styles.js.map +1 -0
  73. package/dist/shared/style-values.d.ts +78 -0
  74. package/dist/{theme/presets/rmx-01/index.js → shared/style-values.js} +3 -34
  75. package/dist/shared/style-values.js.map +1 -0
  76. package/dist/{interactions/typeahead/typeahead-mixin.js → shared/typeahead.js} +1 -1
  77. package/dist/shared/typeahead.js.map +1 -0
  78. package/dist/shared/wait-for-css-transition.js.map +1 -0
  79. package/dist/shared/wait.js.map +1 -0
  80. package/dist/tabs/index.d.ts +32 -0
  81. package/dist/tabs/index.js +157 -0
  82. package/dist/tabs/index.js.map +1 -0
  83. package/dist/tabs/primitives.d.ts +57 -0
  84. package/dist/tabs/primitives.js +238 -0
  85. package/dist/tabs/primitives.js.map +1 -0
  86. package/dist/toggle/index.d.ts +9 -0
  87. package/dist/toggle/index.js +108 -0
  88. package/dist/toggle/index.js.map +1 -0
  89. package/dist/toggle/primitives.d.ts +29 -0
  90. package/dist/toggle/primitives.js +127 -0
  91. package/dist/toggle/primitives.js.map +1 -0
  92. package/package.json +73 -44
  93. package/src/{components/accordion → accordion}/README.md +50 -14
  94. package/src/accordion/index.tsx +285 -0
  95. package/src/accordion/primitives.tsx +512 -0
  96. package/src/{components/anchor → anchor}/README.md +37 -2
  97. package/src/animation/demos/reordering.tsx +9 -6
  98. package/src/{components/breadcrumbs → breadcrumbs}/README.md +4 -4
  99. package/src/{components/breadcrumbs/breadcrumbs.tsx → breadcrumbs/index.tsx} +18 -18
  100. package/src/button/README.md +44 -0
  101. package/src/button/index.ts +168 -0
  102. package/src/checkbox/README.md +59 -0
  103. package/src/checkbox/index.ts +162 -0
  104. package/src/{components/combobox → combobox}/README.md +58 -9
  105. package/src/combobox/index.tsx +112 -0
  106. package/src/{components/combobox/combobox.tsx → combobox/primitives.tsx} +5 -91
  107. package/src/input/README.md +52 -0
  108. package/src/input/index.ts +147 -0
  109. package/src/{components/listbox → listbox}/README.md +9 -41
  110. package/src/{components/listbox/listbox.ts → listbox/index.ts} +2 -87
  111. package/src/{components/menu → menu}/README.md +55 -14
  112. package/src/menu/index.tsx +354 -0
  113. package/src/{components/menu/menu.tsx → menu/primitives.tsx} +99 -413
  114. package/src/{components/popover → popover}/README.md +20 -39
  115. package/src/{components/popover/popover.ts → popover/index.ts} +3 -57
  116. package/src/{utils → popover}/scroll-lock.ts +0 -28
  117. package/src/radio/README.md +53 -0
  118. package/src/radio/index.ts +100 -0
  119. package/src/runtime/demos/readme.demo.tsx +16 -14
  120. package/src/runtime/reconcile.ts +11 -5
  121. package/src/runtime/vdom.ts +3 -0
  122. package/src/{components/select → select}/README.md +29 -19
  123. package/src/select/index.tsx +159 -0
  124. package/src/{components/select/select.tsx → select/primitives.tsx} +10 -98
  125. package/src/server/stream.ts +3 -3
  126. package/src/shared/focus-styles.ts +2 -0
  127. package/src/shared/icons.tsx +62 -0
  128. package/src/shared/listbox-popover-styles.ts +146 -0
  129. package/src/shared/style-values.ts +165 -0
  130. package/src/tabs/README.md +141 -0
  131. package/src/tabs/index.tsx +244 -0
  132. package/src/tabs/primitives.ts +365 -0
  133. package/src/toggle/README.md +56 -0
  134. package/src/toggle/index.tsx +134 -0
  135. package/src/toggle/primitives.ts +192 -0
  136. package/dist/animation/demos/animation.demo.d.ts +0 -5
  137. package/dist/animation/demos/animation.demo.js +0 -92
  138. package/dist/animation/demos/animation.demo.js.map +0 -1
  139. package/dist/animation/demos/spring.demo.d.ts +0 -6
  140. package/dist/animation/demos/spring.demo.js +0 -304
  141. package/dist/animation/demos/spring.demo.js.map +0 -1
  142. package/dist/components/accordion/accordion.d.ts +0 -92
  143. package/dist/components/accordion/accordion.js +0 -335
  144. package/dist/components/accordion/accordion.js.map +0 -1
  145. package/dist/components/accordion/demos/card.demo.d.ts +0 -7
  146. package/dist/components/accordion/demos/card.demo.js +0 -59
  147. package/dist/components/accordion/demos/card.demo.js.map +0 -1
  148. package/dist/components/accordion/demos/multiple.demo.d.ts +0 -7
  149. package/dist/components/accordion/demos/multiple.demo.js +0 -20
  150. package/dist/components/accordion/demos/multiple.demo.js.map +0 -1
  151. package/dist/components/accordion/demos/overview.demo.d.ts +0 -7
  152. package/dist/components/accordion/demos/overview.demo.js +0 -20
  153. package/dist/components/accordion/demos/overview.demo.js.map +0 -1
  154. package/dist/components/anchor/anchor.js.map +0 -1
  155. package/dist/components/breadcrumbs/basic.demo.d.ts +0 -6
  156. package/dist/components/breadcrumbs/basic.demo.js +0 -15
  157. package/dist/components/breadcrumbs/basic.demo.js.map +0 -1
  158. package/dist/components/breadcrumbs/breadcrumbs.js.map +0 -1
  159. package/dist/components/breadcrumbs/separator.demo.d.ts +0 -6
  160. package/dist/components/breadcrumbs/separator.demo.js +0 -15
  161. package/dist/components/breadcrumbs/separator.demo.js.map +0 -1
  162. package/dist/components/button/button.d.ts +0 -105
  163. package/dist/components/button/button.js +0 -207
  164. package/dist/components/button/button.js.map +0 -1
  165. package/dist/components/button/demos/aliases.demo.d.ts +0 -7
  166. package/dist/components/button/demos/aliases.demo.js +0 -19
  167. package/dist/components/button/demos/aliases.demo.js.map +0 -1
  168. package/dist/components/button/demos/basic.demo.d.ts +0 -7
  169. package/dist/components/button/demos/basic.demo.js +0 -20
  170. package/dist/components/button/demos/basic.demo.js.map +0 -1
  171. package/dist/components/button/demos/states.demo.d.ts +0 -7
  172. package/dist/components/button/demos/states.demo.js +0 -30
  173. package/dist/components/button/demos/states.demo.js.map +0 -1
  174. package/dist/components/combobox/combobox.js.map +0 -1
  175. package/dist/components/combobox/overview.demo.d.ts +0 -7
  176. package/dist/components/combobox/overview.demo.js +0 -154
  177. package/dist/components/combobox/overview.demo.js.map +0 -1
  178. package/dist/components/glyph/glyph.d.ts +0 -14
  179. package/dist/components/glyph/glyph.js +0 -65
  180. package/dist/components/glyph/glyph.js.map +0 -1
  181. package/dist/components/listbox/listbox.js.map +0 -1
  182. package/dist/components/listbox/overview.demo.d.ts +0 -7
  183. package/dist/components/listbox/overview.demo.js +0 -65
  184. package/dist/components/listbox/overview.demo.js.map +0 -1
  185. package/dist/components/menu/demos/bubbling.demo.d.ts +0 -6
  186. package/dist/components/menu/demos/bubbling.demo.js +0 -16
  187. package/dist/components/menu/demos/bubbling.demo.js.map +0 -1
  188. package/dist/components/menu/demos/context-trigger.demo.d.ts +0 -7
  189. package/dist/components/menu/demos/context-trigger.demo.js +0 -87
  190. package/dist/components/menu/demos/context-trigger.demo.js.map +0 -1
  191. package/dist/components/menu/demos/overview.demo.d.ts +0 -7
  192. package/dist/components/menu/demos/overview.demo.js +0 -33
  193. package/dist/components/menu/demos/overview.demo.js.map +0 -1
  194. package/dist/components/menu/hover-aim.js.map +0 -1
  195. package/dist/components/menu/menu.js.map +0 -1
  196. package/dist/components/popover/overview.demo.d.ts +0 -7
  197. package/dist/components/popover/overview.demo.js +0 -65
  198. package/dist/components/popover/overview.demo.js.map +0 -1
  199. package/dist/components/popover/popover.js.map +0 -1
  200. package/dist/components/select/deconstructed.demo.d.ts +0 -7
  201. package/dist/components/select/deconstructed.demo.js +0 -54
  202. package/dist/components/select/deconstructed.demo.js.map +0 -1
  203. package/dist/components/select/overview.demo.d.ts +0 -6
  204. package/dist/components/select/overview.demo.js +0 -28
  205. package/dist/components/select/overview.demo.js.map +0 -1
  206. package/dist/components/select/select.js.map +0 -1
  207. package/dist/components/separator/separator.d.ts +0 -2
  208. package/dist/components/separator/separator.js +0 -15
  209. package/dist/components/separator/separator.js.map +0 -1
  210. package/dist/components/tabs/tabs.d.ts +0 -78
  211. package/dist/components/tabs/tabs.js +0 -339
  212. package/dist/components/tabs/tabs.js.map +0 -1
  213. package/dist/interactions/keydown/keydown.js.map +0 -1
  214. package/dist/interactions/outside-click/outside-click-mixin.js.map +0 -1
  215. package/dist/interactions/typeahead/typeahead-mixin.js.map +0 -1
  216. package/dist/runtime/demos/basic.demo.d.ts +0 -6
  217. package/dist/runtime/demos/basic.demo.js +0 -16
  218. package/dist/runtime/demos/basic.demo.js.map +0 -1
  219. package/dist/runtime/demos/controlled-uncontrolled-values.demo.d.ts +0 -6
  220. package/dist/runtime/demos/controlled-uncontrolled-values.demo.js +0 -77
  221. package/dist/runtime/demos/controlled-uncontrolled-values.demo.js.map +0 -1
  222. package/dist/runtime/demos/keyed-list.demo.d.ts +0 -6
  223. package/dist/runtime/demos/keyed-list.demo.js +0 -62
  224. package/dist/runtime/demos/keyed-list.demo.js.map +0 -1
  225. package/dist/runtime/demos/readme.demo.d.ts +0 -5
  226. package/dist/runtime/demos/readme.demo.js +0 -396
  227. package/dist/runtime/demos/readme.demo.js.map +0 -1
  228. package/dist/theme/contract.d.ts +0 -61
  229. package/dist/theme/contract.js +0 -131
  230. package/dist/theme/contract.js.map +0 -1
  231. package/dist/theme/glyph-contract.d.ts +0 -11
  232. package/dist/theme/glyph-contract.js +0 -34
  233. package/dist/theme/glyph-contract.js.map +0 -1
  234. package/dist/theme/presets/rmx-01/glyphs.d.ts +0 -2
  235. package/dist/theme/presets/rmx-01/glyphs.js +0 -252
  236. package/dist/theme/presets/rmx-01/glyphs.js.map +0 -1
  237. package/dist/theme/presets/rmx-01/index.d.ts +0 -4
  238. package/dist/theme/presets/rmx-01/index.js.map +0 -1
  239. package/dist/theme/runtime.d.ts +0 -2
  240. package/dist/theme/runtime.js +0 -84
  241. package/dist/theme/runtime.js.map +0 -1
  242. package/dist/theme/theme.d.ts +0 -6
  243. package/dist/theme/theme.js +0 -5
  244. package/dist/theme/theme.js.map +0 -1
  245. package/dist/utils/flash-attribute.js.map +0 -1
  246. package/dist/utils/scroll-lock.d.ts +0 -3
  247. package/dist/utils/scroll-lock.js.map +0 -1
  248. package/dist/utils/wait-for-css-transition.js.map +0 -1
  249. package/dist/utils/wait.js.map +0 -1
  250. package/src/animation/demos/animation.demo.tsx +0 -194
  251. package/src/animation/demos/spring.demo.tsx +0 -385
  252. package/src/components/accordion/accordion.tsx +0 -565
  253. package/src/components/accordion/demos/card.demo.tsx +0 -114
  254. package/src/components/accordion/demos/multiple.demo.tsx +0 -57
  255. package/src/components/accordion/demos/overview.demo.tsx +0 -57
  256. package/src/components/breadcrumbs/basic.demo.tsx +0 -18
  257. package/src/components/breadcrumbs/separator.demo.tsx +0 -19
  258. package/src/components/button/README.md +0 -44
  259. package/src/components/button/button.tsx +0 -286
  260. package/src/components/button/demos/aliases.demo.tsx +0 -28
  261. package/src/components/button/demos/basic.demo.tsx +0 -31
  262. package/src/components/button/demos/states.demo.tsx +0 -53
  263. package/src/components/combobox/overview.demo.tsx +0 -200
  264. package/src/components/glyph/README.md +0 -72
  265. package/src/components/glyph/glyph.tsx +0 -110
  266. package/src/components/listbox/overview.demo.tsx +0 -102
  267. package/src/components/menu/demos/bubbling.demo.tsx +0 -35
  268. package/src/components/menu/demos/context-trigger.demo.tsx +0 -133
  269. package/src/components/menu/demos/overview.demo.tsx +0 -77
  270. package/src/components/popover/overview.demo.tsx +0 -144
  271. package/src/components/select/deconstructed.demo.tsx +0 -96
  272. package/src/components/select/overview.demo.tsx +0 -52
  273. package/src/components/separator/separator.ts +0 -19
  274. package/src/components/tabs/README.md +0 -105
  275. package/src/components/tabs/tabs.tsx +0 -504
  276. package/src/runtime/demos/basic.demo.tsx +0 -21
  277. package/src/runtime/demos/controlled-uncontrolled-values.demo.tsx +0 -177
  278. package/src/runtime/demos/keyed-list.demo.tsx +0 -94
  279. package/src/theme/README.md +0 -103
  280. package/src/theme/contract.ts +0 -215
  281. package/src/theme/glyph-contract.ts +0 -59
  282. package/src/theme/presets/rmx-01/glyphs.tsx +0 -308
  283. package/src/theme/presets/rmx-01/index.ts +0 -123
  284. package/src/theme/runtime.ts +0 -119
  285. package/src/theme/theme.ts +0 -15
  286. package/src/utils/scroll-lock/README.md +0 -33
  287. /package/dist/{components/anchor/anchor.d.ts → anchor/index.d.ts} +0 -0
  288. /package/dist/{components/breadcrumbs/breadcrumbs.d.ts → breadcrumbs/index.d.ts} +0 -0
  289. /package/dist/{utils → listbox}/flash-attribute.d.ts +0 -0
  290. /package/dist/{utils → listbox}/flash-attribute.js +0 -0
  291. /package/dist/{components/menu → menu}/hover-aim.d.ts +0 -0
  292. /package/dist/{components/menu → menu}/hover-aim.js +0 -0
  293. /package/dist/{interactions/outside-click/outside-click-mixin.d.ts → popover/outside-click.d.ts} +0 -0
  294. /package/dist/{interactions/keydown → select}/keydown.d.ts +0 -0
  295. /package/dist/{interactions/keydown → select}/keydown.js +0 -0
  296. /package/dist/{interactions/typeahead/typeahead-mixin.d.ts → shared/typeahead.d.ts} +0 -0
  297. /package/dist/{utils → shared}/wait-for-css-transition.d.ts +0 -0
  298. /package/dist/{utils → shared}/wait-for-css-transition.js +0 -0
  299. /package/dist/{utils → shared}/wait.d.ts +0 -0
  300. /package/dist/{utils → shared}/wait.js +0 -0
  301. /package/src/{components/anchor/anchor.ts → anchor/index.ts} +0 -0
  302. /package/src/{utils → listbox}/flash-attribute.ts +0 -0
  303. /package/src/{components/menu → menu}/hover-aim.ts +0 -0
  304. /package/src/{interactions/outside-click/outside-click-mixin.ts → popover/outside-click.ts} +0 -0
  305. /package/src/{interactions/keydown → select}/keydown.ts +0 -0
  306. /package/src/{interactions/typeahead/typeahead-mixin.ts → shared/typeahead.ts} +0 -0
  307. /package/src/{utils → shared}/wait-for-css-transition.ts +0 -0
  308. /package/src/{utils → shared}/wait.ts +0 -0
@@ -1,504 +0,0 @@
1
- import {
2
- attrs,
3
- css,
4
- createElement,
5
- createMixin,
6
- on,
7
- type CSSMixinDescriptor,
8
- type Dispatched,
9
- type ElementProps,
10
- type Handle,
11
- type MixinHandle,
12
- type Props,
13
- type RemixNode,
14
- } from '@remix-run/ui'
15
- import * as button from '../button/button.tsx'
16
- import { theme } from '../../theme/theme.ts'
17
-
18
- const TABS_CHANGE_EVENT = 'rmx:tabs-change' as const
19
-
20
- type TabsChangeHandler<target extends HTMLElement> = (
21
- event: Dispatched<TabsChangeEvent, target>,
22
- signal: AbortSignal,
23
- ) => void | Promise<void>
24
-
25
- const tabsListCss: CSSMixinDescriptor = css({
26
- display: 'inline-flex',
27
- alignItems: 'center',
28
- gap: theme.space.xs,
29
- minWidth: 0,
30
- padding: theme.space.xs,
31
- border: `1px solid ${theme.colors.border.subtle}`,
32
- borderRadius: theme.radius.xl,
33
- backgroundColor: theme.surface.lvl2,
34
- })
35
-
36
- const tabsTriggerCss: CSSMixinDescriptor = css({
37
- justifyContent: 'flex-start',
38
- minHeight: theme.control.height.md,
39
- paddingInline: theme.space.md,
40
- border: '1px solid transparent',
41
- borderRadius: theme.radius.md,
42
- backgroundColor: 'transparent',
43
- color: theme.colors.text.secondary,
44
- fontSize: theme.fontSize.md,
45
- '&:hover:not(:disabled):not([aria-disabled="true"])': {
46
- color: theme.colors.text.primary,
47
- },
48
- '&:focus-visible': {
49
- outline: `2px solid ${theme.colors.focus.ring}`,
50
- outlineOffset: '2px',
51
- color: theme.colors.text.primary,
52
- },
53
- '&[aria-selected="true"], &[aria-selected="true"]:hover, &[aria-selected="true"]:focus-visible': {
54
- backgroundColor: theme.surface.lvl0,
55
- borderColor: theme.colors.border.subtle,
56
- boxShadow: theme.shadow.xs,
57
- color: theme.colors.text.primary,
58
- },
59
- '&:disabled, &[aria-disabled="true"]': {
60
- opacity: 0.55,
61
- },
62
- })
63
-
64
- export const listStyle = tabsListCss
65
- export const triggerStyle = tabsTriggerCss
66
-
67
- const panelVisibilityUtility = css({
68
- '&[hidden]': {
69
- display: 'none',
70
- },
71
- })
72
-
73
- declare global {
74
- interface HTMLElementEventMap {
75
- [TABS_CHANGE_EVENT]: TabsChangeEvent
76
- }
77
- }
78
-
79
- export class TabsChangeEvent extends Event {
80
- readonly previousValue: string | null
81
- readonly value: string
82
-
83
- constructor(value: string, previousValue: string | null) {
84
- super(TABS_CHANGE_EVENT, { bubbles: true })
85
- this.value = value
86
- this.previousValue = previousValue
87
- }
88
- }
89
-
90
- export type TabsOrientation = 'horizontal' | 'vertical'
91
-
92
- type TabsDirection = 'first' | 'last' | 'next' | 'previous'
93
-
94
- type RegisteredTab = {
95
- disabled: boolean
96
- getNode(): HTMLElement | null
97
- value: string
98
- }
99
-
100
- export interface TabsRef {
101
- readonly selectedValue: string | null
102
- focus: (value?: string) => void
103
- focusFirst: () => void
104
- focusLast: () => void
105
- select: (value: string) => void
106
- }
107
-
108
- interface TabsContextValue {
109
- readonly focusableValue: string | null
110
- readonly orientation: TabsOrientation
111
- readonly value: string | null
112
- getPanelId: (value: string) => string
113
- getTriggerId: (value: string) => string
114
- move: (fromValue: string, direction: TabsDirection) => void
115
- registerTab: (tab: RegisteredTab) => void
116
- select: (value: string) => void
117
- }
118
-
119
- export interface TabsContextProps {
120
- children?: RemixNode
121
- defaultValue?: string
122
- onValueChange?: (value: string) => void
123
- orientation?: TabsOrientation
124
- ref?: (ref: TabsRef) => void
125
- value?: string
126
- }
127
-
128
- export interface TabsProps extends TabsContextProps {}
129
-
130
- export interface TabsTriggerOptions {
131
- disabled?: boolean
132
- value: string
133
- }
134
-
135
- export interface TabsPanelOptions {
136
- value: string
137
- }
138
-
139
- export type TabsListProps = Props<'div'>
140
-
141
- export type TabProps = Omit<Props<'button'>, 'type'> & {
142
- children?: RemixNode
143
- disabled?: boolean
144
- type?: 'button' | 'submit' | 'reset'
145
- value: string
146
- }
147
-
148
- export type TabsPanelProps = Props<'div'> & {
149
- children?: RemixNode
150
- value: string
151
- }
152
-
153
- function getTabsContext(handle: Handle | MixinHandle) {
154
- return handle.context.get(TabsProvider)
155
- }
156
-
157
- function toIdFragment(value: string) {
158
- let fragment = value.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '')
159
- return fragment || 'tab'
160
- }
161
-
162
- function TabsProvider(handle: Handle<TabsContextProps, TabsContextValue>) {
163
- let registeredTabs: RegisteredTab[] = []
164
- let hasInitialized = false
165
- let uncontrolledValue: string | null = null
166
-
167
- function getValue() {
168
- return handle.props.value !== undefined ? handle.props.value : uncontrolledValue
169
- }
170
-
171
- function getOrientation() {
172
- return handle.props.orientation ?? 'horizontal'
173
- }
174
-
175
- function isActiveTab(tab: RegisteredTab | undefined): tab is RegisteredTab {
176
- return !!tab && !tab.disabled && !!tab.getNode()?.isConnected
177
- }
178
-
179
- function getTab(value: string | null | undefined) {
180
- if (value == null) {
181
- return undefined
182
- }
183
-
184
- return registeredTabs.find((tab) => tab.value === value)
185
- }
186
-
187
- function getEnabledTabs() {
188
- return registeredTabs.filter(isActiveTab)
189
- }
190
-
191
- function getFocusableValue() {
192
- let currentValue = getValue()
193
- let currentTab = getTab(currentValue)
194
-
195
- if (isActiveTab(currentTab)) {
196
- return currentValue
197
- }
198
-
199
- return getEnabledTabs()[0]?.value ?? null
200
- }
201
-
202
- function getTargetTab(fromValue: string, direction: TabsDirection) {
203
- let enabledTabs = getEnabledTabs()
204
- if (enabledTabs.length === 0) {
205
- return undefined
206
- }
207
-
208
- let currentIndex = enabledTabs.findIndex((tab) => tab.value === fromValue)
209
- let targetIndex = 0
210
-
211
- switch (direction) {
212
- case 'first':
213
- targetIndex = 0
214
- break
215
- case 'last':
216
- targetIndex = enabledTabs.length - 1
217
- break
218
- case 'next':
219
- targetIndex =
220
- currentIndex === -1 || currentIndex === enabledTabs.length - 1 ? 0 : currentIndex + 1
221
- break
222
- case 'previous':
223
- targetIndex = currentIndex <= 0 ? enabledTabs.length - 1 : currentIndex - 1
224
- break
225
- }
226
-
227
- return enabledTabs[targetIndex]
228
- }
229
-
230
- function dispatchChange(value: string, previousValue: string | null) {
231
- getTab(value)?.getNode()?.dispatchEvent(new TabsChangeEvent(value, previousValue))
232
- }
233
-
234
- function select(value: string) {
235
- let nextTab = getTab(value)
236
- if (!isActiveTab(nextTab)) {
237
- return
238
- }
239
-
240
- let previousValue = getValue()
241
- if (previousValue === value) {
242
- return
243
- }
244
-
245
- if (handle.props.value === undefined) {
246
- uncontrolledValue = value
247
- void handle.update()
248
- }
249
-
250
- handle.props.onValueChange?.(value)
251
- dispatchChange(value, previousValue ?? null)
252
- }
253
-
254
- let tabsRef: TabsRef = {
255
- get selectedValue() {
256
- return getValue()
257
- },
258
- focus(value = getFocusableValue() ?? undefined) {
259
- getTab(value)?.getNode()?.focus()
260
- },
261
- focusFirst() {
262
- getTargetTab(getFocusableValue() ?? '', 'first')
263
- ?.getNode()
264
- ?.focus()
265
- },
266
- focusLast() {
267
- getTargetTab(getFocusableValue() ?? '', 'last')
268
- ?.getNode()
269
- ?.focus()
270
- },
271
- select,
272
- }
273
-
274
- handle.context.set({
275
- get focusableValue() {
276
- return getFocusableValue()
277
- },
278
- get orientation() {
279
- return getOrientation()
280
- },
281
- get value() {
282
- return getValue()
283
- },
284
- getPanelId(value) {
285
- return `${handle.id}-panel-${toIdFragment(value)}`
286
- },
287
- getTriggerId(value) {
288
- return `${handle.id}-trigger-${toIdFragment(value)}`
289
- },
290
- move(fromValue, direction) {
291
- let target = getTargetTab(fromValue, direction)
292
- if (!target) {
293
- return
294
- }
295
-
296
- target.getNode()?.focus()
297
- select(target.value)
298
- },
299
- registerTab(tab) {
300
- registeredTabs.push(tab)
301
- },
302
- select,
303
- })
304
-
305
- return () => {
306
- registeredTabs = []
307
-
308
- if (!hasInitialized) {
309
- uncontrolledValue = handle.props.defaultValue ?? null
310
- hasInitialized = true
311
- }
312
-
313
- handle.queueTask(() => {
314
- handle.props.ref?.(tabsRef)
315
-
316
- if (handle.props.value !== undefined) {
317
- return
318
- }
319
-
320
- let nextValue = getFocusableValue()
321
- if (nextValue === uncontrolledValue) {
322
- return
323
- }
324
-
325
- uncontrolledValue = nextValue
326
- void handle.update()
327
- })
328
-
329
- return handle.props.children
330
- }
331
- }
332
-
333
- const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
334
- let context = getTabsContext(handle)
335
-
336
- return () =>
337
- attrs({
338
- role: 'tablist',
339
- 'aria-orientation': context.orientation === 'vertical' ? 'vertical' : undefined,
340
- 'data-orientation': context.orientation,
341
- })
342
- })
343
-
344
- const triggerMixin = createMixin<HTMLElement, [options: TabsTriggerOptions], ElementProps>(
345
- (handle) => {
346
- let triggerRef: HTMLElement | null = null
347
- let context = getTabsContext(handle)
348
-
349
- handle.queueTask((node) => {
350
- triggerRef = node
351
- })
352
-
353
- return (options, props) => {
354
- let disabled = options.disabled === true || props.disabled === true
355
- context.registerTab({
356
- disabled,
357
- getNode() {
358
- return triggerRef
359
- },
360
- value: options.value,
361
- })
362
-
363
- return [
364
- attrs({
365
- id: context.getTriggerId(options.value),
366
- role: 'tab',
367
- tabIndex: !disabled && context.focusableValue === options.value ? 0 : -1,
368
- 'aria-controls': context.getPanelId(options.value),
369
- 'aria-disabled': disabled ? 'true' : undefined,
370
- 'aria-selected': context.value === options.value ? 'true' : 'false',
371
- 'data-orientation': context.orientation,
372
- 'data-selected': context.value === options.value ? 'true' : 'false',
373
- }),
374
- !disabled && [
375
- on('click', () => {
376
- context.select(options.value)
377
- }),
378
- on('keydown', (event) => {
379
- switch (event.key) {
380
- case 'ArrowRight':
381
- if (context.orientation !== 'horizontal') {
382
- return
383
- }
384
- event.preventDefault()
385
- context.move(options.value, 'next')
386
- break
387
- case 'ArrowLeft':
388
- if (context.orientation !== 'horizontal') {
389
- return
390
- }
391
- event.preventDefault()
392
- context.move(options.value, 'previous')
393
- break
394
- case 'ArrowDown':
395
- if (context.orientation !== 'vertical') {
396
- return
397
- }
398
- event.preventDefault()
399
- context.move(options.value, 'next')
400
- break
401
- case 'ArrowUp':
402
- if (context.orientation !== 'vertical') {
403
- return
404
- }
405
- event.preventDefault()
406
- context.move(options.value, 'previous')
407
- break
408
- case 'Home':
409
- event.preventDefault()
410
- context.move(options.value, 'first')
411
- break
412
- case 'End':
413
- event.preventDefault()
414
- context.move(options.value, 'last')
415
- break
416
- }
417
- }),
418
- ],
419
- ]
420
- }
421
- },
422
- )
423
-
424
- const panelMixin = createMixin<HTMLElement, [options: TabsPanelOptions], ElementProps>((handle) => {
425
- let context = getTabsContext(handle)
426
-
427
- return (options) => {
428
- let isSelected = context.value === options.value
429
-
430
- return [
431
- attrs({
432
- id: context.getPanelId(options.value),
433
- role: 'tabpanel',
434
- hidden: isSelected ? undefined : true,
435
- 'aria-labelledby': context.getTriggerId(options.value),
436
- 'data-orientation': context.orientation,
437
- 'data-selected': isSelected ? 'true' : 'false',
438
- }),
439
- panelVisibilityUtility,
440
- ]
441
- }
442
- })
443
-
444
- export const Context = TabsProvider
445
- export const list = listMixin
446
- export const panel = panelMixin
447
- export const trigger = triggerMixin
448
-
449
- const tabs = { Context, list, panel, trigger } as const
450
-
451
- export function onTabsChange<target extends HTMLElement>(
452
- handler: TabsChangeHandler<target>,
453
- captureBoolean?: boolean,
454
- ) {
455
- return on(TABS_CHANGE_EVENT, handler, captureBoolean)
456
- }
457
-
458
- export function Tabs(handle: Handle<TabsProps>) {
459
- return () => {
460
- let { children, ...contextProps } = handle.props
461
- return <tabs.Context {...contextProps}>{children}</tabs.Context>
462
- }
463
- }
464
-
465
- export function TabsList(handle: Handle<TabsListProps>) {
466
- return () => {
467
- let { children, mix, ...divProps } = handle.props
468
-
469
- return (
470
- <div {...divProps} mix={[listStyle, tabs.list(), mix]}>
471
- {children}
472
- </div>
473
- )
474
- }
475
- }
476
-
477
- export function Tab(handle: Handle<TabProps>) {
478
- return () => {
479
- let { children, disabled, mix, type, value, ...buttonProps } = handle.props
480
-
481
- return (
482
- <button
483
- {...buttonProps}
484
- disabled={disabled ? true : undefined}
485
- mix={[button.baseStyle, triggerStyle, tabs.trigger({ disabled, value }), mix]}
486
- type={type ?? 'button'}
487
- >
488
- {children}
489
- </button>
490
- )
491
- }
492
- }
493
-
494
- export function TabsPanel(handle: Handle<TabsPanelProps>) {
495
- return () => {
496
- let { children, mix, value, ...divProps } = handle.props
497
-
498
- return (
499
- <div {...divProps} mix={[tabs.panel({ value }), mix]}>
500
- {children}
501
- </div>
502
- )
503
- }
504
- }
@@ -1,21 +0,0 @@
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
- }
@@ -1,177 +0,0 @@
1
- import { on, type Handle } from '@remix-run/ui'
2
-
3
- /**
4
- * @name Controlled and Uncontrolled Values
5
- * @description Compares controlled props with uncontrolled DOM values across rerenders and remounts.
6
- */
7
- export default function App(handle: Handle) {
8
- let controlledText = 'hello'
9
- let controlledChecked = true
10
- let controlledChoice = 'alpha'
11
- let uncontrolledTextSnapshot = 'type to update this'
12
- let uncontrolledCheckedSnapshot = true
13
- let renderCount = 0
14
- let uncontrolledVersion = 0
15
-
16
- let rerender = () => {
17
- renderCount++
18
- handle.update()
19
- }
20
-
21
- let resetControlled = () => {
22
- controlledText = 'hello'
23
- controlledChecked = true
24
- controlledChoice = 'alpha'
25
- rerender()
26
- }
27
-
28
- let remountUncontrolled = () => {
29
- uncontrolledVersion++
30
- uncontrolledTextSnapshot = 'type to update this'
31
- uncontrolledCheckedSnapshot = true
32
- rerender()
33
- }
34
-
35
- return () => (
36
- <main
37
- style={{
38
- fontFamily: 'system-ui, sans-serif',
39
- maxWidth: '860px',
40
- margin: '24px auto',
41
- padding: '0 16px',
42
- lineHeight: 1.45,
43
- }}
44
- >
45
- <h1>Controlled vs Uncontrolled Values</h1>
46
-
47
- <p>
48
- Render count: <strong>{renderCount}</strong>
49
- </p>
50
-
51
- <div style={{ display: 'flex', gap: '10px', marginBottom: '18px' }}>
52
- <button mix={[on('click', rerender)]}>Force Re-render</button>
53
- <button mix={[on('click', resetControlled)]}>Reset Controlled</button>
54
- <button mix={[on('click', remountUncontrolled)]}>Remount Uncontrolled</button>
55
- </div>
56
-
57
- <section
58
- style={{
59
- border: '1px solid #d0d7de',
60
- borderRadius: '8px',
61
- padding: '12px',
62
- marginBottom: '12px',
63
- }}
64
- >
65
- <h2>Controlled</h2>
66
- <p>
67
- These values come from component state. The text input allows everything except digits,
68
- and invalid input does not call update.
69
- </p>
70
-
71
- <label style={{ display: 'block', marginBottom: '8px' }}>
72
- Text:
73
- <input
74
- style={{ marginLeft: '8px' }}
75
- value={controlledText}
76
- mix={[
77
- on('input', (event) => {
78
- let nextValue = event.currentTarget.value
79
- if (/\d/.test(nextValue)) {
80
- return
81
- }
82
- controlledText = nextValue
83
- rerender()
84
- }),
85
- ]}
86
- />
87
- </label>
88
-
89
- <label style={{ display: 'block', marginBottom: '8px' }}>
90
- <input
91
- type="checkbox"
92
- checked={controlledChecked}
93
- mix={[
94
- on('change', (event) => {
95
- controlledChecked = event.currentTarget.checked
96
- rerender()
97
- }),
98
- ]}
99
- />{' '}
100
- Checked
101
- </label>
102
-
103
- <label style={{ display: 'block', marginBottom: '8px' }}>
104
- Choice:
105
- <select
106
- style={{ marginLeft: '8px' }}
107
- value={controlledChoice}
108
- mix={[
109
- on('change', (event) => {
110
- controlledChoice = event.currentTarget.value
111
- rerender()
112
- }),
113
- ]}
114
- >
115
- <option value="alpha">Alpha</option>
116
- <option value="beta">Beta</option>
117
- <option value="gamma">Gamma</option>
118
- </select>
119
- </label>
120
-
121
- <div>
122
- State snapshot: text=<code>{JSON.stringify(controlledText)}</code>, checked=
123
- <code>{String(controlledChecked)}</code>, choice=
124
- <code>{JSON.stringify(controlledChoice)}</code>
125
- </div>
126
- </section>
127
-
128
- <section
129
- key={`uncontrolled-${uncontrolledVersion}`}
130
- style={{
131
- border: '1px solid #d0d7de',
132
- borderRadius: '8px',
133
- padding: '12px',
134
- }}
135
- >
136
- <h2>Uncontrolled</h2>
137
- <p>
138
- These initialize from <code>defaultValue/defaultChecked</code> once and then keep their
139
- own DOM state.
140
- </p>
141
-
142
- <label style={{ display: 'block', marginBottom: '8px' }}>
143
- Text:
144
- <input
145
- style={{ marginLeft: '8px' }}
146
- defaultValue="type to update this"
147
- mix={[
148
- on('input', (event) => {
149
- uncontrolledTextSnapshot = event.currentTarget.value
150
- rerender()
151
- }),
152
- ]}
153
- />
154
- </label>
155
-
156
- <label style={{ display: 'block', marginBottom: '8px' }}>
157
- <input
158
- type="checkbox"
159
- defaultChecked={true}
160
- mix={[
161
- on('change', (event) => {
162
- uncontrolledCheckedSnapshot = event.currentTarget.checked
163
- rerender()
164
- }),
165
- ]}
166
- />{' '}
167
- Checked
168
- </label>
169
-
170
- <div>
171
- Last DOM snapshot: text=<code>{JSON.stringify(uncontrolledTextSnapshot)}</code>, checked=
172
- <code>{String(uncontrolledCheckedSnapshot)}</code>
173
- </div>
174
- </section>
175
- </main>
176
- )
177
- }