@remix-run/ui 0.0.0 → 0.1.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 (273) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/animation/animate-layout-mixin.d.ts +12 -0
  4. package/dist/animation/animate-layout-mixin.js +214 -0
  5. package/dist/animation/animate-layout-mixin.js.map +1 -0
  6. package/dist/animation/animate-mixins.d.ts +29 -0
  7. package/dist/animation/animate-mixins.js +235 -0
  8. package/dist/animation/animate-mixins.js.map +1 -0
  9. package/dist/animation/index.d.ts +6 -0
  10. package/dist/animation/index.js +5 -0
  11. package/dist/animation/index.js.map +1 -0
  12. package/dist/animation/spring.d.ts +75 -0
  13. package/dist/animation/spring.js +183 -0
  14. package/dist/animation/spring.js.map +1 -0
  15. package/dist/animation/tween.d.ts +70 -0
  16. package/dist/animation/tween.js +93 -0
  17. package/dist/animation/tween.js.map +1 -0
  18. package/dist/components/accordion/accordion.d.ts +92 -0
  19. package/dist/components/accordion/accordion.js +337 -0
  20. package/dist/components/accordion/accordion.js.map +1 -0
  21. package/dist/components/anchor/anchor.d.ts +13 -0
  22. package/dist/components/anchor/anchor.js +558 -0
  23. package/dist/components/anchor/anchor.js.map +1 -0
  24. package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
  25. package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
  26. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
  27. package/dist/components/button/button.d.ts +23 -0
  28. package/dist/components/button/button.js +147 -0
  29. package/dist/components/button/button.js.map +1 -0
  30. package/dist/components/combobox/combobox.d.ts +101 -0
  31. package/dist/components/combobox/combobox.js +708 -0
  32. package/dist/components/combobox/combobox.js.map +1 -0
  33. package/dist/components/glyph/glyph.d.ts +14 -0
  34. package/dist/components/glyph/glyph.js +65 -0
  35. package/dist/components/glyph/glyph.js.map +1 -0
  36. package/dist/components/listbox/listbox.d.ts +67 -0
  37. package/dist/components/listbox/listbox.js +340 -0
  38. package/dist/components/listbox/listbox.js.map +1 -0
  39. package/dist/components/menu/hover-aim.d.ts +5 -0
  40. package/dist/components/menu/hover-aim.js +308 -0
  41. package/dist/components/menu/hover-aim.js.map +1 -0
  42. package/dist/components/menu/menu.d.ts +164 -0
  43. package/dist/components/menu/menu.js +1106 -0
  44. package/dist/components/menu/menu.js.map +1 -0
  45. package/dist/components/popover/popover.d.ts +35 -0
  46. package/dist/components/popover/popover.js +138 -0
  47. package/dist/components/popover/popover.js.map +1 -0
  48. package/dist/components/select/select.d.ts +67 -0
  49. package/dist/components/select/select.js +397 -0
  50. package/dist/components/select/select.js.map +1 -0
  51. package/dist/components/separator/separator.d.ts +2 -0
  52. package/dist/components/separator/separator.js +15 -0
  53. package/dist/components/separator/separator.js.map +1 -0
  54. package/dist/components/tabs/tabs.d.ts +78 -0
  55. package/dist/components/tabs/tabs.js +341 -0
  56. package/dist/components/tabs/tabs.js.map +1 -0
  57. package/dist/index.d.ts +28 -0
  58. package/dist/index.js +21 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/interactions/keydown/keydown.d.ts +1 -0
  61. package/dist/interactions/keydown/keydown.js +8 -0
  62. package/dist/interactions/keydown/keydown.js.map +1 -0
  63. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
  64. package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
  65. package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
  66. package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
  67. package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
  68. package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
  69. package/dist/jsx-runtime.d.ts +2 -0
  70. package/dist/jsx-runtime.js +3 -0
  71. package/dist/jsx-runtime.js.map +1 -0
  72. package/dist/runtime/client-entries.d.ts +91 -0
  73. package/dist/runtime/client-entries.js +40 -0
  74. package/dist/runtime/client-entries.js.map +1 -0
  75. package/dist/runtime/component.d.ts +223 -0
  76. package/dist/runtime/component.js +152 -0
  77. package/dist/runtime/component.js.map +1 -0
  78. package/dist/runtime/create-element.d.ts +10 -0
  79. package/dist/runtime/create-element.js +29 -0
  80. package/dist/runtime/create-element.js.map +1 -0
  81. package/dist/runtime/diff-dom.d.ts +2 -0
  82. package/dist/runtime/diff-dom.js +364 -0
  83. package/dist/runtime/diff-dom.js.map +1 -0
  84. package/dist/runtime/diff-props.d.ts +9 -0
  85. package/dist/runtime/diff-props.js +195 -0
  86. package/dist/runtime/diff-props.js.map +1 -0
  87. package/dist/runtime/document-state.d.ts +11 -0
  88. package/dist/runtime/document-state.js +106 -0
  89. package/dist/runtime/document-state.js.map +1 -0
  90. package/dist/runtime/dom.d.ts +3189 -0
  91. package/dist/runtime/dom.js +2 -0
  92. package/dist/runtime/dom.js.map +1 -0
  93. package/dist/runtime/error-event.d.ts +20 -0
  94. package/dist/runtime/error-event.js +19 -0
  95. package/dist/runtime/error-event.js.map +1 -0
  96. package/dist/runtime/event-listeners.d.ts +50 -0
  97. package/dist/runtime/event-listeners.js +31 -0
  98. package/dist/runtime/event-listeners.js.map +1 -0
  99. package/dist/runtime/frame.d.ts +119 -0
  100. package/dist/runtime/frame.js +915 -0
  101. package/dist/runtime/frame.js.map +1 -0
  102. package/dist/runtime/invariant.d.ts +23 -0
  103. package/dist/runtime/invariant.js +33 -0
  104. package/dist/runtime/invariant.js.map +1 -0
  105. package/dist/runtime/jsx.d.ts +320 -0
  106. package/dist/runtime/jsx.js +31 -0
  107. package/dist/runtime/jsx.js.map +1 -0
  108. package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
  109. package/dist/runtime/mixins/attrs-mixin.js +23 -0
  110. package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
  111. package/dist/runtime/mixins/link-mixin.d.ts +17 -0
  112. package/dist/runtime/mixins/link-mixin.js +101 -0
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -0
  114. package/dist/runtime/mixins/mixin.d.ts +145 -0
  115. package/dist/runtime/mixins/mixin.js +570 -0
  116. package/dist/runtime/mixins/mixin.js.map +1 -0
  117. package/dist/runtime/mixins/on-mixin.d.ts +16 -0
  118. package/dist/runtime/mixins/on-mixin.js +49 -0
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -0
  120. package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
  121. package/dist/runtime/mixins/ref-mixin.js +21 -0
  122. package/dist/runtime/mixins/ref-mixin.js.map +1 -0
  123. package/dist/runtime/navigation.d.ts +22 -0
  124. package/dist/runtime/navigation.js +102 -0
  125. package/dist/runtime/navigation.js.map +1 -0
  126. package/dist/runtime/reconcile.d.ts +11 -0
  127. package/dist/runtime/reconcile.js +1240 -0
  128. package/dist/runtime/reconcile.js.map +1 -0
  129. package/dist/runtime/render.d.ts +54 -0
  130. package/dist/runtime/render.js +50 -0
  131. package/dist/runtime/render.js.map +1 -0
  132. package/dist/runtime/run.d.ts +45 -0
  133. package/dist/runtime/run.js +68 -0
  134. package/dist/runtime/run.js.map +1 -0
  135. package/dist/runtime/scheduler.d.ts +30 -0
  136. package/dist/runtime/scheduler.js +185 -0
  137. package/dist/runtime/scheduler.js.map +1 -0
  138. package/dist/runtime/svg-attributes.d.ts +5 -0
  139. package/dist/runtime/svg-attributes.js +124 -0
  140. package/dist/runtime/svg-attributes.js.map +1 -0
  141. package/dist/runtime/to-vnode.d.ts +3 -0
  142. package/dist/runtime/to-vnode.js +43 -0
  143. package/dist/runtime/to-vnode.js.map +1 -0
  144. package/dist/runtime/typed-event-target.d.ts +50 -0
  145. package/dist/runtime/typed-event-target.js +6 -0
  146. package/dist/runtime/typed-event-target.js.map +1 -0
  147. package/dist/runtime/vdom.d.ts +55 -0
  148. package/dist/runtime/vdom.js +198 -0
  149. package/dist/runtime/vdom.js.map +1 -0
  150. package/dist/runtime/vnode.d.ts +79 -0
  151. package/dist/runtime/vnode.js +38 -0
  152. package/dist/runtime/vnode.js.map +1 -0
  153. package/dist/server/stream.d.ts +57 -0
  154. package/dist/server/stream.js +1007 -0
  155. package/dist/server/stream.js.map +1 -0
  156. package/dist/style/css-mixin.d.ts +8 -0
  157. package/dist/style/css-mixin.js +54 -0
  158. package/dist/style/css-mixin.js.map +1 -0
  159. package/dist/style/index.d.ts +5 -0
  160. package/dist/style/index.js +4 -0
  161. package/dist/style/index.js.map +1 -0
  162. package/dist/style/style.d.ts +24 -0
  163. package/dist/style/style.js +269 -0
  164. package/dist/style/style.js.map +1 -0
  165. package/dist/style/stylesheet.d.ts +11 -0
  166. package/dist/style/stylesheet.js +174 -0
  167. package/dist/style/stylesheet.js.map +1 -0
  168. package/dist/test.d.ts +2 -0
  169. package/dist/test.js +2 -0
  170. package/dist/test.js.map +1 -0
  171. package/dist/theme/contract.d.ts +259 -0
  172. package/dist/theme/contract.js +131 -0
  173. package/dist/theme/contract.js.map +1 -0
  174. package/dist/theme/glyph-contract.d.ts +13 -0
  175. package/dist/theme/glyph-contract.js +34 -0
  176. package/dist/theme/glyph-contract.js.map +1 -0
  177. package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
  178. package/dist/theme/presets/rmx-01/glyphs.js +252 -0
  179. package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
  180. package/dist/theme/presets/rmx-01/index.d.ts +2 -0
  181. package/dist/theme/presets/rmx-01/index.js +119 -0
  182. package/dist/theme/presets/rmx-01/index.js.map +1 -0
  183. package/dist/theme/runtime.d.ts +2 -0
  184. package/dist/theme/runtime.js +81 -0
  185. package/dist/theme/runtime.js.map +1 -0
  186. package/dist/theme/theme.d.ts +6 -0
  187. package/dist/theme/theme.js +5 -0
  188. package/dist/theme/theme.js.map +1 -0
  189. package/dist/utils/flash-attribute.d.ts +1 -0
  190. package/dist/utils/flash-attribute.js +11 -0
  191. package/dist/utils/flash-attribute.js.map +1 -0
  192. package/dist/utils/scroll-lock.d.ts +3 -0
  193. package/dist/utils/scroll-lock.js +69 -0
  194. package/dist/utils/scroll-lock.js.map +1 -0
  195. package/dist/utils/wait-for-css-transition.d.ts +1 -0
  196. package/dist/utils/wait-for-css-transition.js +58 -0
  197. package/dist/utils/wait-for-css-transition.js.map +1 -0
  198. package/dist/utils/wait.d.ts +1 -0
  199. package/dist/utils/wait.js +6 -0
  200. package/dist/utils/wait.js.map +1 -0
  201. package/package.json +125 -5
  202. package/src/animation/animate-layout-mixin.ts +266 -0
  203. package/src/animation/animate-mixins.ts +292 -0
  204. package/src/animation/index.ts +6 -0
  205. package/src/animation/spring.ts +299 -0
  206. package/src/animation/tween.ts +134 -0
  207. package/src/components/accordion/accordion.tsx +565 -0
  208. package/src/components/anchor/anchor.ts +904 -0
  209. package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
  210. package/src/components/button/README.md +44 -0
  211. package/src/components/button/button.tsx +188 -0
  212. package/src/components/combobox/README.md +145 -0
  213. package/src/components/combobox/combobox.tsx +1014 -0
  214. package/src/components/glyph/glyph.tsx +110 -0
  215. package/src/components/listbox/listbox.ts +447 -0
  216. package/src/components/menu/hover-aim.ts +428 -0
  217. package/src/components/menu/menu.tsx +1547 -0
  218. package/src/components/popover/README.md +122 -0
  219. package/src/components/popover/popover.ts +201 -0
  220. package/src/components/select/select.tsx +585 -0
  221. package/src/components/separator/separator.ts +19 -0
  222. package/src/components/tabs/README.md +105 -0
  223. package/src/components/tabs/tabs.tsx +499 -0
  224. package/src/index.ts +72 -0
  225. package/src/interactions/keydown/keydown.ts +14 -0
  226. package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
  227. package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
  228. package/src/jsx-runtime.ts +2 -0
  229. package/src/runtime/client-entries.ts +137 -0
  230. package/src/runtime/component.ts +402 -0
  231. package/src/runtime/create-element.ts +38 -0
  232. package/src/runtime/diff-dom.ts +404 -0
  233. package/src/runtime/diff-props.ts +209 -0
  234. package/src/runtime/document-state.ts +130 -0
  235. package/src/runtime/dom.ts +3971 -0
  236. package/src/runtime/error-event.ts +26 -0
  237. package/src/runtime/event-listeners.ts +171 -0
  238. package/src/runtime/frame.ts +1211 -0
  239. package/src/runtime/invariant.ts +31 -0
  240. package/src/runtime/jsx.ts +398 -0
  241. package/src/runtime/mixins/attrs-mixin.ts +35 -0
  242. package/src/runtime/mixins/link-mixin.ts +131 -0
  243. package/src/runtime/mixins/mixin.ts +908 -0
  244. package/src/runtime/mixins/on-mixin.ts +89 -0
  245. package/src/runtime/mixins/ref-mixin.ts +32 -0
  246. package/src/runtime/navigation.ts +136 -0
  247. package/src/runtime/reconcile.ts +1722 -0
  248. package/src/runtime/render.ts +101 -0
  249. package/src/runtime/run.ts +100 -0
  250. package/src/runtime/scheduler.ts +262 -0
  251. package/src/runtime/svg-attributes.ts +133 -0
  252. package/src/runtime/to-vnode.ts +50 -0
  253. package/src/runtime/typed-event-target.ts +67 -0
  254. package/src/runtime/vdom.ts +295 -0
  255. package/src/runtime/vnode.ts +137 -0
  256. package/src/server/stream.ts +1342 -0
  257. package/src/style/css-mixin.ts +82 -0
  258. package/src/style/index.ts +10 -0
  259. package/src/style/style.ts +312 -0
  260. package/src/style/stylesheet.ts +209 -0
  261. package/src/test/setup.ts +7 -0
  262. package/src/test/utils.ts +45 -0
  263. package/src/test.ts +2 -0
  264. package/src/theme/contract.ts +183 -0
  265. package/src/theme/glyph-contract.ts +57 -0
  266. package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
  267. package/src/theme/presets/rmx-01/index.ts +121 -0
  268. package/src/theme/runtime.ts +116 -0
  269. package/src/theme/theme.ts +15 -0
  270. package/src/utils/flash-attribute.ts +11 -0
  271. package/src/utils/scroll-lock.ts +97 -0
  272. package/src/utils/wait-for-css-transition.ts +66 -0
  273. package/src/utils/wait.ts +5 -0
