@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
|
@@ -40,16 +40,19 @@ const statusColorMap: Record<ResultStatus, string> = {
|
|
|
40
40
|
'500': paletteMainColors.error.main,
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const
|
|
44
|
-
success:
|
|
45
|
-
error:
|
|
46
|
-
warning:
|
|
47
|
-
info:
|
|
48
|
-
'403':
|
|
49
|
-
'404':
|
|
50
|
-
'500':
|
|
43
|
+
const defaultIconDefs: Record<ResultStatus, typeof checkCircle> = {
|
|
44
|
+
success: checkCircle,
|
|
45
|
+
error: errorCircle,
|
|
46
|
+
warning: warningIcon,
|
|
47
|
+
info: infoIcon,
|
|
48
|
+
'403': forbidden,
|
|
49
|
+
'404': searchOff,
|
|
50
|
+
'500': serverError,
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
const getDefaultIcon = (status: ResultStatus): JSX.Element =>
|
|
54
|
+
(<Icon icon={defaultIconDefs[status]} size={64} />) as unknown as JSX.Element
|
|
55
|
+
|
|
53
56
|
const defaultTitles: Record<ResultStatus, string> = {
|
|
54
57
|
success: 'Success',
|
|
55
58
|
error: 'Error',
|
|
@@ -114,7 +117,7 @@ export const Result = Shade<ResultProps>({
|
|
|
114
117
|
render: ({ props, children, useHostProps }) => {
|
|
115
118
|
const { status, title, subtitle, icon, style } = props
|
|
116
119
|
|
|
117
|
-
const displayIcon = icon ??
|
|
120
|
+
const displayIcon = icon ?? getDefaultIcon(status)
|
|
118
121
|
const statusColor = statusColorMap[status]
|
|
119
122
|
|
|
120
123
|
useHostProps({
|
|
@@ -146,4 +149,8 @@ export const Result = Shade<ResultProps>({
|
|
|
146
149
|
},
|
|
147
150
|
})
|
|
148
151
|
|
|
149
|
-
export {
|
|
152
|
+
export {
|
|
153
|
+
getDefaultIcon as resultGetDefaultIcon,
|
|
154
|
+
defaultIconDefs as resultDefaultIconDefs,
|
|
155
|
+
defaultTitles as resultDefaultTitles,
|
|
156
|
+
}
|
|
@@ -83,21 +83,23 @@ export const Suggest: <T>(props: SuggestProps<T>, children: ChildrenList) => JSX
|
|
|
83
83
|
useHostProps({
|
|
84
84
|
'data-opened': isOpened ? '' : undefined,
|
|
85
85
|
})
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
86
|
+
useDisposable('isLoadingSubscription', () =>
|
|
87
|
+
manager.isLoading.subscribe((isLoading) => {
|
|
88
|
+
const loader = loaderRef.current
|
|
89
|
+
if (!loader) return
|
|
90
|
+
if (isLoading) {
|
|
91
|
+
void promisifyAnimation(loader, [{ opacity: 0 }, { opacity: 1 }], {
|
|
92
|
+
duration: 100,
|
|
93
|
+
fill: 'forwards',
|
|
94
|
+
})
|
|
95
|
+
} else {
|
|
96
|
+
void promisifyAnimation(loader, [{ opacity: 1 }, { opacity: 0 }], {
|
|
97
|
+
duration: 100,
|
|
98
|
+
fill: 'forwards',
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
101
103
|
useDisposable('onSelectSuggestion', () =>
|
|
102
104
|
manager.subscribe('onSelectSuggestion', props.onSelectSuggestion as (entry: unknown) => void),
|
|
103
105
|
)
|
|
@@ -133,6 +135,7 @@ export const Suggest: <T>(props: SuggestProps<T>, children: ChildrenList) => JSX
|
|
|
133
135
|
<div className="post-controls">
|
|
134
136
|
<span ref={loaderRef} style={{ display: 'inline-flex' }}>
|
|
135
137
|
<Loader
|
|
138
|
+
// eslint-disable-next-line furystack/no-direct-get-value-in-render -- Initial opacity only; animated transitions handled by isLoadingSubscription via DOM
|
|
136
139
|
style={{ width: '20px', height: '20px', opacity: manager.isLoading.getValue() ? '1' : '0' }}
|
|
137
140
|
delay={0}
|
|
138
141
|
borderWidth={4}
|
|
@@ -13,6 +13,7 @@ describe('Tabs', () => {
|
|
|
13
13
|
afterEach(() => {
|
|
14
14
|
document.body.innerHTML = ''
|
|
15
15
|
window.location.hash = ''
|
|
16
|
+
delete (document as unknown as Record<string, unknown>).startViewTransition
|
|
16
17
|
})
|
|
17
18
|
|
|
18
19
|
const createTabs = (): Tab[] => [
|
|
@@ -595,4 +596,75 @@ describe('Tabs', () => {
|
|
|
595
596
|
})
|
|
596
597
|
})
|
|
597
598
|
})
|
|
599
|
+
|
|
600
|
+
describe('view transitions', () => {
|
|
601
|
+
const mockStartViewTransition = () => {
|
|
602
|
+
const spy = vi.fn((optionsOrCallback: StartViewTransitionOptions | (() => void)) => {
|
|
603
|
+
const update = typeof optionsOrCallback === 'function' ? optionsOrCallback : optionsOrCallback.update
|
|
604
|
+
update?.()
|
|
605
|
+
return {
|
|
606
|
+
finished: Promise.resolve(),
|
|
607
|
+
ready: Promise.resolve(),
|
|
608
|
+
updateCallbackDone: Promise.resolve(),
|
|
609
|
+
skipTransition: vi.fn(),
|
|
610
|
+
} as unknown as ViewTransition
|
|
611
|
+
})
|
|
612
|
+
document.startViewTransition = spy as typeof document.startViewTransition
|
|
613
|
+
return spy
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
it('should call startViewTransition when viewTransition is enabled and hash changes', async () => {
|
|
617
|
+
const spy = mockStartViewTransition()
|
|
618
|
+
|
|
619
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
620
|
+
window.location.hash = '#tab1'
|
|
621
|
+
|
|
622
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
623
|
+
const tabs = createTabs()
|
|
624
|
+
|
|
625
|
+
initializeShadeRoot({
|
|
626
|
+
injector,
|
|
627
|
+
rootElement,
|
|
628
|
+
jsxElement: <Tabs tabs={tabs} viewTransition />,
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
await flushUpdates()
|
|
632
|
+
spy.mockClear()
|
|
633
|
+
|
|
634
|
+
window.location.hash = '#tab2'
|
|
635
|
+
injector.getInstance(LocationService).updateState()
|
|
636
|
+
await flushUpdates()
|
|
637
|
+
|
|
638
|
+
expect(spy).toHaveBeenCalled()
|
|
639
|
+
expect(document.getElementById('content-2')).toBeTruthy()
|
|
640
|
+
})
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
it('should not call startViewTransition when viewTransition is not set', async () => {
|
|
644
|
+
const spy = mockStartViewTransition()
|
|
645
|
+
|
|
646
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
647
|
+
window.location.hash = '#tab1'
|
|
648
|
+
|
|
649
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
650
|
+
const tabs = createTabs()
|
|
651
|
+
|
|
652
|
+
initializeShadeRoot({
|
|
653
|
+
injector,
|
|
654
|
+
rootElement,
|
|
655
|
+
jsxElement: <Tabs tabs={tabs} />,
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
await flushUpdates()
|
|
659
|
+
spy.mockClear()
|
|
660
|
+
|
|
661
|
+
window.location.hash = '#tab2'
|
|
662
|
+
injector.getInstance(LocationService).updateState()
|
|
663
|
+
await flushUpdates()
|
|
664
|
+
|
|
665
|
+
expect(spy).not.toHaveBeenCalled()
|
|
666
|
+
expect(document.getElementById('content-2')).toBeTruthy()
|
|
667
|
+
})
|
|
668
|
+
})
|
|
669
|
+
})
|
|
598
670
|
})
|
package/src/components/tabs.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ViewTransitionConfig } from '@furystack/shades'
|
|
2
|
+
import { LocationService, Shade, createComponent, transitionedValue } from '@furystack/shades'
|
|
2
3
|
import { buildTransition, cssVariableTheme } from '../services/css-variable-theme.js'
|
|
3
4
|
import { close } from './icons/icon-definitions.js'
|
|
4
5
|
import { Icon } from './icons/icon.js'
|
|
@@ -71,6 +72,7 @@ export const Tabs = Shade<{
|
|
|
71
72
|
onClose?: (key: string) => void
|
|
72
73
|
/** Called when the add button is clicked (only shown when this callback is provided) */
|
|
73
74
|
onAdd?: () => void
|
|
75
|
+
viewTransition?: boolean | ViewTransitionConfig
|
|
74
76
|
}>({
|
|
75
77
|
shadowDomName: 'shade-tabs',
|
|
76
78
|
css: {
|
|
@@ -212,7 +214,7 @@ export const Tabs = Shade<{
|
|
|
212
214
|
borderRight: `1px solid ${cssVariableTheme.background.paper}`,
|
|
213
215
|
},
|
|
214
216
|
},
|
|
215
|
-
render: ({ props, useObservable, injector, useHostProps }) => {
|
|
217
|
+
render: ({ props, useObservable, injector, useHostProps, useState }) => {
|
|
216
218
|
useHostProps({
|
|
217
219
|
...(props.containerStyle ? { style: props.containerStyle as Record<string, string> } : {}),
|
|
218
220
|
...(props.orientation === 'vertical' ? { 'data-orientation': 'vertical' } : {}),
|
|
@@ -224,7 +226,10 @@ export const Tabs = Shade<{
|
|
|
224
226
|
const [hash] = useObservable('updateLocation', injector.getInstance(LocationService).onLocationHashChanged)
|
|
225
227
|
|
|
226
228
|
const activeKey = isControlled ? props.activeKey! : hash.replace('#', '')
|
|
227
|
-
|
|
229
|
+
|
|
230
|
+
const displayedKey = transitionedValue(useState, 'displayedKey', activeKey, props.viewTransition)
|
|
231
|
+
|
|
232
|
+
const displayedTab = props.tabs.find((t) => t.hash === displayedKey)
|
|
228
233
|
|
|
229
234
|
const handleTabClick = (e: MouseEvent, tab: Tab, index: number) => {
|
|
230
235
|
const target = e.target as HTMLElement
|
|
@@ -277,7 +282,7 @@ export const Tabs = Shade<{
|
|
|
277
282
|
</button>
|
|
278
283
|
) : null}
|
|
279
284
|
</div>
|
|
280
|
-
{
|
|
285
|
+
{displayedTab?.component}
|
|
281
286
|
</>
|
|
282
287
|
)
|
|
283
288
|
},
|
|
@@ -89,6 +89,7 @@ export const Tree: <T>(props: TreeProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
89
89
|
|
|
90
90
|
const [flattenedNodes] = useObservable('flattenedNodes', props.treeService.flattenedNodes)
|
|
91
91
|
|
|
92
|
+
// eslint-disable-next-line furystack/require-use-observable-for-render -- Used as persistent ref, not reactive state; read and written synchronously in same render cycle
|
|
92
93
|
const previousItemsRef = useDisposable('previousTreeItems', () => new ObservableValue<Set<unknown>>(new Set()))
|
|
93
94
|
const previousItems = previousItemsRef.getValue()
|
|
94
95
|
const currentItems = new Set<unknown>(flattenedNodes.map((n) => n.item))
|
|
@@ -67,12 +67,13 @@ describe('Wizard', () => {
|
|
|
67
67
|
|
|
68
68
|
afterEach(() => {
|
|
69
69
|
document.body.innerHTML = ''
|
|
70
|
+
delete (document as unknown as Record<string, unknown>).startViewTransition
|
|
70
71
|
})
|
|
71
72
|
|
|
72
73
|
const renderWizard = async (
|
|
73
74
|
steps: Array<(props: WizardStepProps, children: ChildrenList) => JSX.Element>,
|
|
74
75
|
onFinish?: () => void,
|
|
75
|
-
options?: { stepLabels?: string[]; showProgress?: boolean },
|
|
76
|
+
options?: { stepLabels?: string[]; showProgress?: boolean; viewTransition?: boolean },
|
|
76
77
|
) => {
|
|
77
78
|
const injector = new Injector()
|
|
78
79
|
const root = document.getElementById('root')!
|
|
@@ -85,6 +86,7 @@ describe('Wizard', () => {
|
|
|
85
86
|
onFinish={onFinish}
|
|
86
87
|
stepLabels={options?.stepLabels}
|
|
87
88
|
showProgress={options?.showProgress}
|
|
89
|
+
viewTransition={options?.viewTransition}
|
|
88
90
|
/>
|
|
89
91
|
),
|
|
90
92
|
})
|
|
@@ -347,4 +349,58 @@ describe('Wizard', () => {
|
|
|
347
349
|
})
|
|
348
350
|
})
|
|
349
351
|
})
|
|
352
|
+
|
|
353
|
+
describe('view transitions', () => {
|
|
354
|
+
const mockStartViewTransition = () => {
|
|
355
|
+
const spy = vi.fn((optionsOrCallback: StartViewTransitionOptions | (() => void)) => {
|
|
356
|
+
const update = typeof optionsOrCallback === 'function' ? optionsOrCallback : optionsOrCallback.update
|
|
357
|
+
update?.()
|
|
358
|
+
return {
|
|
359
|
+
finished: Promise.resolve(),
|
|
360
|
+
ready: Promise.resolve(),
|
|
361
|
+
updateCallbackDone: Promise.resolve(),
|
|
362
|
+
skipTransition: vi.fn(),
|
|
363
|
+
} as unknown as ViewTransition
|
|
364
|
+
})
|
|
365
|
+
document.startViewTransition = spy as typeof document.startViewTransition
|
|
366
|
+
return spy
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
it('should call startViewTransition on next when viewTransition is enabled', async () => {
|
|
370
|
+
const spy = mockStartViewTransition()
|
|
371
|
+
await usingAsync(
|
|
372
|
+
await renderWizard([Step1, Step2, Step3], undefined, { viewTransition: true }),
|
|
373
|
+
async ({ clickNext, getStepName }) => {
|
|
374
|
+
spy.mockClear()
|
|
375
|
+
await clickNext()
|
|
376
|
+
expect(spy).toHaveBeenCalledTimes(1)
|
|
377
|
+
expect(getStepName()).toBe('step2')
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('should call startViewTransition on prev when viewTransition is enabled', async () => {
|
|
383
|
+
const spy = mockStartViewTransition()
|
|
384
|
+
await usingAsync(
|
|
385
|
+
await renderWizard([Step1, Step2, Step3], undefined, { viewTransition: true }),
|
|
386
|
+
async ({ clickNext, clickPrev, getStepName }) => {
|
|
387
|
+
await clickNext()
|
|
388
|
+
spy.mockClear()
|
|
389
|
+
await clickPrev()
|
|
390
|
+
expect(spy).toHaveBeenCalledTimes(1)
|
|
391
|
+
expect(getStepName()).toBe('step1')
|
|
392
|
+
},
|
|
393
|
+
)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('should not call startViewTransition when viewTransition is not set', async () => {
|
|
397
|
+
const spy = mockStartViewTransition()
|
|
398
|
+
await usingAsync(await renderWizard([Step1, Step2, Step3]), async ({ clickNext, getStepName }) => {
|
|
399
|
+
spy.mockClear()
|
|
400
|
+
await clickNext()
|
|
401
|
+
expect(spy).not.toHaveBeenCalled()
|
|
402
|
+
expect(getStepName()).toBe('step2')
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
})
|
|
350
406
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ChildrenList } from '@furystack/shades'
|
|
2
|
-
import { createComponent, Shade } from '@furystack/shades'
|
|
1
|
+
import type { ChildrenList, ViewTransitionConfig } from '@furystack/shades'
|
|
2
|
+
import { createComponent, maybeViewTransition, Shade } from '@furystack/shades'
|
|
3
3
|
import { cssVariableTheme } from '../../services/css-variable-theme.js'
|
|
4
4
|
import { Paper } from '../paper.js'
|
|
5
5
|
|
|
@@ -39,6 +39,7 @@ export interface WizardProps {
|
|
|
39
39
|
* When true, a progress bar is shown above the content.
|
|
40
40
|
*/
|
|
41
41
|
showProgress?: boolean
|
|
42
|
+
viewTransition?: boolean | ViewTransitionConfig
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export const Wizard = Shade<WizardProps>({
|
|
@@ -182,14 +183,14 @@ export const Wizard = Shade<WizardProps>({
|
|
|
182
183
|
maxPages={props.steps.length}
|
|
183
184
|
onNext={() => {
|
|
184
185
|
if (currentPage < props.steps.length - 1) {
|
|
185
|
-
setCurrentPage(currentPage + 1)
|
|
186
|
+
void maybeViewTransition(props.viewTransition, () => setCurrentPage(currentPage + 1))
|
|
186
187
|
} else {
|
|
187
188
|
props.onFinish?.()
|
|
188
189
|
}
|
|
189
190
|
}}
|
|
190
191
|
onPrev={() => {
|
|
191
192
|
if (currentPage > 0) {
|
|
192
|
-
setCurrentPage(currentPage - 1)
|
|
193
|
+
void maybeViewTransition(props.viewTransition, () => setCurrentPage(currentPage - 1))
|
|
193
194
|
}
|
|
194
195
|
}}
|
|
195
196
|
/>
|