@remix-run/ui 0.4.0 → 0.5.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.
- package/README.md +74 -1
- package/dist/accordion/index.js +2 -2
- package/dist/accordion/primitives.d.ts +1 -1
- package/dist/accordion/primitives.js.map +1 -1
- package/dist/anchor/index.js.map +1 -1
- package/dist/animation/animate-layout-mixin.js +1 -1
- package/dist/animation/animate-layout-mixin.js.map +1 -1
- package/dist/animation/animate-mixins.js +2 -2
- package/dist/animation/animate-mixins.js.map +1 -1
- package/dist/animation/demos/color-interpolation.js +1 -1
- package/dist/animation/demos/color-interpolation.js.map +1 -1
- package/dist/animation/demos/drag-release.d.ts +1 -1
- package/dist/animation/demos/hold-to-confirm.d.ts +3 -3
- package/dist/animation/demos/material-ripple.js +1 -3
- package/dist/animation/demos/material-ripple.js.map +1 -1
- package/dist/animation/index.js +4 -4
- package/dist/animation/spring.js.map +1 -1
- package/dist/breadcrumbs/index.js +2 -2
- package/dist/button/index.js +25 -25
- package/dist/button/index.js.map +1 -1
- package/dist/checkbox/index.js +11 -9
- package/dist/checkbox/index.js.map +1 -1
- package/dist/combobox/index.js +4 -4
- package/dist/combobox/index.js.map +1 -1
- package/dist/combobox/primitives.d.ts +1 -1
- package/dist/combobox/primitives.js +5 -5
- package/dist/combobox/primitives.js.map +1 -1
- package/dist/dev/refresh.d.ts +1 -0
- package/dist/dev/refresh.js +2 -0
- package/dist/dev/refresh.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +14 -14
- package/dist/input/index.js +9 -9
- package/dist/input/index.js.map +1 -1
- package/dist/jsx-runtime.js +2 -2
- package/dist/listbox/index.js +2 -2
- package/dist/menu/hover-aim.js.map +1 -1
- package/dist/menu/index.js +3 -3
- package/dist/menu/primitives.d.ts +1 -1
- package/dist/menu/primitives.js +6 -6
- package/dist/menu/primitives.js.map +1 -1
- package/dist/popover/index.js +3 -3
- package/dist/popover/scroll-lock.js.map +1 -1
- package/dist/radio/index.js +7 -7
- package/dist/radio/index.js.map +1 -1
- package/dist/runtime/client-entry-boundary.d.ts +14 -0
- package/dist/runtime/client-entry-boundary.js +29 -0
- package/dist/runtime/client-entry-boundary.js.map +1 -0
- package/dist/runtime/component.d.ts +19 -4
- package/dist/runtime/component.js +39 -9
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/core/attributes.js +36 -3
- package/dist/runtime/core/attributes.js.map +1 -1
- package/dist/runtime/core/children.d.ts +2 -0
- package/dist/runtime/core/children.js +26 -2
- package/dist/runtime/core/children.js.map +1 -1
- package/dist/runtime/core/props.js +2 -2
- package/dist/runtime/core/vnode.d.ts +3 -3
- package/dist/runtime/core/vnode.js +22 -1
- package/dist/runtime/core/vnode.js.map +1 -1
- package/dist/runtime/create-element.d.ts +2 -2
- package/dist/runtime/create-element.js +3 -2
- package/dist/runtime/create-element.js.map +1 -1
- package/dist/runtime/diff-dom.js +294 -219
- package/dist/runtime/diff-dom.js.map +1 -1
- package/dist/runtime/diff-props.js +3 -3
- package/dist/runtime/dom.d.ts +12 -0
- package/dist/runtime/element-function.d.ts +3 -0
- package/dist/runtime/element-function.js +2 -0
- package/dist/runtime/element-function.js.map +1 -0
- package/dist/runtime/form-navigation.d.ts +25 -0
- package/dist/runtime/form-navigation.js +132 -0
- package/dist/runtime/form-navigation.js.map +1 -0
- package/dist/runtime/frame-resolution.d.ts +5 -0
- package/dist/runtime/frame-resolution.js +10 -0
- package/dist/runtime/frame-resolution.js.map +1 -0
- package/dist/runtime/frame.d.ts +58 -15
- package/dist/runtime/frame.js +367 -114
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/jsx.d.ts +1 -1
- package/dist/runtime/jsx.js +1 -1
- package/dist/runtime/jsx.js.map +1 -1
- package/dist/runtime/key.d.ts +1 -0
- package/dist/runtime/key.js +2 -0
- package/dist/runtime/key.js.map +1 -0
- package/dist/runtime/mixins/attrs-mixin.js +2 -2
- package/dist/runtime/mixins/link-mixin.js +4 -3
- package/dist/runtime/mixins/link-mixin.js.map +1 -1
- package/dist/runtime/mixins/mixin.d.ts +15 -10
- package/dist/runtime/mixins/mixin.js +4 -4
- package/dist/runtime/mixins/mixin.js.map +1 -1
- package/dist/runtime/mixins/on-mixin.js +1 -1
- package/dist/runtime/mixins/ref-mixin.js +1 -1
- package/dist/runtime/navigation.d.ts +4 -1
- package/dist/runtime/navigation.js +157 -32
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/reconcile.d.ts +11 -10
- package/dist/runtime/reconcile.js +549 -421
- package/dist/runtime/reconcile.js.map +1 -1
- package/dist/runtime/refresh.d.ts +28 -0
- package/dist/runtime/refresh.js +31 -0
- package/dist/runtime/refresh.js.map +1 -0
- package/dist/runtime/render.js +1 -1
- package/dist/runtime/render.js.map +1 -1
- package/dist/runtime/run.d.ts +3 -1
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/scheduler.js +8 -16
- package/dist/runtime/scheduler.js.map +1 -1
- package/dist/runtime/to-vnode.d.ts +2 -2
- package/dist/runtime/to-vnode.js +68 -20
- package/dist/runtime/to-vnode.js.map +1 -1
- package/dist/runtime/vdom.d.ts +4 -2
- package/dist/runtime/vdom.js +87 -42
- package/dist/runtime/vdom.js.map +1 -1
- package/dist/runtime/vnode.d.ts +138 -67
- package/dist/runtime/vnode.js +17 -12
- package/dist/runtime/vnode.js.map +1 -1
- package/dist/select/index.js +11 -11
- package/dist/select/index.js.map +1 -1
- package/dist/select/primitives.d.ts +1 -1
- package/dist/select/primitives.js +6 -6
- package/dist/server/stream.js +20 -27
- package/dist/server/stream.js.map +1 -1
- package/dist/shared/focus-styles.d.ts +1 -1
- package/dist/shared/focus-styles.js +1 -1
- package/dist/shared/focus-styles.js.map +1 -1
- package/dist/shared/listbox-popover-styles.js +1 -1
- package/dist/shared/style-values.js +26 -26
- package/dist/shared/style-values.js.map +1 -1
- package/dist/style/css-mixin.js +3 -3
- package/dist/style/index.js +2 -2
- package/dist/style/style.js +25 -6
- package/dist/style/style.js.map +1 -1
- package/dist/style/stylesheet.d.ts +0 -1
- package/dist/style/stylesheet.js +13 -54
- package/dist/style/stylesheet.js.map +1 -1
- package/dist/tabs/index.js +11 -11
- package/dist/tabs/index.js.map +1 -1
- package/dist/tabs/primitives.d.ts +1 -1
- package/dist/test.js +1 -1
- package/dist/toggle/index.js +9 -9
- package/dist/toggle/index.js.map +1 -1
- package/dist/toggle/primitives.d.ts +1 -1
- package/package.json +10 -7
- package/src/animation/animate-mixins.ts +3 -3
- package/src/animation/demos/color-interpolation.tsx +1 -1
- package/src/button/index.ts +24 -24
- package/src/checkbox/index.ts +10 -8
- package/src/combobox/index.tsx +1 -1
- package/src/dev/refresh.ts +1 -0
- package/src/index.ts +1 -1
- package/src/input/index.ts +8 -8
- package/src/radio/index.ts +6 -5
- package/src/runtime/client-entry-boundary.ts +54 -0
- package/src/runtime/component.ts +65 -16
- package/src/runtime/core/attributes.ts +35 -1
- package/src/runtime/core/children.ts +24 -2
- package/src/runtime/core/vnode.ts +25 -4
- package/src/runtime/create-element.ts +6 -5
- package/src/runtime/demos/readme.demo.tsx +4 -4
- package/src/runtime/diff-dom.ts +358 -243
- package/src/runtime/dom.ts +331 -214
- package/src/runtime/element-function.ts +1 -0
- package/src/runtime/event-listeners.ts +1 -1
- package/src/runtime/form-navigation.ts +184 -0
- package/src/runtime/frame-resolution.ts +13 -0
- package/src/runtime/frame.ts +482 -124
- package/src/runtime/jsx.ts +3 -5
- package/src/runtime/key.ts +1 -0
- package/src/runtime/mixins/link-mixin.ts +1 -0
- package/src/runtime/mixins/mixin.ts +21 -18
- package/src/runtime/navigation.ts +204 -30
- package/src/runtime/reconcile.ts +762 -759
- package/src/runtime/refresh.ts +46 -0
- package/src/runtime/run.ts +12 -2
- package/src/runtime/scheduler.ts +9 -29
- package/src/runtime/to-vnode.ts +85 -23
- package/src/runtime/vdom.ts +89 -70
- package/src/runtime/vnode.ts +192 -88
- package/src/select/index.tsx +9 -9
- package/src/server/stream.ts +22 -28
- package/src/shared/focus-styles.ts +1 -1
- package/src/shared/style-values.ts +26 -26
- package/src/style/style.ts +25 -6
- package/src/style/stylesheet.ts +32 -57
- package/src/tabs/index.tsx +12 -11
- package/src/test/README.md +151 -60
- package/src/toggle/index.tsx +9 -7
package/src/style/stylesheet.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { REMIX_UI_STYLE_LAYER } from './layers.ts'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// # Style ownership model
|
|
4
|
+
//
|
|
5
|
+
// Rules are content-addressed: a class name is a hash of the style object that
|
|
6
|
+
// produced it, so `.rmxc-x` always means the same declarations. A rule can
|
|
7
|
+
// never become *wrong* — only unused. That observation drives a two-tier
|
|
8
|
+
// lifetime model on a single document-level registry:
|
|
9
|
+
//
|
|
10
|
+
// - **Server-adopted rules are pinned.** Once a `<style data-rmx>` tag is
|
|
11
|
+
// adopted, its rule stays for the life of the manager. Frame reloads,
|
|
12
|
+
// island hydration, and streamed templates never need to agree on which
|
|
13
|
+
// scope "owns" a shared rule — adoption is additive and idempotent, and the
|
|
14
|
+
// registry is bounded by the set of unique style objects the server ever
|
|
15
|
+
// renders, not by render count.
|
|
16
|
+
//
|
|
17
|
+
// - **Client-inserted rules are refcounted.** Dynamic style objects (e.g.
|
|
18
|
+
// interpolated values) mint a new class per distinct value, so rules that
|
|
19
|
+
// only ever existed client-side are dropped when the last css mixin using
|
|
20
|
+
// them releases its ref. A refcounted rule upgrades to pinned if a server
|
|
21
|
+
// tag for the same selector is adopted later.
|
|
22
|
+
|
|
23
|
+
type RuleEntry = { count: number; index: number; pinned: boolean }
|
|
4
24
|
export type ServerStyleSource = ParentNode | Iterable<Node>
|
|
5
25
|
|
|
6
26
|
export interface StyleManager {
|
|
@@ -10,7 +30,6 @@ export interface StyleManager {
|
|
|
10
30
|
getGeneration(): number
|
|
11
31
|
reset(): void
|
|
12
32
|
adoptServerStyles(source: ServerStyleSource): Set<string>
|
|
13
|
-
replaceServerStyles(source: ServerStyleSource): void
|
|
14
33
|
selectors(): IterableIterator<string>
|
|
15
34
|
dispose(): void
|
|
16
35
|
}
|
|
@@ -84,17 +103,8 @@ export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleM
|
|
|
84
103
|
let stylesheet: CSSStyleSheet | null = null
|
|
85
104
|
let generation = 0
|
|
86
105
|
|
|
87
|
-
// Track usage count and rule index per className
|
|
88
|
-
// Using an object to track both count and index together
|
|
89
106
|
let ruleMap = new Map<string, RuleEntry>()
|
|
90
107
|
|
|
91
|
-
// Selectors currently held by a server-style adoption ref. We track this
|
|
92
|
-
// separately from `ruleMap` so `replaceServerStyles` can release only the
|
|
93
|
-
// adoption refs of selectors absent from the next page, without disturbing
|
|
94
|
-
// selectors that exist solely because a client-side css mixin inserted them
|
|
95
|
-
// (e.g. transient UI state that the server never rendered).
|
|
96
|
-
let adoptedSelectors = new Set<string>()
|
|
97
|
-
|
|
98
108
|
function getStylesheet(): CSSStyleSheet {
|
|
99
109
|
if (!stylesheet) {
|
|
100
110
|
stylesheet = new CSSStyleSheet()
|
|
@@ -122,15 +132,11 @@ export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleM
|
|
|
122
132
|
let selector = getStyleSelector(styleEl)
|
|
123
133
|
if (!selector) return undefined
|
|
124
134
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
let entry = ruleMap.get(selector)!
|
|
131
|
-
entry.count++
|
|
132
|
-
adoptedSelectors.add(selector)
|
|
133
|
-
}
|
|
135
|
+
let entry = ruleMap.get(selector)
|
|
136
|
+
if (entry) {
|
|
137
|
+
// The rule already exists — either a previous adoption or a client-side
|
|
138
|
+
// css mixin inserted it first. Pin it so it outlives any mixin refs.
|
|
139
|
+
entry.pinned = true
|
|
134
140
|
styleEl.remove()
|
|
135
141
|
return selector
|
|
136
142
|
}
|
|
@@ -145,8 +151,7 @@ export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleM
|
|
|
145
151
|
let sheet = getStylesheet()
|
|
146
152
|
let index = sheet.cssRules.length
|
|
147
153
|
sheet.insertRule(cssText, index)
|
|
148
|
-
ruleMap.set(selector, { count:
|
|
149
|
-
adoptedSelectors.add(selector)
|
|
154
|
+
ruleMap.set(selector, { count: 0, index, pinned: true })
|
|
150
155
|
styleEl.remove()
|
|
151
156
|
return selector
|
|
152
157
|
} catch {
|
|
@@ -157,7 +162,7 @@ export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleM
|
|
|
157
162
|
|
|
158
163
|
function has(className: string) {
|
|
159
164
|
let entry = ruleMap.get(className)
|
|
160
|
-
return entry !== undefined && entry.count > 0
|
|
165
|
+
return entry !== undefined && (entry.pinned || entry.count > 0)
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
function getGeneration() {
|
|
@@ -168,8 +173,8 @@ export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleM
|
|
|
168
173
|
let entry = ruleMap.get(className)
|
|
169
174
|
|
|
170
175
|
if (entry) {
|
|
171
|
-
//
|
|
172
|
-
entry.count++
|
|
176
|
+
// Pinned rules are permanent; there is nothing to count.
|
|
177
|
+
if (!entry.pinned) entry.count++
|
|
173
178
|
return
|
|
174
179
|
}
|
|
175
180
|
|
|
@@ -179,13 +184,13 @@ export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleM
|
|
|
179
184
|
// This may throw for invalid CSS. If it does, we intentionally let it
|
|
180
185
|
// bubble so the rule is not tracked unless insertion actually succeeds.
|
|
181
186
|
sheet.insertRule(`@layer ${getStyleLayerName(className, layer)} { ${rule} }`, index)
|
|
182
|
-
ruleMap.set(className, { count: 1, index })
|
|
187
|
+
ruleMap.set(className, { count: 1, index, pinned: false })
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
function remove(className: string) {
|
|
186
191
|
let entry = ruleMap.get(className)
|
|
187
192
|
|
|
188
|
-
if (!entry) return
|
|
193
|
+
if (!entry || entry.pinned) return
|
|
189
194
|
|
|
190
195
|
// Decrement count
|
|
191
196
|
entry.count--
|
|
@@ -200,9 +205,7 @@ export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleM
|
|
|
200
205
|
|
|
201
206
|
// Remove from tracking
|
|
202
207
|
ruleMap.delete(className)
|
|
203
|
-
adoptedSelectors.delete(className)
|
|
204
208
|
|
|
205
|
-
// TODO: just search and remove, stop re-indexing
|
|
206
209
|
if (!stylesheet) return
|
|
207
210
|
stylesheet.deleteRule(indexToDelete)
|
|
208
211
|
|
|
@@ -218,7 +221,6 @@ export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleM
|
|
|
218
221
|
function reset() {
|
|
219
222
|
clearStylesheet()
|
|
220
223
|
ruleMap.clear()
|
|
221
|
-
adoptedSelectors.clear()
|
|
222
224
|
removeStylesheet()
|
|
223
225
|
generation++
|
|
224
226
|
}
|
|
@@ -235,31 +237,6 @@ export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleM
|
|
|
235
237
|
return adopted
|
|
236
238
|
}
|
|
237
239
|
|
|
238
|
-
// Snapshot the currently-adopted server selectors, adopt the incoming
|
|
239
|
-
// server styles additively, then release the adoption ref of any prior-only
|
|
240
|
-
// selectors.
|
|
241
|
-
//
|
|
242
|
-
// Refcount-aware semantics keep preserved DOM styled: a prior-only selector
|
|
243
|
-
// with no live css-mixin ref drops immediately, but one still referenced by
|
|
244
|
-
// an active mixin (e.g. inside a hydration region that the DOM diff skipped)
|
|
245
|
-
// stays in the stylesheet and is only fully removed when that mixin's
|
|
246
|
-
// `remove` event eventually fires. This avoids the FOUC that a hard
|
|
247
|
-
// `reset()` would cause between the style swap and the hydration re-render.
|
|
248
|
-
//
|
|
249
|
-
// Only adoption refs are considered — selectors that exist solely because a
|
|
250
|
-
// client-side css mixin inserted them (e.g. transient UI state never
|
|
251
|
-
// rendered by the server) are unaffected.
|
|
252
|
-
function replaceServerStyles(source: ServerStyleSource): void {
|
|
253
|
-
let prior = new Set(adoptedSelectors)
|
|
254
|
-
let adopted = adoptServerStyles(source)
|
|
255
|
-
for (let selector of prior) {
|
|
256
|
-
if (!adopted.has(selector)) {
|
|
257
|
-
adoptedSelectors.delete(selector)
|
|
258
|
-
remove(selector)
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
240
|
function selectors(): IterableIterator<string> {
|
|
264
241
|
return ruleMap.keys()
|
|
265
242
|
}
|
|
@@ -268,7 +245,6 @@ export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleM
|
|
|
268
245
|
removeStylesheet()
|
|
269
246
|
// Clear internal state
|
|
270
247
|
ruleMap.clear()
|
|
271
|
-
adoptedSelectors.clear()
|
|
272
248
|
generation++
|
|
273
249
|
}
|
|
274
250
|
|
|
@@ -279,7 +255,6 @@ export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleM
|
|
|
279
255
|
getGeneration,
|
|
280
256
|
reset,
|
|
281
257
|
adoptServerStyles,
|
|
282
|
-
replaceServerStyles,
|
|
283
258
|
selectors,
|
|
284
259
|
dispose,
|
|
285
260
|
}
|
package/src/tabs/index.tsx
CHANGED
|
@@ -37,9 +37,9 @@ export interface TabPanelProps extends Omit<Props<'div'>, 'children'> {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const tabSliderBackground =
|
|
40
|
-
'linear-gradient(180deg, rgba(0, 0, 0, 0) 33%, rgba(0, 0, 0, 0.04) 100%), #FFFFFF'
|
|
40
|
+
'linear-gradient(180deg, rgba(0, 0, 0, 0) 33%, light-dark(rgba(0, 0, 0, 0.04), rgba(255, 255, 255, 0.08)) 100%), light-dark(#FFFFFF, #1a1a1a)'
|
|
41
41
|
const tabSliderShadow =
|
|
42
|
-
'0 0 0 0.5px rgba(0, 0, 0, 0.06), 0 1px 1px -0.5px rgba(0, 0, 0, 0.12), 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 4px 4px -2px rgba(0, 0, 0, 0.12), inset 0 0 2px 1px #FFFFFF'
|
|
42
|
+
'0 0 0 0.5px light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.12)), 0 1px 1px -0.5px rgba(0, 0, 0, 0.12), 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 4px 4px -2px rgba(0, 0, 0, 0.12), inset 0 0 2px 1px light-dark(#FFFFFF, rgba(255, 255, 255, 0.08))'
|
|
43
43
|
|
|
44
44
|
const tabsRootCss: CSSMixinDescriptor = css({
|
|
45
45
|
'--rmx-tabs-height': '32px',
|
|
@@ -66,7 +66,8 @@ const tabsListCss: CSSMixinDescriptor = css({
|
|
|
66
66
|
padding: 'var(--rmx-tabs-track-padding)',
|
|
67
67
|
border: 0,
|
|
68
68
|
borderRadius: '9999px',
|
|
69
|
-
background:
|
|
69
|
+
background:
|
|
70
|
+
'linear-gradient(180deg, light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.08)) 0%, rgba(0, 0, 0, 0) 100%), light-dark(#EBEBEB, #2c2c2c)',
|
|
70
71
|
boxShadow:
|
|
71
72
|
'inset 0 0 4px 1px rgba(0, 0, 0, 0.08), inset 0 1px 1px rgba(0, 0, 0, 0.02), inset 0 2px 2px rgba(0, 0, 0, 0.02)',
|
|
72
73
|
overflowX: 'auto',
|
|
@@ -81,7 +82,7 @@ const tabsListCss: CSSMixinDescriptor = css({
|
|
|
81
82
|
|
|
82
83
|
const tabsTabCss: CSSMixinDescriptor = css({
|
|
83
84
|
'--rmx-tabs-tab-focus-shadow':
|
|
84
|
-
'0 0 0 1px #3573F6, var(--rmx-tabs-tab-shadow), 0 0 0 4px rgba(53, 115, 246, 0.1), 0 6px 32px 4px rgba(53, 115, 246, 0.08), inset 0 0 8px 1px rgba(53, 115, 246, 0.05)',
|
|
85
|
+
'0 0 0 1px light-dark(#3573F6, #6eaaff), var(--rmx-tabs-tab-shadow), 0 0 0 4px light-dark(rgba(53, 115, 246, 0.1), rgba(110, 170, 255, 0.18)), 0 6px 32px 4px light-dark(rgba(53, 115, 246, 0.08), rgba(110, 170, 255, 0.14)), inset 0 0 8px 1px light-dark(rgba(53, 115, 246, 0.05), rgba(110, 170, 255, 0.1))',
|
|
85
86
|
appearance: 'none',
|
|
86
87
|
margin: 0,
|
|
87
88
|
boxSizing: 'border-box',
|
|
@@ -103,7 +104,7 @@ const tabsTabCss: CSSMixinDescriptor = css({
|
|
|
103
104
|
borderRadius: '999px',
|
|
104
105
|
background: 'transparent',
|
|
105
106
|
boxShadow: 'var(--rmx-tabs-tab-shadow)',
|
|
106
|
-
color: '#707070',
|
|
107
|
+
color: 'light-dark(#707070, #b3b3b3)',
|
|
107
108
|
fontFamily: '"Inter Variable", Inter, ui-sans-serif, system-ui, sans-serif',
|
|
108
109
|
fontStyle: 'normal',
|
|
109
110
|
fontWeight: 500,
|
|
@@ -119,17 +120,17 @@ const tabsTabCss: CSSMixinDescriptor = css({
|
|
|
119
120
|
verticalAlign: 'top',
|
|
120
121
|
whiteSpace: 'nowrap',
|
|
121
122
|
'&[data-state="inactive"]:hover:not(:disabled):not([aria-disabled="true"])': {
|
|
122
|
-
background: 'rgba(16, 16, 16, 0.05)',
|
|
123
|
-
color: '#101010',
|
|
123
|
+
background: 'light-dark(rgba(16, 16, 16, 0.05), rgba(236, 236, 236, 0.1))',
|
|
124
|
+
color: 'light-dark(#101010, #ececec)',
|
|
124
125
|
},
|
|
125
126
|
'&[data-state="inactive"]:active:not(:disabled):not([aria-disabled="true"])': {
|
|
126
|
-
background: 'rgba(16, 16, 16, 0.08)',
|
|
127
|
+
background: 'light-dark(rgba(16, 16, 16, 0.08), rgba(236, 236, 236, 0.14))',
|
|
127
128
|
},
|
|
128
129
|
'&[data-state="active"]': {
|
|
129
130
|
background: tabSliderBackground,
|
|
130
131
|
'--rmx-tabs-tab-shadow': tabSliderShadow,
|
|
131
|
-
color: '#101010',
|
|
132
|
-
textShadow: '0 1px 0 #FFFFFF',
|
|
132
|
+
color: 'light-dark(#101010, #ececec)',
|
|
133
|
+
textShadow: '0 1px 0 light-dark(#FFFFFF, rgb(0 0 0 / 0.35))',
|
|
133
134
|
},
|
|
134
135
|
'&[data-state="active"]:hover:not(:disabled):not([aria-disabled="true"])': {
|
|
135
136
|
background: tabSliderBackground,
|
|
@@ -149,7 +150,7 @@ const tabsTabCss: CSSMixinDescriptor = css({
|
|
|
149
150
|
|
|
150
151
|
const tabsPanelCss: CSSMixinDescriptor = css({
|
|
151
152
|
minWidth: 0,
|
|
152
|
-
color: '#101010',
|
|
153
|
+
color: 'light-dark(#101010, #ececec)',
|
|
153
154
|
fontFamily: '"Inter Variable", Inter, ui-sans-serif, system-ui, sans-serif',
|
|
154
155
|
fontSize: '13px',
|
|
155
156
|
lineHeight: '20px',
|
package/src/test/README.md
CHANGED
|
@@ -1,104 +1,195 @@
|
|
|
1
1
|
# Test
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use `render` from `remix/ui/test` as the primary way to test components in a live browser through a `remix/test` "browser" test (`*.test.browser.tsx`). It creates a DOM container, renders and flushes the initial component tree, and returns helpers for querying, interacting with, and cleaning up the rendered output.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
The main use case is flushing after events that call `handle.update()`. Since updates are asynchronous, you need to flush to ensure the DOM reflects the changes:
|
|
5
|
+
## Rendering Components
|
|
8
6
|
|
|
9
7
|
```tsx
|
|
8
|
+
import { expect } from 'remix/assert'
|
|
9
|
+
import { describe, it } from 'remix/test'
|
|
10
|
+
import { on, type Handle } from 'remix/ui'
|
|
11
|
+
import { render } from 'remix/ui/test'
|
|
12
|
+
|
|
10
13
|
function Counter(handle: Handle) {
|
|
11
14
|
let count = 0
|
|
12
15
|
|
|
13
16
|
return () => (
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
<div>
|
|
18
|
+
<button
|
|
19
|
+
data-action="increment"
|
|
20
|
+
mix={[
|
|
21
|
+
on('click', () => {
|
|
22
|
+
count++
|
|
23
|
+
handle.update()
|
|
24
|
+
}),
|
|
25
|
+
]}
|
|
26
|
+
>
|
|
27
|
+
Increment
|
|
28
|
+
</button>
|
|
29
|
+
<output data-testid="count">{count}</output>
|
|
30
|
+
</div>
|
|
24
31
|
)
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
let
|
|
34
|
+
describe('Counter', () => {
|
|
35
|
+
it('increments the count', async (t) => {
|
|
36
|
+
let { $, act, cleanup } = render(<Counter />)
|
|
37
|
+
t.after(cleanup)
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
root.flush() // Ensure initial render completes
|
|
39
|
+
expect($('[data-testid="count"]')?.textContent).toBe('0')
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
button.click() // Triggers handle.update()
|
|
36
|
-
root.flush() // Flush to apply the update
|
|
41
|
+
await act(() => $('[data-action="increment"]')?.click())
|
|
37
42
|
|
|
38
|
-
expect(
|
|
43
|
+
expect($('[data-testid="count"]')?.textContent).toBe('1')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
39
46
|
```
|
|
40
47
|
|
|
41
|
-
|
|
48
|
+
The initial render is already flushed, so the DOM and event listeners are ready when `render`
|
|
49
|
+
returns. Wrap interactions that may update component state in `act` and always await it. `act`
|
|
50
|
+
waits for the callback and then flushes pending component updates before the next assertion.
|
|
51
|
+
|
|
52
|
+
### Testing Async Operations
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
When an interaction starts an async operation, await that operation inside `act` so updates queued
|
|
55
|
+
after it resolves are flushed before making assertions:
|
|
44
56
|
|
|
45
57
|
```tsx
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
root.flush() // Event listeners now attached
|
|
58
|
+
function UserLoader(handle: Handle<{ loadUser(): Promise<{ name: string }> }>) {
|
|
59
|
+
let name = 'No user loaded'
|
|
49
60
|
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
return () => (
|
|
62
|
+
<div>
|
|
63
|
+
<button
|
|
64
|
+
mix={[
|
|
65
|
+
on('click', async () => {
|
|
66
|
+
let user = await handle.props.loadUser()
|
|
67
|
+
name = user.name
|
|
68
|
+
handle.update()
|
|
69
|
+
}),
|
|
70
|
+
]}
|
|
71
|
+
>
|
|
72
|
+
Load user
|
|
73
|
+
</button>
|
|
74
|
+
<output>{name}</output>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
it('loads a user', async (t) => {
|
|
80
|
+
let userPromise = Promise.resolve({ name: 'Ada' })
|
|
81
|
+
let { $, act, cleanup } = render(<UserLoader loadUser={() => userPromise} />)
|
|
82
|
+
t.after(cleanup)
|
|
83
|
+
|
|
84
|
+
await act(async () => {
|
|
85
|
+
$('button')?.click()
|
|
86
|
+
await userPromise
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
expect($('output')?.textContent).toBe('Ada')
|
|
90
|
+
})
|
|
52
91
|
```
|
|
53
92
|
|
|
54
|
-
|
|
93
|
+
### Querying Rendered Output
|
|
55
94
|
|
|
56
|
-
|
|
95
|
+
The returned query helpers are scoped to the rendered container so you don't have to worry about false positives outside of the rendered component:
|
|
57
96
|
|
|
58
97
|
```tsx
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (signal.aborted) return
|
|
66
|
-
data = json.value
|
|
67
|
-
handle.update()
|
|
68
|
-
})
|
|
98
|
+
let { $, $$, container, cleanup } = render(
|
|
99
|
+
<nav>
|
|
100
|
+
<a href="/one">One</a>
|
|
101
|
+
<a href="/two">Two</a>
|
|
102
|
+
</nav>,
|
|
103
|
+
)
|
|
69
104
|
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
let nav = $('nav') // container.querySelector('nav')
|
|
106
|
+
let links = $$('a') // container.querySelectorAll('a')
|
|
72
107
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
root.flush()
|
|
108
|
+
expect(nav).toBeTruthy()
|
|
109
|
+
expect(links).toHaveLength(2)
|
|
110
|
+
expect(container.textContent).toContain('One')
|
|
77
111
|
|
|
78
|
-
|
|
112
|
+
cleanup()
|
|
113
|
+
```
|
|
79
114
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
root.flush()
|
|
115
|
+
Use `$` for the first matching element, `$$` for all matching elements, and `container` when a
|
|
116
|
+
DOM assertion does not fit a selector.
|
|
83
117
|
|
|
84
|
-
|
|
118
|
+
### Cleaning Up
|
|
119
|
+
|
|
120
|
+
Register `cleanup` with the test context so it always runs, including when an assertion fails:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
it('renders a dialog', (t) => {
|
|
124
|
+
let { $, cleanup } = render(<Dialog />)
|
|
125
|
+
t.after(cleanup)
|
|
126
|
+
|
|
127
|
+
expect($('[role="dialog"]')).toBeTruthy()
|
|
128
|
+
})
|
|
85
129
|
```
|
|
86
130
|
|
|
87
|
-
|
|
131
|
+
`cleanup` disposes the root and removes its container from the document. You can call it directly
|
|
132
|
+
when a test needs to assert cleanup behavior.
|
|
133
|
+
|
|
134
|
+
### Rendering Into an Existing Container
|
|
88
135
|
|
|
89
|
-
|
|
136
|
+
Pass a container when the test needs a specific DOM context:
|
|
90
137
|
|
|
91
138
|
```tsx
|
|
139
|
+
let container = document.createElement('section')
|
|
140
|
+
document.body.appendChild(container)
|
|
141
|
+
|
|
142
|
+
let result = render(<MyComponent />, { container })
|
|
143
|
+
|
|
144
|
+
expect(result.container).toBe(container)
|
|
145
|
+
result.cleanup()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Advanced: Flushing Manually
|
|
149
|
+
|
|
150
|
+
Most component tests should use `render` and `act`. For lower-level runtime tests that need direct
|
|
151
|
+
control over rendering or scheduling, use the returned `root` and call `root.flush()` after an
|
|
152
|
+
operation that queues work:
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
let { root, container, cleanup } = render(<MyComponent value="first" />)
|
|
156
|
+
|
|
157
|
+
root.render(<MyComponent value="second" />)
|
|
158
|
+
root.flush()
|
|
159
|
+
|
|
160
|
+
expect(container.textContent).toContain('second')
|
|
161
|
+
cleanup()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
You can also create and manage a root directly:
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
import { createRoot } from 'remix/ui'
|
|
168
|
+
|
|
169
|
+
let container = document.createElement('div')
|
|
92
170
|
let root = createRoot(container)
|
|
171
|
+
|
|
93
172
|
root.render(<MyComponent />)
|
|
94
|
-
root.flush()
|
|
173
|
+
root.flush() // Complete the initial render and attach event listeners
|
|
95
174
|
|
|
96
|
-
|
|
97
|
-
|
|
175
|
+
container.querySelector('button')?.click()
|
|
176
|
+
root.flush() // Apply updates queued by the interaction
|
|
98
177
|
|
|
99
|
-
// Remove and verify cleanup
|
|
100
178
|
root.dispose()
|
|
101
|
-
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Manual flushing is also useful after an async operation resolves outside an `act` callback:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
root.render(<AsyncLoader />)
|
|
185
|
+
root.flush()
|
|
186
|
+
|
|
187
|
+
expect(container.textContent).toBe('Loading...')
|
|
188
|
+
|
|
189
|
+
await waitForFetch()
|
|
190
|
+
root.flush()
|
|
191
|
+
|
|
192
|
+
expect(container.textContent).toBe('Expected data')
|
|
102
193
|
```
|
|
103
194
|
|
|
104
195
|
## See Also
|
package/src/toggle/index.tsx
CHANGED
|
@@ -17,13 +17,13 @@ const checkedSelector = '&:checked, &[aria-checked="true"], &[data-state="checke
|
|
|
17
17
|
const checkedThumbSelector =
|
|
18
18
|
'&:checked::before, &[aria-checked="true"]::before, &[data-state="checked"]::before'
|
|
19
19
|
const uncheckedThumbBackground =
|
|
20
|
-
'linear-gradient(180deg, rgba(0, 0, 0, 0) 33%, rgba(0, 0, 0, 0.04) 100%), #FFFFFF'
|
|
20
|
+
'linear-gradient(180deg, rgba(0, 0, 0, 0) 33%, light-dark(rgba(0, 0, 0, 0.04), rgba(255, 255, 255, 0.08)) 100%), light-dark(#FFFFFF, #1a1a1a)'
|
|
21
21
|
const uncheckedThumbShadow =
|
|
22
|
-
'0 0 0 0.5px rgba(0, 0, 0, 0.06), 0 1px 1px -0.5px rgba(0, 0, 0, 0.12), 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 4px 4px -2px rgba(0, 0, 0, 0.12), inset 0 0 2px 1px #FFFFFF'
|
|
22
|
+
'0 0 0 0.5px light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.12)), 0 1px 1px -0.5px rgba(0, 0, 0, 0.12), 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 4px 4px -2px rgba(0, 0, 0, 0.12), inset 0 0 2px 1px light-dark(#FFFFFF, rgba(255, 255, 255, 0.08))'
|
|
23
23
|
const checkedThumbBackground =
|
|
24
|
-
'linear-gradient(180deg, rgba(112, 199, 84, 0) 25%, rgba(112, 199, 84, 0.25) 100%), #FFFFFF'
|
|
24
|
+
'linear-gradient(180deg, rgba(112, 199, 84, 0) 25%, rgba(112, 199, 84, 0.25) 100%), light-dark(#FFFFFF, #f5fff0)'
|
|
25
25
|
const checkedThumbShadow =
|
|
26
|
-
'0 1px 2px -0.5px rgba(66, 134, 44, 0.6), 0 2px 4px -1px rgba(66, 134, 44, 0.6), 0 4px 6px -2px rgba(66, 134, 44, 0.6), 0 0 0 0.5px rgba(0, 0, 0, 0.28), inset 0 0 2px 1px #FFFFFF'
|
|
26
|
+
'0 1px 2px -0.5px rgba(66, 134, 44, 0.6), 0 2px 4px -1px rgba(66, 134, 44, 0.6), 0 4px 6px -2px rgba(66, 134, 44, 0.6), 0 0 0 0.5px light-dark(rgba(0, 0, 0, 0.28), rgba(255, 255, 255, 0.28)), inset 0 0 2px 1px light-dark(#FFFFFF, rgba(255, 255, 255, 0.5))'
|
|
27
27
|
|
|
28
28
|
const toggleDefaultAttrs = createMixin<Element, [], ElementProps>((handle, hostType) => (props) => {
|
|
29
29
|
if (hostType !== 'input') {
|
|
@@ -59,7 +59,8 @@ const baseStyles = {
|
|
|
59
59
|
padding: 0,
|
|
60
60
|
border: 0,
|
|
61
61
|
borderRadius: '9999px',
|
|
62
|
-
background:
|
|
62
|
+
background:
|
|
63
|
+
'linear-gradient(180deg, light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.08)) 0%, rgba(0, 0, 0, 0) 100%), light-dark(#EBEBEB, #2c2c2c)',
|
|
63
64
|
boxShadow:
|
|
64
65
|
'inset 0 0 4px 1px rgba(0, 0, 0, 0.08), inset 0 1px 1px rgba(0, 0, 0, 0.02), inset 0 2px 2px rgba(0, 0, 0, 0.02)',
|
|
65
66
|
verticalAlign: 'middle',
|
|
@@ -82,9 +83,10 @@ const baseStyles = {
|
|
|
82
83
|
opacity: 0.55,
|
|
83
84
|
},
|
|
84
85
|
[checkedSelector]: {
|
|
85
|
-
background:
|
|
86
|
+
background:
|
|
87
|
+
'linear-gradient(180deg, light-dark(#70C754, #8ee572) 0%, light-dark(#70C754, #8ee572) 100%)',
|
|
86
88
|
boxShadow:
|
|
87
|
-
'0 1px 0 rgba(255, 255, 255, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.4), 0 -4px 8px 2px #FFFFFF, 0 4px 8px 2px rgba(0, 0, 0, 0.05), 0 0 12px 1px rgba(112, 199, 84, 0.25), inset 0 0 4px 1px rgba(0, 0, 0, 0.25), inset 0 1px 1px rgba(0, 0, 0, 0.1), inset 0 2px 2px rgba(0, 0, 0, 0.1)',
|
|
89
|
+
'0 1px 0 rgba(255, 255, 255, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.4), 0 -4px 8px 2px light-dark(#FFFFFF, rgba(255, 255, 255, 0.18)), 0 4px 8px 2px rgba(0, 0, 0, 0.05), 0 0 12px 1px rgba(112, 199, 84, 0.25), inset 0 0 4px 1px rgba(0, 0, 0, 0.25), inset 0 1px 1px rgba(0, 0, 0, 0.1), inset 0 2px 2px rgba(0, 0, 0, 0.1)',
|
|
88
90
|
},
|
|
89
91
|
[checkedThumbSelector]: {
|
|
90
92
|
background: checkedThumbBackground,
|