@@ -0,0 +1,499 @@
1
+ // @jsxRuntime classic
2
+ // @jsx createElement
3
+ import {
4
+ attrs,
5
+ css,
6
+ createElement,
7
+ createMixin,
8
+ on,
9
+ type CSSMixinDescriptor,
10
+ type ElementProps,
11
+ type Handle,
12
+ type MixinHandle,
13
+ type Props,
14
+ type RemixNode,
15
+ } from '@remix-run/ui'
16
+ import * as button from '../button/button.tsx'
17
+ import { theme } from '../../theme/theme.ts'
18
+
19
+ const TABS_CHANGE_EVENT = 'rmx:tabs-change' as const
20
+
21
+ type TabsChangeHandler = (event: TabsChangeEvent, signal: AbortSignal) => void | Promise<void>
22
+
23
+ const tabsListCss: CSSMixinDescriptor = css({
24
+ display: 'inline-flex',
25
+ alignItems: 'center',
26
+ gap: theme.space.xs,
27
+ minWidth: 0,
28
+ padding: theme.space.xs,
29
+ border: `1px solid ${theme.colors.border.subtle}`,
30
+ borderRadius: theme.radius.xl,
31
+ backgroundColor: theme.surface.lvl2,
32
+ })
33
+
34
+ const tabsTriggerCss: CSSMixinDescriptor = css({
35
+ justifyContent: 'flex-start',
36
+ minHeight: theme.control.height.md,
37
+ paddingInline: theme.space.md,
38
+ border: '1px solid transparent',
39
+ borderRadius: theme.radius.md,
40
+ backgroundColor: 'transparent',
41
+ color: theme.colors.text.secondary,
42
+ fontSize: theme.fontSize.md,
43
+ '&:hover:not(:disabled):not([aria-disabled="true"])': {
44
+ color: theme.colors.text.primary,
45
+ },
46
+ '&:focus-visible': {
47
+ outline: `2px solid ${theme.colors.focus.ring}`,
48
+ outlineOffset: '2px',
49
+ color: theme.colors.text.primary,
50
+ },
51
+ '&[aria-selected="true"], &[aria-selected="true"]:hover, &[aria-selected="true"]:focus-visible': {
52
+ backgroundColor: theme.surface.lvl0,
53
+ borderColor: theme.colors.border.subtle,
54
+ boxShadow: theme.shadow.xs,
55
+ color: theme.colors.text.primary,
56
+ },
57
+ '&:disabled, &[aria-disabled="true"]': {
58
+ opacity: 0.55,
59
+ },
60
+ })
61
+
62
+ export const listStyle = tabsListCss
63
+ export const triggerStyle = tabsTriggerCss
64
+
65
+ const panelVisibilityUtility = css({
66
+ '&[hidden]': {
67
+ display: 'none',
68
+ },
69
+ })
70
+
71
+ declare global {
72
+ interface HTMLElementEventMap {
73
+ [TABS_CHANGE_EVENT]: TabsChangeEvent
74
+ }
75
+ }
76
+
77
+ export class TabsChangeEvent extends Event {
78
+ readonly previousValue: string | null
79
+ readonly value: string
80
+
81
+ constructor(value: string, previousValue: string | null) {
82
+ super(TABS_CHANGE_EVENT, { bubbles: true })
83
+ this.value = value
84
+ this.previousValue = previousValue
85
+ }
86
+ }
87
+
88
+ export type TabsOrientation = 'horizontal' | 'vertical'
89
+
90
+ type TabsDirection = 'first' | 'last' | 'next' | 'previous'
91
+
92
+ type RegisteredTab = {
93
+ disabled: boolean
94
+ getNode(): HTMLElement | null
95
+ value: string
96
+ }
97
+
98
+ export interface TabsRef {
99
+ readonly selectedValue: string | null
100
+ focus: (value?: string) => void
101
+ focusFirst: () => void
102
+ focusLast: () => void
103
+ select: (value: string) => void
104
+ }
105
+
106
+ interface TabsContextValue {
107
+ readonly focusableValue: string | null
108
+ readonly orientation: TabsOrientation
109
+ readonly value: string | null
110
+ getPanelId: (value: string) => string
111
+ getTriggerId: (value: string) => string
112
+ move: (fromValue: string, direction: TabsDirection) => void
113
+ registerTab: (tab: RegisteredTab) => void
114
+ select: (value: string) => void
115
+ }
116
+
117
+ export interface TabsContextProps {
118
+ children?: RemixNode
119
+ defaultValue?: string
120
+ onValueChange?: (value: string) => void
121
+ orientation?: TabsOrientation
122
+ ref?: (ref: TabsRef) => void
123
+ value?: string
124
+ }
125
+
126
+ export interface TabsProps extends TabsContextProps {}
127
+
128
+ export interface TabsTriggerOptions {
129
+ disabled?: boolean
130
+ value: string
131
+ }
132
+
133
+ export interface TabsPanelOptions {
134
+ value: string
135
+ }
136
+
137
+ export type TabsListProps = Props<'div'>
138
+
139
+ export type TabProps = Omit<Props<'button'>, 'type'> & {
140
+ children?: RemixNode
141
+ disabled?: boolean
142
+ type?: 'button' | 'submit' | 'reset'
143
+ value: string
144
+ }
145
+
146
+ export type TabsPanelProps = Props<'div'> & {
147
+ children?: RemixNode
148
+ value: string
149
+ }
150
+
151
+ function getTabsContext(handle: Handle | MixinHandle) {
152
+ return handle.context.get(TabsProvider)
153
+ }
154
+
155
+ function toIdFragment(value: string) {
156
+ let fragment = value.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '')
157
+ return fragment || 'tab'
158
+ }
159
+
160
+ function TabsProvider(handle: Handle<TabsContextProps, TabsContextValue>) {
161
+ let registeredTabs: RegisteredTab[] = []
162
+ let hasInitialized = false
163
+ let uncontrolledValue: string | null = null
164
+
165
+ function getValue() {
166
+ return handle.props.value !== undefined ? handle.props.value : uncontrolledValue
167
+ }
168
+
169
+ function getOrientation() {
170
+ return handle.props.orientation ?? 'horizontal'
171
+ }
172
+
173
+ function isActiveTab(tab: RegisteredTab | undefined): tab is RegisteredTab {
174
+ return !!tab && !tab.disabled && !!tab.getNode()?.isConnected
175
+ }
176
+
177
+ function getTab(value: string | null | undefined) {
178
+ if (value == null) {
179
+ return undefined
180
+ }
181
+
182
+ return registeredTabs.find((tab) => tab.value === value)
183
+ }
184
+
185
+ function getEnabledTabs() {
186
+ return registeredTabs.filter(isActiveTab)
187
+ }
188
+
189
+ function getFocusableValue() {
190
+ let currentValue = getValue()
191
+ let currentTab = getTab(currentValue)
192
+
193
+ if (isActiveTab(currentTab)) {
194
+ return currentValue
195
+ }
196
+
197
+ return getEnabledTabs()[0]?.value ?? null
198
+ }
199
+
200
+ function getTargetTab(fromValue: string, direction: TabsDirection) {
201
+ let enabledTabs = getEnabledTabs()
202
+ if (enabledTabs.length === 0) {
203
+ return undefined
204
+ }
205
+
206
+ let currentIndex = enabledTabs.findIndex((tab) => tab.value === fromValue)
207
+ let targetIndex = 0
208
+
209
+ switch (direction) {
210
+ case 'first':
211
+ targetIndex = 0
212
+ break
213
+ case 'last':
214
+ targetIndex = enabledTabs.length - 1
215
+ break
216
+ case 'next':
217
+ targetIndex =
218
+ currentIndex === -1 || currentIndex === enabledTabs.length - 1 ? 0 : currentIndex + 1
219
+ break
220
+ case 'previous':
221
+ targetIndex = currentIndex <= 0 ? enabledTabs.length - 1 : currentIndex - 1
222
+ break
223
+ }
224
+
225
+ return enabledTabs[targetIndex]
226
+ }
227
+
228
+ function dispatchChange(value: string, previousValue: string | null) {
229
+ getTab(value)?.getNode()?.dispatchEvent(new TabsChangeEvent(value, previousValue))
230
+ }
231
+
232
+ function select(value: string) {
233
+ let nextTab = getTab(value)
234
+ if (!isActiveTab(nextTab)) {
235
+ return
236
+ }
237
+
238
+ let previousValue = getValue()
239
+ if (previousValue === value) {
240
+ return
241
+ }
242
+
243
+ if (handle.props.value === undefined) {
244
+ uncontrolledValue = value
245
+ void handle.update()
246
+ }
247
+
248
+ handle.props.onValueChange?.(value)
249
+ dispatchChange(value, previousValue ?? null)
250
+ }
251
+
252
+ let tabsRef: TabsRef = {
253
+ get selectedValue() {
254
+ return getValue()
255
+ },
256
+ focus(value = getFocusableValue() ?? undefined) {
257
+ getTab(value)?.getNode()?.focus()
258
+ },
259
+ focusFirst() {
260
+ getTargetTab(getFocusableValue() ?? '', 'first')
261
+ ?.getNode()
262
+ ?.focus()
263
+ },
264
+ focusLast() {
265
+ getTargetTab(getFocusableValue() ?? '', 'last')
266
+ ?.getNode()
267
+ ?.focus()
268
+ },
269
+ select,
270
+ }
271
+
272
+ handle.context.set({
273
+ get focusableValue() {
274
+ return getFocusableValue()
275
+ },
276
+ get orientation() {
277
+ return getOrientation()
278
+ },
279
+ get value() {
280
+ return getValue()
281
+ },
282
+ getPanelId(value) {
283
+ return `${handle.id}-panel-${toIdFragment(value)}`
284
+ },
285
+ getTriggerId(value) {
286
+ return `${handle.id}-trigger-${toIdFragment(value)}`
287
+ },
288
+ move(fromValue, direction) {
289
+ let target = getTargetTab(fromValue, direction)
290
+ if (!target) {
291
+ return
292
+ }
293
+
294
+ target.getNode()?.focus()
295
+ select(target.value)
296
+ },
297
+ registerTab(tab) {
298
+ registeredTabs.push(tab)
299
+ },
300
+ select,
301
+ })
302
+
303
+ return () => {
304
+ registeredTabs = []
305
+
306
+ if (!hasInitialized) {
307
+ uncontrolledValue = handle.props.defaultValue ?? null
308
+ hasInitialized = true
309
+ }
310
+
311
+ handle.queueTask(() => {
312
+ handle.props.ref?.(tabsRef)
313
+
314
+ if (handle.props.value !== undefined) {
315
+ return
316
+ }
317
+
318
+ let nextValue = getFocusableValue()
319
+ if (nextValue === uncontrolledValue) {
320
+ return
321
+ }
322
+
323
+ uncontrolledValue = nextValue
324
+ void handle.update()
325
+ })
326
+
327
+ return handle.props.children
328
+ }
329
+ }
330
+
331
+ const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
332
+ let context = getTabsContext(handle)
333
+
334
+ return () =>
335
+ attrs({
336
+ role: 'tablist',
337
+ 'aria-orientation': context.orientation === 'vertical' ? 'vertical' : undefined,
338
+ 'data-orientation': context.orientation,
339
+ })
340
+ })
341
+
342
+ const triggerMixin = createMixin<HTMLElement, [options: TabsTriggerOptions], ElementProps>(
343
+ (handle) => {
344
+ let triggerRef: HTMLElement | null = null
345
+ let context = getTabsContext(handle)
346
+
347
+ handle.queueTask((node) => {
348
+ triggerRef = node
349
+ })
350
+
351
+ return (options, props) => {
352
+ let disabled = options.disabled === true || props.disabled === true
353
+ context.registerTab({
354
+ disabled,
355
+ getNode() {
356
+ return triggerRef
357
+ },
358
+ value: options.value,
359
+ })
360
+
361
+ return [
362
+ attrs({
363
+ id: context.getTriggerId(options.value),
364
+ role: 'tab',
365
+ tabIndex: !disabled && context.focusableValue === options.value ? 0 : -1,
366
+ 'aria-controls': context.getPanelId(options.value),
367
+ 'aria-disabled': disabled ? 'true' : undefined,
368
+ 'aria-selected': context.value === options.value ? 'true' : 'false',
369
+ 'data-orientation': context.orientation,
370
+ 'data-selected': context.value === options.value ? 'true' : 'false',
371
+ }),
372
+ !disabled && [
373
+ on('click', () => {
374
+ context.select(options.value)
375
+ }),
376
+ on('keydown', (event) => {
377
+ switch (event.key) {
378
+ case 'ArrowRight':
379
+ if (context.orientation !== 'horizontal') {
380
+ return
381
+ }
382
+ event.preventDefault()
383
+ context.move(options.value, 'next')
384
+ break
385
+ case 'ArrowLeft':
386
+ if (context.orientation !== 'horizontal') {
387
+ return
388
+ }
389
+ event.preventDefault()
390
+ context.move(options.value, 'previous')
391
+ break
392
+ case 'ArrowDown':
393
+ if (context.orientation !== 'vertical') {
394
+ return
395
+ }
396
+ event.preventDefault()
397
+ context.move(options.value, 'next')
398
+ break
399
+ case 'ArrowUp':
400
+ if (context.orientation !== 'vertical') {
401
+ return
402
+ }
403
+ event.preventDefault()
404
+ context.move(options.value, 'previous')
405
+ break
406
+ case 'Home':
407
+ event.preventDefault()
408
+ context.move(options.value, 'first')
409
+ break
410
+ case 'End':
411
+ event.preventDefault()
412
+ context.move(options.value, 'last')
413
+ break
414
+ }
415
+ }),
416
+ ],
417
+ ]
418
+ }
419
+ },
420
+ )
421
+
422
+ const panelMixin = createMixin<HTMLElement, [options: TabsPanelOptions], ElementProps>((handle) => {
423
+ let context = getTabsContext(handle)
424
+
425
+ return (options) => {
426
+ let isSelected = context.value === options.value
427
+
428
+ return [
429
+ attrs({
430
+ id: context.getPanelId(options.value),
431
+ role: 'tabpanel',
432
+ hidden: isSelected ? undefined : true,
433
+ 'aria-labelledby': context.getTriggerId(options.value),
434
+ 'data-orientation': context.orientation,
435
+ 'data-selected': isSelected ? 'true' : 'false',
436
+ }),
437
+ panelVisibilityUtility,
438
+ ]
439
+ }
440
+ })
441
+
442
+ export const Context = TabsProvider
443
+ export const list = listMixin
444
+ export const panel = panelMixin
445
+ export const trigger = triggerMixin
446
+
447
+ const tabs = { Context, list, panel, trigger } as const
448
+
449
+ export function onTabsChange(handler: TabsChangeHandler, captureBoolean?: boolean) {
450
+ return on<HTMLElement, typeof TABS_CHANGE_EVENT>(TABS_CHANGE_EVENT, handler, captureBoolean)
451
+ }
452
+
453
+ export function Tabs(handle: Handle<TabsProps>) {
454
+ return () => {
455
+ let { children, ...contextProps } = handle.props
456
+ return <tabs.Context {...contextProps}>{children}</tabs.Context>
457
+ }
458
+ }
459
+
460
+ export function TabsList(handle: Handle<TabsListProps>) {
461
+ return () => {
462
+ let { children, mix, ...divProps } = handle.props
463
+
464
+ return (
465
+ <div {...divProps} mix={[listStyle, tabs.list(), mix]}>
466
+ {children}
467
+ </div>
468
+ )
469
+ }
470
+ }
471
+
472
+ export function Tab(handle: Handle<TabProps>) {
473
+ return () => {
474
+ let { children, disabled, mix, type, value, ...buttonProps } = handle.props
475
+
476
+ return (
477
+ <button
478
+ {...buttonProps}
479
+ disabled={disabled ? true : undefined}
480
+ mix={[button.baseStyle, triggerStyle, tabs.trigger({ disabled, value }), mix]}
481
+ type={type ?? 'button'}
482
+ >
483
+ {children}
484
+ </button>
485
+ )
486
+ }
487
+ }
488
+
489
+ export function TabsPanel(handle: Handle<TabsPanelProps>) {
490
+ return () => {
491
+ let { children, mix, value, ...divProps } = handle.props
492
+
493
+ return (
494
+ <div {...divProps} mix={[tabs.panel({ value }), mix]}>
495
+ {children}
496
+ </div>
497
+ )
498
+ }
499
+ }
package/src/index.ts ADDED
@@ -0,0 +1,72 @@
1
+ /// <reference types="dom-navigation" preserve="true" />
2
+
3
+ // -- Roots --
4
+ export { run } from './runtime/run.ts'
5
+ export type { AppRuntime, AppRuntimeEventMap, RunInit } from './runtime/run.ts'
6
+ export type { ComponentErrorEvent } from './runtime/error-event.ts'
7
+
8
+ export { createRoot, createRangeRoot, createScheduler } from './runtime/vdom.ts'
9
+ export type {
10
+ VirtualRoot,
11
+ VirtualRootEventMap,
12
+ VirtualRootOptions,
13
+ Scheduler,
14
+ } from './runtime/vdom.ts'
15
+
16
+ // -- Client Entries --
17
+ export { clientEntry } from './runtime/client-entries.ts'
18
+ export type {
19
+ SerializablePrimitive,
20
+ SerializableObject,
21
+ SerializableArray,
22
+ SerializableValue,
23
+ SerializableProps,
24
+ EntryComponent,
25
+ } from './runtime/client-entries.ts'
26
+
27
+ // -- Components --
28
+ export { Fragment, Frame } from './runtime/component.ts'
29
+ export type {
30
+ Task,
31
+ Handle,
32
+ Context,
33
+ FrameHandleEventMap,
34
+ FrameContent,
35
+ FrameHandle,
36
+ FrameProps,
37
+ } from './runtime/component.ts'
38
+ export type { LoadModule, ResolveFrame } from './runtime/frame.ts'
39
+
40
+ // -- Elements/JSX/Props --
41
+ export { createElement } from './runtime/create-element.ts'
42
+ export type {
43
+ ElementType,
44
+ ElementProps,
45
+ RemixElement,
46
+ Renderable,
47
+ RemixNode,
48
+ Props,
49
+ } from './runtime/jsx.ts'
50
+ export type { HostProps, LayoutAnimationConfig } from './runtime/dom.ts'
51
+ export { createMixin } from './runtime/mixins/mixin.ts'
52
+ export type {
53
+ MixinDescriptor,
54
+ MixinHandle,
55
+ MixinType,
56
+ MixInput,
57
+ MixValue,
58
+ } from './runtime/mixins/mixin.ts'
59
+ export { TypedEventTarget } from './runtime/typed-event-target.ts'
60
+ export { addEventListeners } from './runtime/event-listeners.ts'
61
+ export { on } from './runtime/mixins/on-mixin.ts'
62
+ export type { Dispatched } from './runtime/mixins/on-mixin.ts'
63
+ export { link } from './runtime/mixins/link-mixin.ts'
64
+ export { ref } from './runtime/mixins/ref-mixin.ts'
65
+ export type { RefCallback } from './runtime/mixins/ref-mixin.ts'
66
+ export { attrs } from './runtime/mixins/attrs-mixin.ts'
67
+ export { css } from './style/css-mixin.ts'
68
+ export type { CSSMixinDescriptor } from './style/css-mixin.ts'
69
+
70
+ // -- Navigation --
71
+ export { navigate } from './runtime/navigation.ts'
72
+ export type { NavigationOptions } from './runtime/navigation.ts'
@@ -0,0 +1,14 @@
1
+ import { createMixin, on } from '@remix-run/ui'
2
+
3
+ export const onKeyDown = createMixin<
4
+ HTMLElement,
5
+ [key: string, handler: (event: KeyboardEvent) => void]
6
+ >(
7
+ () => (key, handler) =>
8
+ on('keydown', (event) => {
9
+ if (event.key === key) {
10
+ event.preventDefault()
11
+ handler(event)
12
+ }
13
+ }),
14
+ )
@@ -0,0 +1,55 @@
1
+ import { createMixin } from '@remix-run/ui'
2
+
3
+ type InsideTargetMatcher = (target: Node) => boolean
4
+
5
+ const onOutsideClick = createMixin<
6
+ HTMLElement,
7
+ [
8
+ active: boolean,
9
+ handler: (target: Node | null) => void,
10
+ isInsideTarget?: InsideTargetMatcher,
11
+ stopPropagation?: boolean,
12
+ ]
13
+ >((handle) => {
14
+ let active = false
15
+ let handler: (target: Node | null) => void = () => {}
16
+ let isInsideTarget: InsideTargetMatcher = () => false
17
+ let stopPropagation = true
18
+
19
+ handle.addEventListener('insert', (event) => {
20
+ let node = event.node
21
+ let doc = node.ownerDocument
22
+
23
+ doc.addEventListener(
24
+ 'click',
25
+ (event) => {
26
+ let target = event.target instanceof Node ? event.target : null
27
+
28
+ if (!active || (target && (node.contains(target) || isInsideTarget(target)))) {
29
+ return
30
+ }
31
+
32
+ if (stopPropagation) {
33
+ event.stopPropagation()
34
+ }
35
+
36
+ handler(target)
37
+ },
38
+ { capture: true, signal: handle.signal },
39
+ )
40
+ })
41
+
42
+ return (
43
+ nextActive,
44
+ nextHandler,
45
+ nextIsInsideTarget = () => false,
46
+ nextStopPropagation = true,
47
+ ) => {
48
+ active = nextActive
49
+ handler = nextHandler
50
+ isInsideTarget = nextIsInsideTarget
51
+ stopPropagation = nextStopPropagation
52
+ }
53
+ })
54
+
55
+ export { onOutsideClick }