@furystack/shades 11.1.0 → 12.0.1

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 (166) hide show
  1. package/CHANGELOG.md +312 -0
  2. package/README.md +13 -13
  3. package/esm/component-factory.spec.js +13 -5
  4. package/esm/component-factory.spec.js.map +1 -1
  5. package/esm/components/index.d.ts +4 -1
  6. package/esm/components/index.d.ts.map +1 -1
  7. package/esm/components/index.js +4 -1
  8. package/esm/components/index.js.map +1 -1
  9. package/esm/components/lazy-load.d.ts +2 -4
  10. package/esm/components/lazy-load.d.ts.map +1 -1
  11. package/esm/components/lazy-load.js +40 -24
  12. package/esm/components/lazy-load.js.map +1 -1
  13. package/esm/components/lazy-load.spec.js +57 -50
  14. package/esm/components/lazy-load.spec.js.map +1 -1
  15. package/esm/components/link-to-route.d.ts +2 -0
  16. package/esm/components/link-to-route.d.ts.map +1 -1
  17. package/esm/components/link-to-route.js +3 -2
  18. package/esm/components/link-to-route.js.map +1 -1
  19. package/esm/components/link-to-route.spec.js +13 -9
  20. package/esm/components/link-to-route.spec.js.map +1 -1
  21. package/esm/components/nested-route-link.d.ts +62 -0
  22. package/esm/components/nested-route-link.d.ts.map +1 -0
  23. package/esm/components/nested-route-link.js +66 -0
  24. package/esm/components/nested-route-link.js.map +1 -0
  25. package/esm/components/nested-route-link.spec.d.ts +2 -0
  26. package/esm/components/nested-route-link.spec.d.ts.map +1 -0
  27. package/esm/components/nested-route-link.spec.js +179 -0
  28. package/esm/components/nested-route-link.spec.js.map +1 -0
  29. package/esm/components/nested-route-types.d.ts +37 -0
  30. package/esm/components/nested-route-types.d.ts.map +1 -0
  31. package/esm/components/nested-route-types.js +2 -0
  32. package/esm/components/nested-route-types.js.map +1 -0
  33. package/esm/components/nested-router.d.ts +103 -0
  34. package/esm/components/nested-router.d.ts.map +1 -0
  35. package/esm/components/nested-router.js +183 -0
  36. package/esm/components/nested-router.js.map +1 -0
  37. package/esm/components/nested-router.spec.d.ts +2 -0
  38. package/esm/components/nested-router.spec.d.ts.map +1 -0
  39. package/esm/components/nested-router.spec.js +737 -0
  40. package/esm/components/nested-router.spec.js.map +1 -0
  41. package/esm/components/route-link.d.ts +4 -0
  42. package/esm/components/route-link.d.ts.map +1 -1
  43. package/esm/components/route-link.js +5 -5
  44. package/esm/components/route-link.js.map +1 -1
  45. package/esm/components/route-link.spec.js +16 -12
  46. package/esm/components/route-link.spec.js.map +1 -1
  47. package/esm/components/router.d.ts +20 -2
  48. package/esm/components/router.d.ts.map +1 -1
  49. package/esm/components/router.js +12 -7
  50. package/esm/components/router.js.map +1 -1
  51. package/esm/components/router.spec.js +141 -74
  52. package/esm/components/router.spec.js.map +1 -1
  53. package/esm/initialize.d.ts +11 -0
  54. package/esm/initialize.d.ts.map +1 -1
  55. package/esm/initialize.js +5 -0
  56. package/esm/initialize.js.map +1 -1
  57. package/esm/jsx.d.ts +83 -2
  58. package/esm/jsx.d.ts.map +1 -1
  59. package/esm/models/children-list.d.ts +5 -1
  60. package/esm/models/children-list.d.ts.map +1 -1
  61. package/esm/models/partial-element.d.ts +12 -2
  62. package/esm/models/partial-element.d.ts.map +1 -1
  63. package/esm/models/render-options.d.ts +89 -3
  64. package/esm/models/render-options.d.ts.map +1 -1
  65. package/esm/models/selection-state.d.ts +4 -0
  66. package/esm/models/selection-state.d.ts.map +1 -1
  67. package/esm/services/location-service.d.ts +11 -0
  68. package/esm/services/location-service.d.ts.map +1 -1
  69. package/esm/services/location-service.js +11 -0
  70. package/esm/services/location-service.js.map +1 -1
  71. package/esm/services/resource-manager.d.ts +24 -0
  72. package/esm/services/resource-manager.d.ts.map +1 -1
  73. package/esm/services/resource-manager.js +36 -1
  74. package/esm/services/resource-manager.js.map +1 -1
  75. package/esm/services/resource-manager.spec.js +102 -0
  76. package/esm/services/resource-manager.spec.js.map +1 -1
  77. package/esm/services/screen-service.d.ts +81 -4
  78. package/esm/services/screen-service.d.ts.map +1 -1
  79. package/esm/services/screen-service.js +75 -4
  80. package/esm/services/screen-service.js.map +1 -1
  81. package/esm/services/screen-service.spec.js +91 -7
  82. package/esm/services/screen-service.spec.js.map +1 -1
  83. package/esm/shade-component.d.ts +17 -4
  84. package/esm/shade-component.d.ts.map +1 -1
  85. package/esm/shade-component.js +67 -5
  86. package/esm/shade-component.js.map +1 -1
  87. package/esm/shade-host-props-ref.integration.spec.d.ts +2 -0
  88. package/esm/shade-host-props-ref.integration.spec.d.ts.map +1 -0
  89. package/esm/shade-host-props-ref.integration.spec.js +381 -0
  90. package/esm/shade-host-props-ref.integration.spec.js.map +1 -0
  91. package/esm/shade-resources.integration.spec.js +208 -39
  92. package/esm/shade-resources.integration.spec.js.map +1 -1
  93. package/esm/shade.d.ts +20 -17
  94. package/esm/shade.d.ts.map +1 -1
  95. package/esm/shade.js +172 -33
  96. package/esm/shade.js.map +1 -1
  97. package/esm/shade.spec.js +31 -30
  98. package/esm/shade.spec.js.map +1 -1
  99. package/esm/shades.integration.spec.js +135 -72
  100. package/esm/shades.integration.spec.js.map +1 -1
  101. package/esm/style-manager.d.ts +2 -2
  102. package/esm/style-manager.js +2 -2
  103. package/esm/svg-types.d.ts +389 -0
  104. package/esm/svg-types.d.ts.map +1 -0
  105. package/esm/svg-types.js +9 -0
  106. package/esm/svg-types.js.map +1 -0
  107. package/esm/svg.d.ts +15 -0
  108. package/esm/svg.d.ts.map +1 -0
  109. package/esm/svg.js +76 -0
  110. package/esm/svg.js.map +1 -0
  111. package/esm/svg.spec.d.ts +2 -0
  112. package/esm/svg.spec.d.ts.map +1 -0
  113. package/esm/svg.spec.js +80 -0
  114. package/esm/svg.spec.js.map +1 -0
  115. package/esm/vnode.d.ts +103 -0
  116. package/esm/vnode.d.ts.map +1 -0
  117. package/esm/vnode.integration.spec.d.ts +2 -0
  118. package/esm/vnode.integration.spec.d.ts.map +1 -0
  119. package/esm/vnode.integration.spec.js +494 -0
  120. package/esm/vnode.integration.spec.js.map +1 -0
  121. package/esm/vnode.js +453 -0
  122. package/esm/vnode.js.map +1 -0
  123. package/esm/vnode.spec.d.ts +2 -0
  124. package/esm/vnode.spec.d.ts.map +1 -0
  125. package/esm/vnode.spec.js +473 -0
  126. package/esm/vnode.spec.js.map +1 -0
  127. package/package.json +8 -9
  128. package/src/component-factory.spec.tsx +18 -5
  129. package/src/components/index.ts +4 -1
  130. package/src/components/lazy-load.spec.tsx +82 -75
  131. package/src/components/lazy-load.tsx +49 -27
  132. package/src/components/link-to-route.spec.tsx +25 -21
  133. package/src/components/link-to-route.tsx +4 -2
  134. package/src/components/nested-route-link.spec.tsx +303 -0
  135. package/src/components/nested-route-link.tsx +100 -0
  136. package/src/components/nested-route-types.ts +42 -0
  137. package/src/components/nested-router.spec.tsx +918 -0
  138. package/src/components/nested-router.tsx +260 -0
  139. package/src/components/route-link.spec.tsx +22 -18
  140. package/src/components/route-link.tsx +6 -5
  141. package/src/components/router.spec.tsx +196 -108
  142. package/src/components/router.tsx +21 -8
  143. package/src/initialize.ts +12 -0
  144. package/src/jsx.ts +129 -2
  145. package/src/models/children-list.ts +7 -1
  146. package/src/models/partial-element.ts +13 -2
  147. package/src/models/render-options.ts +90 -3
  148. package/src/models/selection-state.ts +4 -0
  149. package/src/services/location-service.tsx +11 -0
  150. package/src/services/resource-manager.spec.ts +128 -0
  151. package/src/services/resource-manager.ts +36 -1
  152. package/src/services/screen-service.spec.ts +109 -7
  153. package/src/services/screen-service.ts +81 -4
  154. package/src/shade-component.ts +72 -6
  155. package/src/shade-host-props-ref.integration.spec.tsx +460 -0
  156. package/src/shade-resources.integration.spec.tsx +276 -52
  157. package/src/shade.spec.tsx +40 -39
  158. package/src/shade.ts +186 -58
  159. package/src/shades.integration.spec.tsx +154 -80
  160. package/src/style-manager.ts +2 -2
  161. package/src/svg-types.ts +437 -0
  162. package/src/svg.spec.ts +89 -0
  163. package/src/svg.ts +78 -0
  164. package/src/vnode.integration.spec.tsx +657 -0
  165. package/src/vnode.spec.ts +579 -0
  166. package/src/vnode.ts +508 -0
