@furystack/shades 13.2.2 → 15.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/esm/component-factory.spec.d.ts.map +1 -1
  3. package/esm/component-factory.spec.js +2 -2
  4. package/esm/component-factory.spec.js.map +1 -1
  5. package/esm/components/define-nested-routes.d.ts +27 -0
  6. package/esm/components/define-nested-routes.d.ts.map +1 -0
  7. package/esm/components/define-nested-routes.js +26 -0
  8. package/esm/components/define-nested-routes.js.map +1 -0
  9. package/esm/components/define-nested-routes.spec.d.ts +2 -0
  10. package/esm/components/define-nested-routes.spec.d.ts.map +1 -0
  11. package/esm/components/define-nested-routes.spec.js +81 -0
  12. package/esm/components/define-nested-routes.spec.js.map +1 -0
  13. package/esm/components/index.d.ts +3 -3
  14. package/esm/components/index.d.ts.map +1 -1
  15. package/esm/components/index.js +3 -3
  16. package/esm/components/index.js.map +1 -1
  17. package/esm/components/nested-hooks.d.ts +62 -0
  18. package/esm/components/nested-hooks.d.ts.map +1 -0
  19. package/esm/components/nested-hooks.js +73 -0
  20. package/esm/components/nested-hooks.js.map +1 -0
  21. package/esm/components/nested-hooks.spec.d.ts +2 -0
  22. package/esm/components/nested-hooks.spec.d.ts.map +1 -0
  23. package/esm/components/nested-hooks.spec.js +211 -0
  24. package/esm/components/nested-hooks.spec.js.map +1 -0
  25. package/esm/components/nested-navigate.d.ts +52 -21
  26. package/esm/components/nested-navigate.d.ts.map +1 -1
  27. package/esm/components/nested-navigate.js +32 -17
  28. package/esm/components/nested-navigate.js.map +1 -1
  29. package/esm/components/nested-navigate.spec.js +109 -19
  30. package/esm/components/nested-navigate.spec.js.map +1 -1
  31. package/esm/components/nested-replace.d.ts +36 -0
  32. package/esm/components/nested-replace.d.ts.map +1 -0
  33. package/esm/components/nested-replace.js +38 -0
  34. package/esm/components/nested-replace.js.map +1 -0
  35. package/esm/components/nested-replace.spec.d.ts +2 -0
  36. package/esm/components/nested-replace.spec.d.ts.map +1 -0
  37. package/esm/components/nested-replace.spec.js +59 -0
  38. package/esm/components/nested-replace.spec.js.map +1 -0
  39. package/esm/components/nested-route-link.d.ts +36 -26
  40. package/esm/components/nested-route-link.d.ts.map +1 -1
  41. package/esm/components/nested-route-link.js +18 -20
  42. package/esm/components/nested-route-link.js.map +1 -1
  43. package/esm/components/nested-route-link.spec.js +67 -17
  44. package/esm/components/nested-route-link.spec.js.map +1 -1
  45. package/esm/components/nested-route-types.d.ts +79 -2
  46. package/esm/components/nested-route-types.d.ts.map +1 -1
  47. package/esm/components/nested-router.d.ts +77 -7
  48. package/esm/components/nested-router.d.ts.map +1 -1
  49. package/esm/components/nested-router.js +139 -13
  50. package/esm/components/nested-router.js.map +1 -1
  51. package/esm/components/nested-router.spec.js +283 -52
  52. package/esm/components/nested-router.spec.js.map +1 -1
  53. package/esm/services/location-service.d.ts +39 -42
  54. package/esm/services/location-service.d.ts.map +1 -1
  55. package/esm/services/location-service.js +115 -134
  56. package/esm/services/location-service.js.map +1 -1
  57. package/esm/services/location-service.spec.js +59 -24
  58. package/esm/services/location-service.spec.js.map +1 -1
  59. package/esm/services/route-match-service.d.ts +3 -2
  60. package/esm/services/route-match-service.d.ts.map +1 -1
  61. package/esm/services/route-match-service.js +13 -22
  62. package/esm/services/route-match-service.js.map +1 -1
  63. package/esm/services/route-match-service.spec.js +42 -49
  64. package/esm/services/route-match-service.spec.js.map +1 -1
  65. package/esm/services/route-meta-utils.d.ts +28 -6
  66. package/esm/services/route-meta-utils.d.ts.map +1 -1
  67. package/esm/services/route-meta-utils.js +18 -4
  68. package/esm/services/route-meta-utils.js.map +1 -1
  69. package/esm/services/route-meta-utils.spec.js +60 -5
  70. package/esm/services/route-meta-utils.spec.js.map +1 -1
  71. package/esm/services/screen-service.d.ts +13 -72
  72. package/esm/services/screen-service.d.ts.map +1 -1
  73. package/esm/services/screen-service.js +45 -137
  74. package/esm/services/screen-service.js.map +1 -1
  75. package/esm/services/screen-service.spec.js +19 -18
  76. package/esm/services/screen-service.spec.js.map +1 -1
  77. package/esm/services/spatial-navigation-service.d.ts +17 -39
  78. package/esm/services/spatial-navigation-service.d.ts.map +1 -1
  79. package/esm/services/spatial-navigation-service.js +280 -313
  80. package/esm/services/spatial-navigation-service.js.map +1 -1
  81. package/esm/services/spatial-navigation-service.spec.js +163 -158
  82. package/esm/services/spatial-navigation-service.spec.js.map +1 -1
  83. package/esm/shade.d.ts +1 -1
  84. package/esm/shade.d.ts.map +1 -1
  85. package/esm/shade.js +10 -7
  86. package/esm/shade.js.map +1 -1
  87. package/esm/vnode.d.ts +1 -1
  88. package/esm/vnode.js +1 -1
  89. package/package.json +5 -4
  90. package/src/component-factory.spec.tsx +3 -2
  91. package/src/components/define-nested-routes.spec.ts +91 -0
  92. package/src/components/define-nested-routes.ts +27 -0
  93. package/src/components/index.ts +3 -3
  94. package/src/components/nested-hooks.spec.ts +254 -0
  95. package/src/components/nested-hooks.ts +109 -0
  96. package/src/components/nested-navigate.spec.ts +136 -32
  97. package/src/components/nested-navigate.ts +75 -26
  98. package/src/components/nested-replace.spec.ts +89 -0
  99. package/src/components/nested-replace.ts +46 -0
  100. package/src/components/nested-route-link.spec.tsx +90 -22
  101. package/src/components/nested-route-link.tsx +54 -29
  102. package/src/components/nested-route-types.ts +82 -2
  103. package/src/components/nested-router.spec.tsx +334 -51
  104. package/src/components/nested-router.tsx +184 -24
  105. package/src/services/location-service.spec.ts +68 -25
  106. package/src/services/location-service.tsx +156 -122
  107. package/src/services/route-match-service.spec.ts +41 -28
  108. package/src/services/route-match-service.ts +17 -8
  109. package/src/services/route-meta-utils.spec.ts +78 -10
  110. package/src/services/route-meta-utils.ts +44 -15
  111. package/src/services/screen-service.spec.ts +19 -18
  112. package/src/services/screen-service.ts +65 -110
  113. package/src/services/spatial-navigation-service.spec.ts +165 -160
  114. package/src/services/spatial-navigation-service.ts +299 -287
  115. package/src/shade.ts +11 -8
  116. package/src/vnode.ts +1 -1
  117. package/esm/components/link-to-route.d.ts +0 -10
  118. package/esm/components/link-to-route.d.ts.map +0 -1
  119. package/esm/components/link-to-route.js +0 -16
  120. package/esm/components/link-to-route.js.map +0 -1
  121. package/esm/components/link-to-route.spec.d.ts +0 -2
  122. package/esm/components/link-to-route.spec.d.ts.map +0 -1
  123. package/esm/components/link-to-route.spec.js +0 -30
  124. package/esm/components/link-to-route.spec.js.map +0 -1
  125. package/esm/components/route-link.d.ts +0 -10
  126. package/esm/components/route-link.d.ts.map +0 -1
  127. package/esm/components/route-link.js +0 -25
  128. package/esm/components/route-link.js.map +0 -1
  129. package/esm/components/route-link.spec.d.ts +0 -2
  130. package/esm/components/route-link.spec.d.ts.map +0 -1
  131. package/esm/components/route-link.spec.js +0 -34
  132. package/esm/components/route-link.spec.js.map +0 -1
  133. package/esm/components/router.d.ts +0 -44
  134. package/esm/components/router.d.ts.map +0 -1
  135. package/esm/components/router.js +0 -66
  136. package/esm/components/router.js.map +0 -1
  137. package/esm/components/router.spec.d.ts +0 -2
  138. package/esm/components/router.spec.d.ts.map +0 -1
  139. package/esm/components/router.spec.js +0 -161
  140. package/esm/components/router.spec.js.map +0 -1
  141. package/src/components/link-to-route.spec.tsx +0 -45
  142. package/src/components/link-to-route.tsx +0 -26
  143. package/src/components/route-link.spec.tsx +0 -44
  144. package/src/components/route-link.tsx +0 -29
  145. package/src/components/router.spec.tsx +0 -217
  146. package/src/components/router.tsx +0 -103
