@furystack/shades-common-components 13.3.1 → 13.4.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.
- package/CHANGELOG.md +33 -0
- package/esm/components/alert.d.ts.map +1 -1
- package/esm/components/alert.js +7 -6
- package/esm/components/alert.js.map +1 -1
- package/esm/components/cache-view.d.ts +2 -1
- package/esm/components/cache-view.d.ts.map +1 -1
- package/esm/components/cache-view.js +9 -7
- package/esm/components/cache-view.js.map +1 -1
- package/esm/components/cache-view.spec.js +195 -145
- package/esm/components/cache-view.spec.js.map +1 -1
- package/esm/components/command-palette/command-palette-suggestion-list.js +1 -1
- package/esm/components/command-palette/command-palette-suggestion-list.js.map +1 -1
- package/esm/components/inputs/input.d.ts.map +1 -1
- package/esm/components/inputs/input.js +2 -0
- package/esm/components/inputs/input.js.map +1 -1
- package/esm/components/inputs/select.d.ts.map +1 -1
- package/esm/components/inputs/select.js +3 -1
- package/esm/components/inputs/select.js.map +1 -1
- package/esm/components/result.d.ts +4 -2
- package/esm/components/result.d.ts.map +1 -1
- package/esm/components/result.js +11 -10
- package/esm/components/result.js.map +1 -1
- package/esm/components/suggest/index.d.ts.map +1 -1
- package/esm/components/suggest/index.js +7 -3
- package/esm/components/suggest/index.js.map +1 -1
- package/esm/components/tabs.d.ts +2 -0
- package/esm/components/tabs.d.ts.map +1 -1
- package/esm/components/tabs.js +5 -4
- package/esm/components/tabs.js.map +1 -1
- package/esm/components/tabs.spec.js +57 -0
- package/esm/components/tabs.spec.js.map +1 -1
- package/esm/components/tree/tree.d.ts.map +1 -1
- package/esm/components/tree/tree.js +1 -0
- package/esm/components/tree/tree.js.map +1 -1
- package/esm/components/wizard/index.d.ts +2 -1
- package/esm/components/wizard/index.d.ts.map +1 -1
- package/esm/components/wizard/index.js +3 -3
- package/esm/components/wizard/index.js.map +1 -1
- package/esm/components/wizard/index.spec.js +46 -1
- package/esm/components/wizard/index.spec.js.map +1 -1
- package/package.json +6 -6
- package/src/components/alert.tsx +9 -6
- package/src/components/cache-view.spec.tsx +266 -173
- package/src/components/cache-view.tsx +21 -8
- package/src/components/command-palette/command-palette-suggestion-list.tsx +1 -1
- package/src/components/inputs/input.tsx +2 -0
- package/src/components/inputs/select.tsx +3 -1
- package/src/components/result.tsx +17 -10
- package/src/components/suggest/index.tsx +18 -15
- package/src/components/tabs.spec.tsx +72 -0
- package/src/components/tabs.tsx +9 -4
- package/src/components/tree/tree.tsx +1 -0
- package/src/components/wizard/index.spec.tsx +57 -1
- package/src/components/wizard/index.tsx +5 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Cache } from '@furystack/cache'
|
|
2
2
|
import type { CacheResult, CacheWithValue } from '@furystack/cache'
|
|
3
3
|
import { Shade, createComponent, flushUpdates } from '@furystack/shades'
|
|
4
|
+
import { using, usingAsync } from '@furystack/utils'
|
|
4
5
|
|
|
5
6
|
import { describe, expect, it, vi } from 'vitest'
|
|
6
7
|
import { CacheView } from './cache-view.js'
|
|
@@ -45,232 +46,324 @@ describe('CacheView', () => {
|
|
|
45
46
|
})
|
|
46
47
|
|
|
47
48
|
it('should create a cache-view element', () => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
using(new Cache<string, [string]>({ load: async (key) => key }), (cache) => {
|
|
50
|
+
const el = (<CacheView cache={cache} args={['test']} content={TestContent} />) as unknown as HTMLElement
|
|
51
|
+
expect(el).toBeDefined()
|
|
52
|
+
expect(el.tagName?.toLowerCase()).toBe('shade-cache-view')
|
|
53
|
+
})
|
|
53
54
|
})
|
|
54
55
|
|
|
55
56
|
describe('loading state', () => {
|
|
56
57
|
it('should render null by default when loading', async () => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
await usingAsync(
|
|
59
|
+
new Cache<string, [string]>({
|
|
60
|
+
load: () => new Promise(() => {}),
|
|
61
|
+
}),
|
|
62
|
+
async (cache) => {
|
|
63
|
+
const { cacheView } = await renderCacheView(cache, ['test'])
|
|
64
|
+
expect(cacheView.querySelector('test-cache-content')).toBeNull()
|
|
65
|
+
expect(cacheView.querySelector('shade-result')).toBeNull()
|
|
66
|
+
},
|
|
67
|
+
)
|
|
64
68
|
})
|
|
65
69
|
|
|
66
70
|
it('should render custom loader when provided', async () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
await usingAsync(
|
|
72
|
+
new Cache<string, [string]>({
|
|
73
|
+
load: () => new Promise(() => {}),
|
|
74
|
+
}),
|
|
75
|
+
async (cache) => {
|
|
76
|
+
const { cacheView } = await renderCacheView(cache, ['test'], {
|
|
77
|
+
loader: (<span className="custom-loader">Loading...</span>) as unknown as JSX.Element,
|
|
78
|
+
})
|
|
79
|
+
const loader = cacheView.querySelector('.custom-loader')
|
|
80
|
+
expect(loader).not.toBeNull()
|
|
81
|
+
expect(loader?.textContent).toBe('Loading...')
|
|
82
|
+
},
|
|
83
|
+
)
|
|
77
84
|
})
|
|
78
85
|
})
|
|
79
86
|
|
|
80
87
|
describe('loaded state', () => {
|
|
81
88
|
it('should render content when cache has loaded value', async () => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
await usingAsync(new Cache<string, [string]>({ load: async (key) => `Hello ${key}` }), async (cache) => {
|
|
90
|
+
await cache.get('world')
|
|
91
|
+
const { cacheView } = await renderCacheView(cache, ['world'])
|
|
92
|
+
const contentEl = cacheView.querySelector('test-cache-content')
|
|
93
|
+
expect(contentEl).not.toBeNull()
|
|
94
|
+
const contentComponent = contentEl as JSX.Element
|
|
95
|
+
contentComponent.updateComponent()
|
|
96
|
+
await flushUpdates()
|
|
97
|
+
const valueEl = contentComponent.querySelector('.content-value')
|
|
98
|
+
expect(valueEl?.textContent).toBe('Hello world')
|
|
99
|
+
})
|
|
93
100
|
})
|
|
94
101
|
})
|
|
95
102
|
|
|
96
103
|
describe('failed state', () => {
|
|
97
104
|
it('should render default error UI when cache has failed', async () => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
await usingAsync(
|
|
106
|
+
new Cache<string, [string]>({
|
|
107
|
+
load: async () => {
|
|
108
|
+
throw new Error('Test failure')
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
async (cache) => {
|
|
112
|
+
try {
|
|
113
|
+
await cache.get('test')
|
|
114
|
+
} catch {
|
|
115
|
+
// expected
|
|
116
|
+
}
|
|
117
|
+
const { cacheView } = await renderCacheView(cache, ['test'])
|
|
118
|
+
const resultEl = cacheView.querySelector('shade-result')
|
|
119
|
+
expect(resultEl).not.toBeNull()
|
|
120
|
+
const resultComponent = resultEl as JSX.Element
|
|
121
|
+
resultComponent.updateComponent()
|
|
122
|
+
await flushUpdates()
|
|
123
|
+
const titleEl = resultComponent.querySelector('.result-title') as JSX.Element
|
|
124
|
+
titleEl.updateComponent()
|
|
125
|
+
await flushUpdates()
|
|
126
|
+
expect(titleEl?.textContent).toBe('Something went wrong')
|
|
101
127
|
},
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
await cache.get('test')
|
|
105
|
-
} catch {
|
|
106
|
-
// expected
|
|
107
|
-
}
|
|
108
|
-
const { cacheView } = await renderCacheView(cache, ['test'])
|
|
109
|
-
const resultEl = cacheView.querySelector('shade-result')
|
|
110
|
-
expect(resultEl).not.toBeNull()
|
|
111
|
-
const resultComponent = resultEl as JSX.Element
|
|
112
|
-
resultComponent.updateComponent()
|
|
113
|
-
await flushUpdates()
|
|
114
|
-
const titleEl = resultComponent.querySelector('.result-title') as JSX.Element
|
|
115
|
-
titleEl.updateComponent()
|
|
116
|
-
await flushUpdates()
|
|
117
|
-
expect(titleEl?.textContent).toBe('Something went wrong')
|
|
118
|
-
cache[Symbol.dispose]()
|
|
128
|
+
)
|
|
119
129
|
})
|
|
120
130
|
|
|
121
131
|
it('should render custom error UI when error prop is provided', async () => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
await usingAsync(
|
|
133
|
+
new Cache<string, [string]>({
|
|
134
|
+
load: async () => {
|
|
135
|
+
throw new Error('Custom failure')
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
async (cache) => {
|
|
139
|
+
try {
|
|
140
|
+
await cache.get('test')
|
|
141
|
+
} catch {
|
|
142
|
+
// expected
|
|
143
|
+
}
|
|
144
|
+
const customError = vi.fn(
|
|
145
|
+
(err: unknown, _retry: () => void) =>
|
|
146
|
+
(<span className="custom-error">{String(err)}</span>) as unknown as JSX.Element,
|
|
147
|
+
)
|
|
148
|
+
const { cacheView } = await renderCacheView(cache, ['test'], { error: customError })
|
|
149
|
+
expect(customError).toHaveBeenCalledOnce()
|
|
150
|
+
expect(customError.mock.calls[0][0]).toBeInstanceOf(Error)
|
|
151
|
+
const customErrorEl = cacheView.querySelector('.custom-error')
|
|
152
|
+
expect(customErrorEl).not.toBeNull()
|
|
125
153
|
},
|
|
126
|
-
})
|
|
127
|
-
try {
|
|
128
|
-
await cache.get('test')
|
|
129
|
-
} catch {
|
|
130
|
-
// expected
|
|
131
|
-
}
|
|
132
|
-
const customError = vi.fn(
|
|
133
|
-
(err: unknown, _retry: () => void) =>
|
|
134
|
-
(<span className="custom-error">{String(err)}</span>) as unknown as JSX.Element,
|
|
135
154
|
)
|
|
136
|
-
const { cacheView } = await renderCacheView(cache, ['test'], { error: customError })
|
|
137
|
-
expect(customError).toHaveBeenCalledOnce()
|
|
138
|
-
expect(customError.mock.calls[0][0]).toBeInstanceOf(Error)
|
|
139
|
-
const customErrorEl = cacheView.querySelector('.custom-error')
|
|
140
|
-
expect(customErrorEl).not.toBeNull()
|
|
141
|
-
cache[Symbol.dispose]()
|
|
142
155
|
})
|
|
143
156
|
|
|
144
157
|
it('should not render content when failed even if stale value exists', async () => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
await usingAsync(new Cache<string, [string]>({ load: async (key) => key }), async (cache) => {
|
|
159
|
+
await cache.get('test')
|
|
160
|
+
cache.setExplicitValue({
|
|
161
|
+
loadArgs: ['test'],
|
|
162
|
+
value: { status: 'failed', error: new Error('fail'), value: 'stale', updatedAt: new Date() },
|
|
163
|
+
})
|
|
164
|
+
const { cacheView } = await renderCacheView(cache, ['test'])
|
|
165
|
+
expect(cacheView.querySelector('test-cache-content')).toBeNull()
|
|
166
|
+
expect(cacheView.querySelector('shade-result')).not.toBeNull()
|
|
150
167
|
})
|
|
151
|
-
const { cacheView } = await renderCacheView(cache, ['test'])
|
|
152
|
-
expect(cacheView.querySelector('test-cache-content')).toBeNull()
|
|
153
|
-
expect(cacheView.querySelector('shade-result')).not.toBeNull()
|
|
154
|
-
cache[Symbol.dispose]()
|
|
155
168
|
})
|
|
156
169
|
|
|
157
170
|
it('should call cache.reload when retry is invoked', async () => {
|
|
158
171
|
const loadFn = vi.fn<(key: string) => Promise<string>>(async () => {
|
|
159
172
|
throw new Error('fail')
|
|
160
173
|
})
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
174
|
+
await usingAsync(new Cache<string, [string]>({ load: loadFn }), async (cache) => {
|
|
175
|
+
try {
|
|
176
|
+
await cache.get('test')
|
|
177
|
+
} catch {
|
|
178
|
+
// expected
|
|
179
|
+
}
|
|
180
|
+
let capturedRetry: (() => void) | undefined
|
|
181
|
+
const customError = (_err: unknown, retry: () => void) => {
|
|
182
|
+
capturedRetry = retry
|
|
183
|
+
return (<span className="custom-error">Error</span>) as unknown as JSX.Element
|
|
184
|
+
}
|
|
185
|
+
await renderCacheView(cache, ['test'], { error: customError })
|
|
186
|
+
expect(capturedRetry).toBeDefined()
|
|
187
|
+
|
|
188
|
+
loadFn.mockResolvedValueOnce('recovered')
|
|
189
|
+
capturedRetry!()
|
|
190
|
+
await flushUpdates()
|
|
191
|
+
|
|
192
|
+
const observable = cache.getObservable('test')
|
|
193
|
+
const result = observable.getValue()
|
|
194
|
+
expect(result.status).toBe('loaded')
|
|
195
|
+
expect(result.value).toBe('recovered')
|
|
196
|
+
})
|
|
184
197
|
})
|
|
185
198
|
})
|
|
186
199
|
|
|
187
200
|
describe('obsolete state', () => {
|
|
188
201
|
it('should render content when obsolete and trigger reload', async () => {
|
|
189
202
|
const loadFn = vi.fn(async (key: string) => `Hello ${key}`)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
await usingAsync(new Cache<string, [string]>({ load: loadFn }), async (cache) => {
|
|
204
|
+
await cache.get('test')
|
|
205
|
+
cache.setObsolete('test')
|
|
206
|
+
|
|
207
|
+
const { cacheView } = await renderCacheView(cache, ['test'])
|
|
208
|
+
const contentEl = cacheView.querySelector('test-cache-content')
|
|
209
|
+
expect(contentEl).not.toBeNull()
|
|
210
|
+
|
|
211
|
+
await flushUpdates()
|
|
212
|
+
// reload should have been called (initial load + obsolete reload)
|
|
213
|
+
expect(loadFn).toHaveBeenCalledTimes(2)
|
|
214
|
+
})
|
|
202
215
|
})
|
|
203
216
|
})
|
|
204
217
|
|
|
205
218
|
describe('error takes priority over value', () => {
|
|
206
219
|
it('should show error when failed with value, not content', async () => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
+
await usingAsync(new Cache<string, [string]>({ load: async (key) => key }), async (cache) => {
|
|
221
|
+
await cache.get('test')
|
|
222
|
+
const failedWithValue: CacheResult<string> = {
|
|
223
|
+
status: 'failed',
|
|
224
|
+
error: new Error('whoops'),
|
|
225
|
+
value: 'stale-data',
|
|
226
|
+
updatedAt: new Date(),
|
|
227
|
+
}
|
|
228
|
+
cache.setExplicitValue({ loadArgs: ['test'], value: failedWithValue })
|
|
229
|
+
const { cacheView } = await renderCacheView(cache, ['test'])
|
|
230
|
+
expect(cacheView.querySelector('test-cache-content')).toBeNull()
|
|
231
|
+
expect(cacheView.querySelector('shade-result')).not.toBeNull()
|
|
232
|
+
})
|
|
220
233
|
})
|
|
221
234
|
})
|
|
222
235
|
|
|
223
236
|
describe('contentProps', () => {
|
|
224
237
|
it('should forward contentProps to the content component', async () => {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
238
|
+
await usingAsync(new Cache<string, [string]>({ load: async (key) => `Hello ${key}` }), async (cache) => {
|
|
239
|
+
await cache.get('world')
|
|
240
|
+
|
|
241
|
+
const el = (
|
|
242
|
+
<div>
|
|
243
|
+
<CacheView
|
|
244
|
+
cache={cache}
|
|
245
|
+
args={['world']}
|
|
246
|
+
content={TestContentWithLabel}
|
|
247
|
+
contentProps={{ label: 'Greeting' }}
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
)
|
|
251
|
+
const cacheView = el.firstElementChild as JSX.Element
|
|
252
|
+
cacheView.updateComponent()
|
|
253
|
+
await flushUpdates()
|
|
254
|
+
|
|
255
|
+
const contentEl = cacheView.querySelector('test-cache-content-with-label') as JSX.Element
|
|
256
|
+
expect(contentEl).not.toBeNull()
|
|
257
|
+
contentEl.updateComponent()
|
|
258
|
+
await flushUpdates()
|
|
259
|
+
const valueEl = contentEl.querySelector('.content-value')
|
|
260
|
+
expect(valueEl?.textContent).toBe('Greeting: Hello world')
|
|
261
|
+
})
|
|
249
262
|
})
|
|
250
263
|
|
|
251
264
|
it('should forward contentProps when cache entry is obsolete', async () => {
|
|
252
265
|
const loadFn = vi.fn(async (key: string) => `Hello ${key}`)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
266
|
+
await usingAsync(new Cache<string, [string]>({ load: loadFn }), async (cache) => {
|
|
267
|
+
await cache.get('world')
|
|
268
|
+
cache.setObsolete('world')
|
|
269
|
+
|
|
270
|
+
const el = (
|
|
271
|
+
<div>
|
|
272
|
+
<CacheView
|
|
273
|
+
cache={cache}
|
|
274
|
+
args={['world']}
|
|
275
|
+
content={TestContentWithLabel}
|
|
276
|
+
contentProps={{ label: 'Stale' }}
|
|
277
|
+
/>
|
|
278
|
+
</div>
|
|
279
|
+
)
|
|
280
|
+
const cacheView = el.firstElementChild as JSX.Element
|
|
281
|
+
cacheView.updateComponent()
|
|
282
|
+
await flushUpdates()
|
|
283
|
+
|
|
284
|
+
const contentEl = cacheView.querySelector('test-cache-content-with-label') as JSX.Element
|
|
285
|
+
expect(contentEl).not.toBeNull()
|
|
286
|
+
contentEl.updateComponent()
|
|
287
|
+
await flushUpdates()
|
|
288
|
+
const valueEl = contentEl.querySelector('.content-value')
|
|
289
|
+
expect(valueEl?.textContent).toBe('Stale: Hello world')
|
|
290
|
+
expect(loadFn).toHaveBeenCalledTimes(2)
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
describe('view transitions', () => {
|
|
296
|
+
const mockStartViewTransition = () => {
|
|
297
|
+
const spy = vi.fn((optionsOrCallback: StartViewTransitionOptions | (() => void)) => {
|
|
298
|
+
const update = typeof optionsOrCallback === 'function' ? optionsOrCallback : optionsOrCallback.update
|
|
299
|
+
update?.()
|
|
300
|
+
return {
|
|
301
|
+
finished: Promise.resolve(),
|
|
302
|
+
ready: Promise.resolve(),
|
|
303
|
+
updateCallbackDone: Promise.resolve(),
|
|
304
|
+
skipTransition: vi.fn(),
|
|
305
|
+
} as unknown as ViewTransition
|
|
306
|
+
})
|
|
307
|
+
document.startViewTransition = spy as typeof document.startViewTransition
|
|
308
|
+
return spy
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
it('should call startViewTransition when viewTransition is enabled and cache state category changes', async () => {
|
|
312
|
+
const spy = mockStartViewTransition()
|
|
313
|
+
await usingAsync(new Cache<string, [string]>({ load: async (key) => `loaded-${key}` }), async (cache) => {
|
|
314
|
+
const el = (
|
|
315
|
+
<div>
|
|
316
|
+
<CacheView
|
|
317
|
+
cache={cache}
|
|
318
|
+
args={['test']}
|
|
319
|
+
content={TestContent}
|
|
320
|
+
loader={<span className="loader">Loading</span>}
|
|
321
|
+
viewTransition
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
)
|
|
325
|
+
const cacheView = el.firstElementChild as JSX.Element
|
|
326
|
+
cacheView.updateComponent()
|
|
327
|
+
await flushUpdates()
|
|
328
|
+
|
|
329
|
+
expect(cacheView.querySelector('.loader')).toBeTruthy()
|
|
330
|
+
spy.mockClear()
|
|
331
|
+
|
|
332
|
+
await cache.get('test')
|
|
333
|
+
cacheView.updateComponent()
|
|
334
|
+
await flushUpdates()
|
|
335
|
+
|
|
336
|
+
expect(spy).toHaveBeenCalled()
|
|
337
|
+
})
|
|
338
|
+
delete (document as unknown as Record<string, unknown>).startViewTransition
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('should not call startViewTransition when viewTransition is not set', async () => {
|
|
342
|
+
const spy = mockStartViewTransition()
|
|
343
|
+
await usingAsync(new Cache<string, [string]>({ load: async (key) => `loaded-${key}` }), async (cache) => {
|
|
344
|
+
const el = (
|
|
345
|
+
<div>
|
|
346
|
+
<CacheView
|
|
347
|
+
cache={cache}
|
|
348
|
+
args={['test']}
|
|
349
|
+
content={TestContent}
|
|
350
|
+
loader={<span className="loader">Loading</span>}
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
353
|
+
)
|
|
354
|
+
const cacheView = el.firstElementChild as JSX.Element
|
|
355
|
+
cacheView.updateComponent()
|
|
356
|
+
await flushUpdates()
|
|
357
|
+
|
|
358
|
+
spy.mockClear()
|
|
359
|
+
|
|
360
|
+
await cache.get('test')
|
|
361
|
+
cacheView.updateComponent()
|
|
362
|
+
await flushUpdates()
|
|
363
|
+
|
|
364
|
+
expect(spy).not.toHaveBeenCalled()
|
|
365
|
+
})
|
|
366
|
+
delete (document as unknown as Record<string, unknown>).startViewTransition
|
|
274
367
|
})
|
|
275
368
|
})
|
|
276
369
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Cache, CacheWithValue } from '@furystack/cache'
|
|
2
2
|
import { hasCacheValue, isFailedCacheResult, isObsoleteCacheResult } from '@furystack/cache'
|
|
3
|
-
import type { PartialElement, ShadeComponent } from '@furystack/shades'
|
|
4
|
-
import { Shade, createComponent } from '@furystack/shades'
|
|
3
|
+
import type { PartialElement, ShadeComponent, ViewTransitionConfig } from '@furystack/shades'
|
|
4
|
+
import { Shade, createComponent, transitionedValue } from '@furystack/shades'
|
|
5
5
|
|
|
6
6
|
import { cssVariableTheme } from '../services/css-variable-theme.js'
|
|
7
7
|
import { Button } from './button.js'
|
|
@@ -32,6 +32,7 @@ export type CacheViewProps<
|
|
|
32
32
|
* If not provided, a default Result + retry Button is shown.
|
|
33
33
|
*/
|
|
34
34
|
error?: (error: unknown, retry: () => void) => JSX.Element
|
|
35
|
+
viewTransition?: boolean | ViewTransitionConfig
|
|
35
36
|
} & (keyof Omit<TContentProps, 'data' | keyof PartialElement<HTMLElement>> extends never
|
|
36
37
|
? { contentProps?: never }
|
|
37
38
|
: { contentProps: Omit<TContentProps, 'data' | keyof PartialElement<HTMLElement>> })
|
|
@@ -80,6 +81,7 @@ type InternalCacheViewProps = {
|
|
|
80
81
|
contentProps?: Record<string, unknown>
|
|
81
82
|
loader?: JSX.Element
|
|
82
83
|
error?: (error: unknown, retry: () => void) => JSX.Element
|
|
84
|
+
viewTransition?: boolean | ViewTransitionConfig
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
export const CacheView = Shade<InternalCacheViewProps>({
|
|
@@ -88,13 +90,24 @@ export const CacheView = Shade<InternalCacheViewProps>({
|
|
|
88
90
|
fontFamily: cssVariableTheme.typography.fontFamily,
|
|
89
91
|
},
|
|
90
92
|
render: ({ props, useObservable, useState }): JSX.Element | null => {
|
|
91
|
-
const { cache, args, content, loader, error, contentProps } = props
|
|
93
|
+
const { cache, args, content, loader, error, contentProps, viewTransition } = props
|
|
92
94
|
|
|
93
95
|
const argsKey = JSON.stringify(args)
|
|
94
96
|
const observable = cache.getObservable(...args)
|
|
95
97
|
|
|
96
98
|
const [result] = useObservable(`cache-${argsKey}`, observable)
|
|
97
99
|
|
|
100
|
+
const getCategory = (r: typeof result) =>
|
|
101
|
+
isFailedCacheResult(r) ? 'error' : hasCacheValue(r) ? 'value' : 'loading'
|
|
102
|
+
|
|
103
|
+
const displayedResult = transitionedValue(
|
|
104
|
+
useState,
|
|
105
|
+
'displayedResult',
|
|
106
|
+
result,
|
|
107
|
+
viewTransition,
|
|
108
|
+
(prev, next) => getCategory(prev) !== getCategory(next),
|
|
109
|
+
)
|
|
110
|
+
|
|
98
111
|
const [lastReloadedArgsKey, setLastReloadedArgsKey] = useState<string | null>('lastReloadedArgsKey', null)
|
|
99
112
|
|
|
100
113
|
const retry = () => {
|
|
@@ -104,14 +117,14 @@ export const CacheView = Shade<InternalCacheViewProps>({
|
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
// 1. Error first
|
|
107
|
-
if (isFailedCacheResult(
|
|
120
|
+
if (isFailedCacheResult(displayedResult)) {
|
|
108
121
|
const errorRenderer = error ?? getDefaultErrorUi
|
|
109
|
-
return errorRenderer(
|
|
122
|
+
return errorRenderer(displayedResult.error, retry)
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
// 2. Value next
|
|
113
|
-
if (hasCacheValue(
|
|
114
|
-
if (isObsoleteCacheResult(
|
|
126
|
+
if (hasCacheValue(displayedResult)) {
|
|
127
|
+
if (isObsoleteCacheResult(displayedResult)) {
|
|
115
128
|
if (lastReloadedArgsKey !== argsKey) {
|
|
116
129
|
setLastReloadedArgsKey(argsKey)
|
|
117
130
|
cache.reload(...args).catch(() => {
|
|
@@ -122,7 +135,7 @@ export const CacheView = Shade<InternalCacheViewProps>({
|
|
|
122
135
|
setLastReloadedArgsKey(null)
|
|
123
136
|
}
|
|
124
137
|
return createComponent(content, {
|
|
125
|
-
data:
|
|
138
|
+
data: displayedResult,
|
|
126
139
|
...(contentProps ?? {}),
|
|
127
140
|
}) as unknown as JSX.Element
|
|
128
141
|
}
|
|
@@ -95,7 +95,7 @@ export const CommandPaletteSuggestionList = Shade<{ manager: CommandPaletteManag
|
|
|
95
95
|
ref={containerRef}
|
|
96
96
|
className="suggestion-items-container"
|
|
97
97
|
style={{
|
|
98
|
-
opacity:
|
|
98
|
+
opacity: isOpenedAtRender ? '1' : '0',
|
|
99
99
|
maxHeight: `${window.innerHeight * 0.8}px`,
|
|
100
100
|
width: `calc(${Math.round(hostParentWidth)}px - 3em)`,
|
|
101
101
|
...(props.fullScreenSuggestions ? { left: '0', width: 'calc(100% - 42px)' } : {}),
|
|
@@ -243,6 +243,8 @@ export const Input = Shade<TextInputProps>({
|
|
|
243
243
|
|
|
244
244
|
const themeProvider = injector.getInstance(ThemeProviderService)
|
|
245
245
|
|
|
246
|
+
// We want to use the CSS state hooks for the focused and validity states, so we need to disable the rule
|
|
247
|
+
// eslint-disable-next-line furystack/no-css-state-hooks
|
|
246
248
|
const [focused, setFocused] = useState('focused', props.autofocus || false)
|
|
247
249
|
const [validity, setValidity] = useState('validity', inputRef.current?.validity || emptyValidity)
|
|
248
250
|
|
|
@@ -4,8 +4,8 @@ import { cssVariableTheme } from '../../services/css-variable-theme.js'
|
|
|
4
4
|
import type { Palette } from '../../services/theme-provider-service.js'
|
|
5
5
|
import { ThemeProviderService } from '../../services/theme-provider-service.js'
|
|
6
6
|
import { FormService } from '../form.js'
|
|
7
|
-
import { Icon } from '../icons/icon.js'
|
|
8
7
|
import { check, close } from '../icons/icon-definitions.js'
|
|
8
|
+
import { Icon } from '../icons/icon.js'
|
|
9
9
|
import type { InputValidationResult } from './input.js'
|
|
10
10
|
|
|
11
11
|
export type SelectOption = {
|
|
@@ -384,6 +384,8 @@ export const Select = Shade<SelectProps>({
|
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
const [state, setState] = useState<SelectState>('selectState', initialState)
|
|
387
|
+
// We want to use the CSS state hooks for the focused and dropdown direction states, so we need to disable the rule
|
|
388
|
+
// eslint-disable-next-line furystack/no-css-state-hooks
|
|
387
389
|
const [isFocused, setIsFocused] = useState('isFocused', false)
|
|
388
390
|
const [dropdownDirection, setDropdownDirection] = useState<'up' | 'down'>('dropdownDirection', 'down')
|
|
389
391
|
|