@@ -0,0 +1,460 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { ObservableValue, usingAsync } from '@furystack/utils'
3
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
4
+ import { initializeShadeRoot } from './initialize.js'
5
+ import { createComponent } from './shade-component.js'
6
+ import { flushUpdates, Shade } from './shade.js'
7
+
8
+ describe('useHostProps integration tests', () => {
9
+ beforeEach(() => {
10
+ document.body.innerHTML = '<div id="root"></div>'
11
+ })
12
+ afterEach(() => {
13
+ document.body.innerHTML = ''
14
+ })
15
+
16
+ it('should set data attributes on the host element', async () => {
17
+ await usingAsync(new Injector(), async (injector) => {
18
+ const rootElement = document.getElementById('root') as HTMLDivElement
19
+
20
+ const ExampleComponent = Shade<{ variant: string }>({
21
+ shadowDomName: 'host-props-data-attr-test',
22
+ render: ({ props, useHostProps }) => {
23
+ useHostProps({
24
+ 'data-variant': props.variant,
25
+ })
26
+ return <div>content</div>
27
+ },
28
+ })
29
+
30
+ initializeShadeRoot({
31
+ injector,
32
+ rootElement,
33
+ jsxElement: <ExampleComponent variant="primary" />,
34
+ })
35
+ await flushUpdates()
36
+
37
+ const el = document.querySelector('host-props-data-attr-test') as HTMLElement
38
+ expect(el).toBeTruthy()
39
+ expect(el.getAttribute('data-variant')).toBe('primary')
40
+ })
41
+ })
42
+
43
+ it('should set aria attributes on the host element', async () => {
44
+ await usingAsync(new Injector(), async (injector) => {
45
+ const rootElement = document.getElementById('root') as HTMLDivElement
46
+
47
+ const ExampleComponent = Shade({
48
+ shadowDomName: 'host-props-aria-test',
49
+ render: ({ useHostProps }) => {
50
+ useHostProps({
51
+ role: 'progressbar',
52
+ 'aria-valuenow': '50',
53
+ 'aria-valuemin': '0',
54
+ 'aria-valuemax': '100',
55
+ })
56
+ return <div>progress</div>
57
+ },
58
+ })
59
+
60
+ initializeShadeRoot({
61
+ injector,
62
+ rootElement,
63
+ jsxElement: <ExampleComponent />,
64
+ })
65
+ await flushUpdates()
66
+
67
+ const el = document.querySelector('host-props-aria-test') as HTMLElement
68
+ expect(el.getAttribute('role')).toBe('progressbar')
69
+ expect(el.getAttribute('aria-valuenow')).toBe('50')
70
+ expect(el.getAttribute('aria-valuemin')).toBe('0')
71
+ expect(el.getAttribute('aria-valuemax')).toBe('100')
72
+ })
73
+ })
74
+
75
+ it('should apply CSS custom properties via style', async () => {
76
+ await usingAsync(new Injector(), async (injector) => {
77
+ const rootElement = document.getElementById('root') as HTMLDivElement
78
+
79
+ const ExampleComponent = Shade({
80
+ shadowDomName: 'host-props-css-vars-test',
81
+ render: ({ useHostProps }) => {
82
+ useHostProps({
83
+ style: {
84
+ '--my-color': 'red',
85
+ '--my-size': '16px',
86
+ },
87
+ })
88
+ return <div>styled</div>
89
+ },
90
+ })
91
+
92
+ initializeShadeRoot({
93
+ injector,
94
+ rootElement,
95
+ jsxElement: <ExampleComponent />,
96
+ })
97
+ await flushUpdates()
98
+
99
+ const el = document.querySelector('host-props-css-vars-test') as HTMLElement
100
+ expect(el.style.getPropertyValue('--my-color')).toBe('red')
101
+ expect(el.style.getPropertyValue('--my-size')).toBe('16px')
102
+ })
103
+ })
104
+
105
+ it('should apply standard inline styles', async () => {
106
+ await usingAsync(new Injector(), async (injector) => {
107
+ const rootElement = document.getElementById('root') as HTMLDivElement
108
+
109
+ const ExampleComponent = Shade({
110
+ shadowDomName: 'host-props-inline-style-test',
111
+ render: ({ useHostProps }) => {
112
+ useHostProps({
113
+ style: {
114
+ display: 'flex',
115
+ gap: '8px',
116
+ },
117
+ })
118
+ return <div>styled</div>
119
+ },
120
+ })
121
+
122
+ initializeShadeRoot({
123
+ injector,
124
+ rootElement,
125
+ jsxElement: <ExampleComponent />,
126
+ })
127
+ await flushUpdates()
128
+
129
+ const el = document.querySelector('host-props-inline-style-test') as HTMLElement
130
+ expect(el.style.display).toBe('flex')
131
+ expect(el.style.gap).toBe('8px')
132
+ })
133
+ })
134
+
135
+ it('should merge multiple useHostProps calls', async () => {
136
+ await usingAsync(new Injector(), async (injector) => {
137
+ const rootElement = document.getElementById('root') as HTMLDivElement
138
+
139
+ const ExampleComponent = Shade({
140
+ shadowDomName: 'host-props-merge-test',
141
+ render: ({ useHostProps }) => {
142
+ useHostProps({
143
+ 'data-first': 'one',
144
+ style: { '--color-a': 'red' },
145
+ })
146
+ useHostProps({
147
+ 'data-second': 'two',
148
+ style: { '--color-b': 'blue' },
149
+ })
150
+ return <div>merged</div>
151
+ },
152
+ })
153
+
154
+ initializeShadeRoot({
155
+ injector,
156
+ rootElement,
157
+ jsxElement: <ExampleComponent />,
158
+ })
159
+ await flushUpdates()
160
+
161
+ const el = document.querySelector('host-props-merge-test') as HTMLElement
162
+ expect(el.getAttribute('data-first')).toBe('one')
163
+ expect(el.getAttribute('data-second')).toBe('two')
164
+ expect(el.style.getPropertyValue('--color-a')).toBe('red')
165
+ expect(el.style.getPropertyValue('--color-b')).toBe('blue')
166
+ })
167
+ })
168
+
169
+ it('should remove attributes when they are no longer set on re-render', async () => {
170
+ await usingAsync(new Injector(), async (injector) => {
171
+ const rootElement = document.getElementById('root') as HTMLDivElement
172
+ const showExtra = new ObservableValue(true)
173
+
174
+ const ExampleComponent = Shade({
175
+ shadowDomName: 'host-props-remove-attr-test',
176
+ render: ({ useHostProps, useObservable }) => {
177
+ const [show] = useObservable('showExtra', showExtra)
178
+ useHostProps({
179
+ 'data-always': 'yes',
180
+ ...(show ? { 'data-extra': 'present' } : {}),
181
+ })
182
+ return <div>content</div>
183
+ },
184
+ })
185
+
186
+ initializeShadeRoot({
187
+ injector,
188
+ rootElement,
189
+ jsxElement: <ExampleComponent />,
190
+ })
191
+ await flushUpdates()
192
+
193
+ const el = document.querySelector('host-props-remove-attr-test') as HTMLElement
194
+ expect(el.getAttribute('data-always')).toBe('yes')
195
+ expect(el.getAttribute('data-extra')).toBe('present')
196
+
197
+ showExtra.setValue(false)
198
+ await flushUpdates()
199
+
200
+ expect(el.getAttribute('data-always')).toBe('yes')
201
+ expect(el.getAttribute('data-extra')).toBeNull()
202
+ })
203
+ })
204
+
205
+ it('should remove CSS custom properties when they are no longer set', async () => {
206
+ await usingAsync(new Injector(), async (injector) => {
207
+ const rootElement = document.getElementById('root') as HTMLDivElement
208
+ const showColor = new ObservableValue(true)
209
+
210
+ const ExampleComponent = Shade({
211
+ shadowDomName: 'host-props-remove-css-var-test',
212
+ render: ({ useHostProps, useObservable }) => {
213
+ const [show] = useObservable('showColor', showColor)
214
+ useHostProps({
215
+ style: {
216
+ display: 'block',
217
+ ...(show ? { '--my-color': 'red' } : {}),
218
+ },
219
+ })
220
+ return <div>content</div>
221
+ },
222
+ })
223
+
224
+ initializeShadeRoot({
225
+ injector,
226
+ rootElement,
227
+ jsxElement: <ExampleComponent />,
228
+ })
229
+ await flushUpdates()
230
+
231
+ const el = document.querySelector('host-props-remove-css-var-test') as HTMLElement
232
+ expect(el.style.getPropertyValue('--my-color')).toBe('red')
233
+ expect(el.style.display).toBe('block')
234
+
235
+ showColor.setValue(false)
236
+ await flushUpdates()
237
+
238
+ expect(el.style.getPropertyValue('--my-color')).toBe('')
239
+ expect(el.style.display).toBe('block')
240
+ })
241
+ })
242
+
243
+ it('should set event handlers on the host element', async () => {
244
+ await usingAsync(new Injector(), async (injector) => {
245
+ const rootElement = document.getElementById('root') as HTMLDivElement
246
+ let clicked = false
247
+
248
+ const ExampleComponent = Shade({
249
+ shadowDomName: 'host-props-event-test',
250
+ render: ({ useHostProps }) => {
251
+ useHostProps({
252
+ onclick: () => {
253
+ clicked = true
254
+ },
255
+ })
256
+ return <div>clickable</div>
257
+ },
258
+ })
259
+
260
+ initializeShadeRoot({
261
+ injector,
262
+ rootElement,
263
+ jsxElement: <ExampleComponent />,
264
+ })
265
+ await flushUpdates()
266
+
267
+ const el = document.querySelector('host-props-event-test') as HTMLElement
268
+ el.click()
269
+ expect(clicked).toBe(true)
270
+ })
271
+ })
272
+ })
273
+
274
+ describe('useRef integration tests', () => {
275
+ beforeEach(() => {
276
+ document.body.innerHTML = '<div id="root"></div>'
277
+ })
278
+ afterEach(() => {
279
+ document.body.innerHTML = ''
280
+ })
281
+
282
+ it('should set ref.current to the mounted element', async () => {
283
+ await usingAsync(new Injector(), async (injector) => {
284
+ const rootElement = document.getElementById('root') as HTMLDivElement
285
+
286
+ let capturedRef: { readonly current: HTMLDivElement | null } | undefined
287
+
288
+ const ExampleComponent = Shade({
289
+ shadowDomName: 'use-ref-basic-test',
290
+ render: ({ useRef }) => {
291
+ const divRef = useRef<HTMLDivElement>('myDiv')
292
+ capturedRef = divRef
293
+ return (
294
+ <div ref={divRef} id="target">
295
+ hello
296
+ </div>
297
+ )
298
+ },
299
+ })
300
+
301
+ initializeShadeRoot({
302
+ injector,
303
+ rootElement,
304
+ jsxElement: <ExampleComponent />,
305
+ })
306
+ await flushUpdates()
307
+
308
+ expect(capturedRef).toBeTruthy()
309
+ expect(capturedRef!.current).toBeTruthy()
310
+ expect(capturedRef!.current).toBe(document.getElementById('target'))
311
+ expect(capturedRef!.current?.textContent).toBe('hello')
312
+ })
313
+ })
314
+
315
+ it('should return the same ref object across re-renders', async () => {
316
+ await usingAsync(new Injector(), async (injector) => {
317
+ const rootElement = document.getElementById('root') as HTMLDivElement
318
+ const counter = new ObservableValue(0)
319
+
320
+ const capturedRefs: Array<{ readonly current: Element | null }> = []
321
+
322
+ const ExampleComponent = Shade({
323
+ shadowDomName: 'use-ref-stable-test',
324
+ render: ({ useRef, useObservable }) => {
325
+ const [count] = useObservable('counter', counter)
326
+ const divRef = useRef('myDiv')
327
+ capturedRefs.push(divRef)
328
+ return <div ref={divRef}>{count}</div>
329
+ },
330
+ })
331
+
332
+ initializeShadeRoot({
333
+ injector,
334
+ rootElement,
335
+ jsxElement: <ExampleComponent />,
336
+ })
337
+ await flushUpdates()
338
+
339
+ counter.setValue(1)
340
+ await flushUpdates()
341
+
342
+ counter.setValue(2)
343
+ await flushUpdates()
344
+
345
+ expect(capturedRefs.length).toBe(3)
346
+ expect(capturedRefs[0]).toBe(capturedRefs[1])
347
+ expect(capturedRefs[1]).toBe(capturedRefs[2])
348
+ })
349
+ })
350
+
351
+ it('should set ref.current on nested child elements', async () => {
352
+ await usingAsync(new Injector(), async (injector) => {
353
+ const rootElement = document.getElementById('root') as HTMLDivElement
354
+
355
+ let capturedInputRef: { readonly current: HTMLInputElement | null } | undefined
356
+
357
+ const ExampleComponent = Shade({
358
+ shadowDomName: 'use-ref-nested-test',
359
+ render: ({ useRef }) => {
360
+ const inputRef = useRef<HTMLInputElement>('input')
361
+ capturedInputRef = inputRef
362
+ return (
363
+ <div>
364
+ <label>
365
+ <input ref={inputRef} type="text" id="my-input" />
366
+ </label>
367
+ </div>
368
+ )
369
+ },
370
+ })
371
+
372
+ initializeShadeRoot({
373
+ injector,
374
+ rootElement,
375
+ jsxElement: <ExampleComponent />,
376
+ })
377
+ await flushUpdates()
378
+
379
+ expect(capturedInputRef).toBeTruthy()
380
+ expect(capturedInputRef!.current).toBeTruthy()
381
+ expect(capturedInputRef!.current).toBe(document.getElementById('my-input'))
382
+ expect(capturedInputRef!.current?.type).toBe('text')
383
+ })
384
+ })
385
+
386
+ it('should clear ref.current when element is unmounted', async () => {
387
+ await usingAsync(new Injector(), async (injector) => {
388
+ const rootElement = document.getElementById('root') as HTMLDivElement
389
+ const showChild = new ObservableValue(true)
390
+
391
+ let capturedRef: { readonly current: HTMLSpanElement | null } | undefined
392
+
393
+ const ExampleComponent = Shade({
394
+ shadowDomName: 'use-ref-unmount-test',
395
+ render: ({ useRef, useObservable }) => {
396
+ const [show] = useObservable('showChild', showChild)
397
+ const spanRef = useRef<HTMLSpanElement>('span')
398
+ capturedRef = spanRef
399
+ return <div>{show ? <span ref={spanRef}>visible</span> : null}</div>
400
+ },
401
+ })
402
+
403
+ initializeShadeRoot({
404
+ injector,
405
+ rootElement,
406
+ jsxElement: <ExampleComponent />,
407
+ })
408
+ await flushUpdates()
409
+
410
+ expect(capturedRef!.current).toBeTruthy()
411
+ expect(capturedRef!.current?.textContent).toBe('visible')
412
+
413
+ showChild.setValue(false)
414
+ await flushUpdates()
415
+
416
+ expect(capturedRef!.current).toBeNull()
417
+ })
418
+ })
419
+
420
+ it('should work with useRef in onChange callbacks', async () => {
421
+ await usingAsync(new Injector(), async (injector) => {
422
+ const rootElement = document.getElementById('root') as HTMLDivElement
423
+ const counter = new ObservableValue(0)
424
+
425
+ const ExampleComponent = Shade({
426
+ shadowDomName: 'use-ref-onchange-test',
427
+ render: ({ useRef, useObservable }) => {
428
+ const spanRef = useRef<HTMLSpanElement>('counterSpan')
429
+ useObservable('counter', counter, {
430
+ onChange: (value) => {
431
+ if (spanRef.current) {
432
+ spanRef.current.textContent = String(value)
433
+ }
434
+ },
435
+ })
436
+ return (
437
+ <span ref={spanRef} id="counter-span">
438
+ 0
439
+ </span>
440
+ )
441
+ },
442
+ })
443
+
444
+ initializeShadeRoot({
445
+ injector,
446
+ rootElement,
447
+ jsxElement: <ExampleComponent />,
448
+ })
449
+ await flushUpdates()
450
+
451
+ const span = document.getElementById('counter-span')
452
+ expect(span?.textContent).toBe('0')
453
+
454
+ counter.setValue(42)
455
+ await flushUpdates()
456
+
457
+ expect(span?.textContent).toBe('42')
458
+ })
459
+ })
460
+ })