@@ -1,44 +0,0 @@
1
- import { Injector } from '@furystack/inject'
2
- import { usingAsync } from '@furystack/utils'
3
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
- import { initializeShadeRoot } from '../initialize.js'
5
- import { LocationService } from '../services/location-service.js'
6
- import { createComponent } from '../shade-component.js'
7
- import { flushUpdates } from '../shade.js'
8
- import { RouteLink } from './route-link.js'
9
-
10
- describe('RouteLink', () => {
11
- beforeEach(() => {
12
- document.body.innerHTML = '<div id="root"></div>'
13
- })
14
- afterEach(() => {
15
- document.body.innerHTML = ''
16
- })
17
-
18
- it('Shuld display the loader and completed state', async () => {
19
- await usingAsync(new Injector(), async (injector) => {
20
- const rootElement = document.getElementById('root') as HTMLDivElement
21
-
22
- const onRouteChange = vi.fn()
23
-
24
- injector.getInstance(LocationService).onLocationPathChanged.subscribe(onRouteChange)
25
-
26
- initializeShadeRoot({
27
- injector,
28
- rootElement,
29
- jsxElement: (
30
- <RouteLink id="route" href="/subroute">
31
- Link
32
- </RouteLink>
33
- ),
34
- })
35
- await flushUpdates()
36
- expect(document.body.innerHTML).toMatchInlineSnapshot(
37
- `"<div id="root"><a is="route-link" id="route" href="/subroute">Link</a></div>"`,
38
- )
39
- expect(onRouteChange).not.toBeCalled()
40
- document.getElementById('route')?.click()
41
- expect(onRouteChange).toBeCalledTimes(1)
42
- })
43
- })
44
- })
@@ -1,29 +0,0 @@
1
- import type { PartialElement } from '../models/partial-element.js'
2
- import { LocationService } from '../services/location-service.js'
3
- import { createComponent } from '../shade-component.js'
4
- import { Shade } from '../shade.js'
5
-
6
- /** @deprecated Use `NestedRouteLinkProps` from `nested-route-link` instead */
7
- export type RouteLinkProps = PartialElement<Omit<HTMLAnchorElement, 'onclick'>>
8
-
9
- /** @deprecated Use `NestedRouteLink` from `nested-route-link` instead */
10
- export const RouteLink = Shade<RouteLinkProps>({
11
- customElementName: 'route-link',
12
- elementBase: HTMLAnchorElement,
13
- elementBaseName: 'a',
14
- css: {
15
- color: 'inherit',
16
- textDecoration: 'inherit',
17
- },
18
- render: ({ children, props, injector, useHostProps }) => {
19
- useHostProps({
20
- onclick: (ev: MouseEvent) => {
21
- ev.preventDefault()
22
- // eslint-disable-next-line furystack/prefer-location-service -- Deprecated framework component; must call pushState directly.
23
- history.pushState('', props.title || '', props.href)
24
- injector.getInstance(LocationService).updateState()
25
- },
26
- })
27
- return <>{children}</>
28
- },
29
- })
@@ -1,217 +0,0 @@
1
- import { Injector } from '@furystack/inject'
2
- import { sleepAsync, usingAsync } from '@furystack/utils'
3
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
- import { initializeShadeRoot } from '../initialize.js'
5
- import { LocationService } from '../services/location-service.js'
6
- import { createComponent } from '../shade-component.js'
7
- import { flushUpdates } from '../shade.js'
8
- import { RouteLink } from './route-link.js'
9
- import { Router, type Route } from './router.js'
10
-
11
- describe('Router', () => {
12
- beforeEach(() => {
13
- document.body.innerHTML = '<div id="root"></div>'
14
- })
15
- afterEach(() => {
16
- document.body.innerHTML = ''
17
- })
18
-
19
- it('should skip intermediate route when navigating rapidly (latest wins)', async () => {
20
- await usingAsync(new Injector(), async (injector) => {
21
- history.pushState(null, '', '/route-a')
22
-
23
- const callOrder: string[] = []
24
-
25
- const onVisitA = vi.fn(async () => {
26
- callOrder.push('visit-a')
27
- })
28
- const onLeaveA = vi.fn(async () => {
29
- callOrder.push('leave-a')
30
- })
31
- const onVisitB = vi.fn(async () => {
32
- await sleepAsync(200)
33
- callOrder.push('visit-b')
34
- })
35
- const onLeaveB = vi.fn(async () => {
36
- callOrder.push('leave-b')
37
- })
38
- const onVisitC = vi.fn(async () => {
39
- callOrder.push('visit-c')
40
- })
41
-
42
- const routeA: Route = {
43
- url: '/route-a',
44
- component: () => <div id="content">route-a</div>,
45
- onVisit: onVisitA,
46
- onLeave: onLeaveA,
47
- }
48
- const routeB: Route = {
49
- url: '/route-b',
50
- component: () => <div id="content">route-b</div>,
51
- onVisit: onVisitB,
52
- onLeave: onLeaveB,
53
- }
54
- const routeC: Route = {
55
- url: '/route-c',
56
- component: () => <div id="content">route-c</div>,
57
- onVisit: onVisitC,
58
- }
59
-
60
- const rootElement = document.getElementById('root') as HTMLDivElement
61
-
62
- initializeShadeRoot({
63
- injector,
64
- rootElement,
65
- jsxElement: (
66
- <div>
67
- <RouteLink id="go-a" href="/route-a">
68
- a
69
- </RouteLink>
70
- <RouteLink id="go-b" href="/route-b">
71
- b
72
- </RouteLink>
73
- <RouteLink id="go-c" href="/route-c">
74
- c
75
- </RouteLink>
76
- <Router routes={[routeA, routeB, routeC]} />
77
- </div>
78
- ),
79
- })
80
-
81
- const getContent = () => document.getElementById('content')?.innerHTML
82
- const clickOn = (name: string) => document.getElementById(name)?.click()
83
-
84
- // --- Initial load at /route-a ---
85
- await flushUpdates()
86
- expect(getContent()).toBe('route-a')
87
- expect(onVisitA).toHaveBeenCalledTimes(1)
88
-
89
- // --- Rapid navigation: click B then immediately C ---
90
- callOrder.length = 0
91
- clickOn('go-b')
92
- clickOn('go-c')
93
-
94
- // Wait long enough for both transitions to settle (onVisitB has 200ms delay)
95
- await sleepAsync(500)
96
-
97
- // The final destination should be route-c
98
- expect(getContent()).toBe('route-c')
99
- expect(onVisitC).toHaveBeenCalledTimes(1)
100
-
101
- // route-b's onVisit should have been abandoned
102
- expect(callOrder).not.toContain('visit-b')
103
- })
104
- })
105
-
106
- it('Shuld display the loader and completed state', async () => {
107
- await usingAsync(new Injector(), async (injector) => {
108
- history.pushState(null, '', '/')
109
-
110
- const onVisit = vi.fn()
111
- const onLeave = vi.fn()
112
- const onLastLeave = vi.fn()
113
-
114
- const rootElement = document.getElementById('root') as HTMLDivElement
115
-
116
- const onRouteChange = vi.fn()
117
-
118
- injector.getInstance(LocationService).onLocationPathChanged.subscribe(onRouteChange)
119
-
120
- const route1: Route = {
121
- url: '/route-a',
122
- component: () => <div id="content">route-a</div>,
123
- onVisit,
124
- onLeave,
125
- }
126
-
127
- const route2: Route<{ id: string }> = {
128
- url: '/route-b{/:id}',
129
- component: ({ match }) => <div id="content">route-b{match.params.id}</div>,
130
- }
131
-
132
- const route3: Route = {
133
- url: '/route-c',
134
- component: () => <div id="content">route-c</div>,
135
- onLeave: onLastLeave,
136
- }
137
-
138
- const route4 = { url: '/', component: () => <div id="content">home</div> }
139
-
140
- initializeShadeRoot({
141
- injector,
142
- rootElement,
143
- jsxElement: (
144
- <div>
145
- <RouteLink id="home" href="/">
146
- home
147
- </RouteLink>
148
- <RouteLink id="a" href="/route-a">
149
- a
150
- </RouteLink>
151
- <RouteLink id="b" href="/route-b">
152
- b
153
- </RouteLink>
154
- <RouteLink id="b-with-id" href="/route-b/123">
155
- b-with-id
156
- </RouteLink>
157
- <RouteLink id="c" href="/route-c">
158
- c
159
- </RouteLink>
160
- <RouteLink id="x" href="/route-x">
161
- x
162
- </RouteLink>
163
- <Router routes={[route1, route2, route3, route4]} notFound={<div id="content">not found</div>} />
164
- </div>
165
- ),
166
- })
167
-
168
- const getContent = () => document.getElementById('content')?.innerHTML
169
- const getLocation = () => location.pathname
170
-
171
- const clickOn = (name: string) => document.getElementById(name)?.click()
172
-
173
- await flushUpdates()
174
-
175
- expect(getLocation()).toBe('/')
176
- expect(getContent()).toBe('home')
177
-
178
- expect(onVisit).not.toBeCalled()
179
-
180
- clickOn('a')
181
- await flushUpdates()
182
- expect(getContent()).toBe('route-a')
183
- expect(getLocation()).toBe('/route-a')
184
- expect(onRouteChange).toBeCalledTimes(1)
185
- expect(onVisit).toBeCalledTimes(1)
186
-
187
- clickOn('a')
188
- await flushUpdates()
189
- expect(onVisit).toBeCalledTimes(1)
190
- expect(onLeave).not.toBeCalled()
191
-
192
- clickOn('b')
193
- await flushUpdates()
194
- expect(onLeave).toBeCalledTimes(1)
195
-
196
- expect(getContent()).toBe('route-b')
197
- expect(getLocation()).toBe('/route-b')
198
-
199
- clickOn('b-with-id')
200
- await flushUpdates()
201
- expect(getContent()).toBe('route-b123')
202
- expect(getLocation()).toBe('/route-b/123')
203
-
204
- clickOn('c')
205
- await flushUpdates()
206
- expect(getContent()).toBe('route-c')
207
- expect(getLocation()).toBe('/route-c')
208
-
209
- expect(onLastLeave).not.toBeCalled()
210
- clickOn('x')
211
- await flushUpdates()
212
- expect(getContent()).toBe('not found')
213
- expect(getLocation()).toBe('/route-x')
214
- expect(onLastLeave).toBeCalledTimes(1)
215
- })
216
- })
217
- })
@@ -1,103 +0,0 @@
1
- import { ObservableAlreadyDisposedError } from '@furystack/utils'
2
- import type { MatchOptions, MatchResult } from 'path-to-regexp'
3
- import { match } from 'path-to-regexp'
4
- import type { RenderOptions } from '../models/render-options.js'
5
- import { LocationService } from '../services/location-service.js'
6
- import { createComponent } from '../shade-component.js'
7
- import { Shade } from '../shade.js'
8
-
9
- /**
10
- * @deprecated Use NestedRouter instead
11
- */
12
- export interface Route<TMatchResult = unknown> {
13
- url: string
14
- component: (options: {
15
- currentUrl: string
16
- match: MatchResult<TMatchResult extends object ? TMatchResult : object>
17
- }) => JSX.Element
18
- routingOptions?: MatchOptions
19
- onVisit?: (options: RenderOptions<unknown> & { element: JSX.Element }) => Promise<void>
20
- onLeave?: (options: RenderOptions<unknown> & { element: JSX.Element }) => Promise<void>
21
- }
22
-
23
- /**
24
- * @deprecated Use NestedRouterProps instead
25
- */
26
- export interface RouterProps {
27
- style?: CSSStyleDeclaration
28
- routes: Array<Route<any>>
29
- notFound?: JSX.Element
30
- }
31
-
32
- /**
33
- * @deprecated Use NestedRouterState instead
34
- */
35
- export interface RouterState {
36
- activeRoute?: Route<unknown> | null
37
- activeRouteParams?: unknown
38
- jsx: JSX.Element
39
- }
40
-
41
- /**
42
- * @deprecated Use NestedRouter instead
43
- */
44
- export const Router = Shade<RouterProps>({
45
- customElementName: 'shade-router',
46
- render: (options) => {
47
- const { useState, useObservable, injector } = options
48
- const [versionRef] = useState('navVersion', { current: 0 })
49
- const [state, setState] = useState<RouterState>('routerState', {
50
- jsx: <div />,
51
- })
52
-
53
- const updateUrl = async (currentUrl: string) => {
54
- const [lastState] = useState<RouterState>('routerState', state)
55
- const { activeRoute: lastRoute, activeRouteParams: lastRouteParams, jsx: lastJsx } = lastState
56
- try {
57
- for (const route of options.props.routes) {
58
- const matchFn = match(route.url, route.routingOptions)
59
- const matchResult = matchFn(currentUrl)
60
- if (matchResult) {
61
- if (route !== lastRoute || JSON.stringify(lastRouteParams) !== JSON.stringify(matchResult.params)) {
62
- const version = ++versionRef.current
63
- await lastRoute?.onLeave?.({ ...options, element: lastState.jsx })
64
- if (version !== versionRef.current) return
65
- const newJsx = route.component({ currentUrl, match: matchResult })
66
- setState({ jsx: newJsx, activeRoute: route, activeRouteParams: matchResult.params })
67
- await route.onVisit?.({ ...options, element: newJsx })
68
- if (version !== versionRef.current) return
69
- }
70
- return
71
- }
72
- }
73
-
74
- if (lastRoute !== null) {
75
- const version = ++versionRef.current
76
- await lastRoute?.onLeave?.({ ...options, element: lastJsx })
77
- if (version !== versionRef.current) return
78
- setState({
79
- jsx: options.props.notFound || <div />,
80
- activeRoute: null,
81
- activeRouteParams: null,
82
- })
83
- }
84
- } catch (e) {
85
- if (!(e instanceof ObservableAlreadyDisposedError)) {
86
- throw e
87
- }
88
- }
89
- }
90
-
91
- const [locationPath] = useObservable(
92
- 'locationPathChanged',
93
- injector.getInstance(LocationService).onLocationPathChanged,
94
- {
95
- onChange: (newValue) => {
96
- void updateUrl(newValue)
97
- },
98
- },
99
- )
100
- void updateUrl(locationPath)
101
- return state.jsx
102
- },
103
- })