@furystack/shades 12.4.0 → 13.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.
- package/CHANGELOG.md +60 -0
- package/README.md +3 -3
- package/esm/component-factory.spec.js +3 -3
- package/esm/component-factory.spec.js.map +1 -1
- package/esm/components/lazy-load.js +1 -1
- package/esm/components/lazy-load.js.map +1 -1
- package/esm/components/link-to-route.js +1 -1
- package/esm/components/link-to-route.js.map +1 -1
- package/esm/components/nested-route-link.d.ts.map +1 -1
- package/esm/components/nested-route-link.js +2 -1
- package/esm/components/nested-route-link.js.map +1 -1
- package/esm/components/nested-router.js +1 -1
- package/esm/components/nested-router.js.map +1 -1
- package/esm/components/route-link.d.ts.map +1 -1
- package/esm/components/route-link.js +2 -1
- package/esm/components/route-link.js.map +1 -1
- package/esm/components/router.js +1 -1
- package/esm/components/router.js.map +1 -1
- package/esm/css-generator.d.ts +1 -1
- package/esm/css-generator.js +1 -1
- package/esm/services/location-service.d.ts +13 -0
- package/esm/services/location-service.d.ts.map +1 -1
- package/esm/services/location-service.js +21 -1
- package/esm/services/location-service.js.map +1 -1
- package/esm/services/screen-service.d.ts.map +1 -1
- package/esm/services/screen-service.js +4 -0
- package/esm/services/screen-service.js.map +1 -1
- package/esm/shade-host-props-ref.integration.spec.js +13 -13
- package/esm/shade-host-props-ref.integration.spec.js.map +1 -1
- package/esm/shade-resources.integration.spec.js +5 -5
- package/esm/shade-resources.integration.spec.js.map +1 -1
- package/esm/shade.d.ts +1 -1
- package/esm/shade.d.ts.map +1 -1
- package/esm/shade.js +2 -3
- package/esm/shade.js.map +1 -1
- package/esm/shade.spec.js +15 -14
- package/esm/shade.spec.js.map +1 -1
- package/esm/shades.integration.spec.js +17 -17
- package/esm/shades.integration.spec.js.map +1 -1
- package/esm/style-manager.d.ts +4 -4
- package/esm/style-manager.d.ts.map +1 -1
- package/esm/style-manager.js +9 -9
- package/esm/style-manager.js.map +1 -1
- package/esm/style-manager.spec.js +3 -3
- package/esm/style-manager.spec.js.map +1 -1
- package/esm/vnode.integration.spec.js +18 -16
- package/esm/vnode.integration.spec.js.map +1 -1
- package/package.json +5 -5
- package/src/component-factory.spec.tsx +3 -3
- package/src/components/lazy-load.tsx +1 -1
- package/src/components/link-to-route.tsx +1 -1
- package/src/components/nested-route-link.tsx +2 -1
- package/src/components/nested-router.tsx +1 -1
- package/src/components/route-link.tsx +2 -1
- package/src/components/router.tsx +1 -1
- package/src/css-generator.ts +1 -1
- package/src/services/location-service.tsx +22 -1
- package/src/services/screen-service.ts +4 -0
- package/src/shade-host-props-ref.integration.spec.tsx +13 -13
- package/src/shade-resources.integration.spec.tsx +6 -5
- package/src/shade.spec.tsx +15 -14
- package/src/shade.ts +3 -4
- package/src/shades.integration.spec.tsx +17 -17
- package/src/style-manager.spec.ts +3 -3
- package/src/style-manager.ts +9 -9
- package/src/vnode.integration.spec.tsx +18 -16
|
@@ -39,10 +39,11 @@ export class LocationService implements Disposable {
|
|
|
39
39
|
public [Symbol.dispose]() {
|
|
40
40
|
window.removeEventListener('popstate', this.popStateListener)
|
|
41
41
|
window.removeEventListener('hashchange', this.hashChangeListener)
|
|
42
|
-
this.onLocationPathChanged[Symbol.dispose]()
|
|
43
42
|
this.onLocationSearchChanged[Symbol.dispose]()
|
|
44
43
|
this.onDeserializedLocationSearchChanged[Symbol.dispose]()
|
|
45
44
|
this.locationDeserializerObserver[Symbol.dispose]()
|
|
45
|
+
this.onLocationPathChanged[Symbol.dispose]()
|
|
46
|
+
this.onLocationHashChanged[Symbol.dispose]()
|
|
46
47
|
|
|
47
48
|
window.history.pushState = this.originalPushState
|
|
48
49
|
window.history.replaceState = this.originalReplaceState
|
|
@@ -69,6 +70,10 @@ export class LocationService implements Disposable {
|
|
|
69
70
|
this.onDeserializedLocationSearchChanged.setValue(this.deserializeQueryString(search))
|
|
70
71
|
})
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Synchronizes the observable state with the current browser location.
|
|
75
|
+
* Called internally after navigation events and history state changes.
|
|
76
|
+
*/
|
|
72
77
|
public updateState = (() => {
|
|
73
78
|
this.onLocationPathChanged.setValue(location.pathname)
|
|
74
79
|
this.onLocationHashChanged.setValue(location.hash.replace('#', ''))
|
|
@@ -80,10 +85,25 @@ export class LocationService implements Disposable {
|
|
|
80
85
|
* The LocationService interceptor ensures routing state is updated correctly.
|
|
81
86
|
*/
|
|
82
87
|
public navigate(path: string): void {
|
|
88
|
+
// eslint-disable-next-line furystack/prefer-location-service -- This IS the LocationService.navigate() implementation.
|
|
83
89
|
history.pushState(null, '', path)
|
|
84
90
|
this.updateState()
|
|
85
91
|
}
|
|
86
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Replace the current history entry with a new path. Use this instead of
|
|
95
|
+
* raw history.replaceState for SPA redirects (e.g. the intermediate URL
|
|
96
|
+
* should not appear in the browser's back/forward stack).
|
|
97
|
+
*/
|
|
98
|
+
public replace(path: string): void {
|
|
99
|
+
// eslint-disable-next-line furystack/prefer-location-service -- This IS the LocationService.replace() implementation.
|
|
100
|
+
history.replaceState(null, '', path)
|
|
101
|
+
this.updateState()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Internal cache of per-key search parameter observables created by {@link useSearchParam}.
|
|
106
|
+
*/
|
|
87
107
|
public readonly searchParamObservables = new Map<string, ObservableValue<any>>()
|
|
88
108
|
|
|
89
109
|
/**
|
|
@@ -110,6 +130,7 @@ export class LocationService implements Disposable {
|
|
|
110
130
|
if (currentQueryStringObject[key] !== value) {
|
|
111
131
|
const params = this.serializeToQueryString({ ...currentQueryStringObject, [key]: value })
|
|
112
132
|
const newUrl = `${location.pathname}?${params}`
|
|
133
|
+
// eslint-disable-next-line furystack/prefer-location-service -- Internal LocationService plumbing for search param sync.
|
|
113
134
|
history.pushState({}, '', newUrl)
|
|
114
135
|
}
|
|
115
136
|
})
|
|
@@ -83,6 +83,10 @@ export class ScreenService implements Disposable {
|
|
|
83
83
|
*/
|
|
84
84
|
public [Symbol.dispose]() {
|
|
85
85
|
window.removeEventListener('resize', this.onResizeListener)
|
|
86
|
+
this.orientation[Symbol.dispose]()
|
|
87
|
+
Object.values(this.screenSize.atLeast).forEach((observable) => {
|
|
88
|
+
observable[Symbol.dispose]()
|
|
89
|
+
})
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
/**
|
|
@@ -18,7 +18,7 @@ describe('useHostProps integration tests', () => {
|
|
|
18
18
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
19
19
|
|
|
20
20
|
const ExampleComponent = Shade<{ variant: string }>({
|
|
21
|
-
|
|
21
|
+
customElementName: 'host-props-data-attr-test',
|
|
22
22
|
render: ({ props, useHostProps }) => {
|
|
23
23
|
useHostProps({
|
|
24
24
|
'data-variant': props.variant,
|
|
@@ -45,7 +45,7 @@ describe('useHostProps integration tests', () => {
|
|
|
45
45
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
46
46
|
|
|
47
47
|
const ExampleComponent = Shade({
|
|
48
|
-
|
|
48
|
+
customElementName: 'host-props-aria-test',
|
|
49
49
|
render: ({ useHostProps }) => {
|
|
50
50
|
useHostProps({
|
|
51
51
|
role: 'progressbar',
|
|
@@ -77,7 +77,7 @@ describe('useHostProps integration tests', () => {
|
|
|
77
77
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
78
78
|
|
|
79
79
|
const ExampleComponent = Shade({
|
|
80
|
-
|
|
80
|
+
customElementName: 'host-props-css-vars-test',
|
|
81
81
|
render: ({ useHostProps }) => {
|
|
82
82
|
useHostProps({
|
|
83
83
|
style: {
|
|
@@ -107,7 +107,7 @@ describe('useHostProps integration tests', () => {
|
|
|
107
107
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
108
108
|
|
|
109
109
|
const ExampleComponent = Shade({
|
|
110
|
-
|
|
110
|
+
customElementName: 'host-props-inline-style-test',
|
|
111
111
|
render: ({ useHostProps }) => {
|
|
112
112
|
useHostProps({
|
|
113
113
|
style: {
|
|
@@ -137,7 +137,7 @@ describe('useHostProps integration tests', () => {
|
|
|
137
137
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
138
138
|
|
|
139
139
|
const ExampleComponent = Shade({
|
|
140
|
-
|
|
140
|
+
customElementName: 'host-props-merge-test',
|
|
141
141
|
render: ({ useHostProps }) => {
|
|
142
142
|
useHostProps({
|
|
143
143
|
'data-first': 'one',
|
|
@@ -172,7 +172,7 @@ describe('useHostProps integration tests', () => {
|
|
|
172
172
|
const showExtra = new ObservableValue(true)
|
|
173
173
|
|
|
174
174
|
const ExampleComponent = Shade({
|
|
175
|
-
|
|
175
|
+
customElementName: 'host-props-remove-attr-test',
|
|
176
176
|
render: ({ useHostProps, useObservable }) => {
|
|
177
177
|
const [show] = useObservable('showExtra', showExtra)
|
|
178
178
|
useHostProps({
|
|
@@ -208,7 +208,7 @@ describe('useHostProps integration tests', () => {
|
|
|
208
208
|
const showColor = new ObservableValue(true)
|
|
209
209
|
|
|
210
210
|
const ExampleComponent = Shade({
|
|
211
|
-
|
|
211
|
+
customElementName: 'host-props-remove-css-var-test',
|
|
212
212
|
render: ({ useHostProps, useObservable }) => {
|
|
213
213
|
const [show] = useObservable('showColor', showColor)
|
|
214
214
|
useHostProps({
|
|
@@ -246,7 +246,7 @@ describe('useHostProps integration tests', () => {
|
|
|
246
246
|
let clicked = false
|
|
247
247
|
|
|
248
248
|
const ExampleComponent = Shade({
|
|
249
|
-
|
|
249
|
+
customElementName: 'host-props-event-test',
|
|
250
250
|
render: ({ useHostProps }) => {
|
|
251
251
|
useHostProps({
|
|
252
252
|
onclick: () => {
|
|
@@ -286,7 +286,7 @@ describe('useRef integration tests', () => {
|
|
|
286
286
|
let capturedRef: { readonly current: HTMLDivElement | null } | undefined
|
|
287
287
|
|
|
288
288
|
const ExampleComponent = Shade({
|
|
289
|
-
|
|
289
|
+
customElementName: 'use-ref-basic-test',
|
|
290
290
|
render: ({ useRef }) => {
|
|
291
291
|
const divRef = useRef<HTMLDivElement>('myDiv')
|
|
292
292
|
capturedRef = divRef
|
|
@@ -320,7 +320,7 @@ describe('useRef integration tests', () => {
|
|
|
320
320
|
const capturedRefs: Array<{ readonly current: Element | null }> = []
|
|
321
321
|
|
|
322
322
|
const ExampleComponent = Shade({
|
|
323
|
-
|
|
323
|
+
customElementName: 'use-ref-stable-test',
|
|
324
324
|
render: ({ useRef, useObservable }) => {
|
|
325
325
|
const [count] = useObservable('counter', counter)
|
|
326
326
|
const divRef = useRef('myDiv')
|
|
@@ -355,7 +355,7 @@ describe('useRef integration tests', () => {
|
|
|
355
355
|
let capturedInputRef: { readonly current: HTMLInputElement | null } | undefined
|
|
356
356
|
|
|
357
357
|
const ExampleComponent = Shade({
|
|
358
|
-
|
|
358
|
+
customElementName: 'use-ref-nested-test',
|
|
359
359
|
render: ({ useRef }) => {
|
|
360
360
|
const inputRef = useRef<HTMLInputElement>('input')
|
|
361
361
|
capturedInputRef = inputRef
|
|
@@ -391,7 +391,7 @@ describe('useRef integration tests', () => {
|
|
|
391
391
|
let capturedRef: { readonly current: HTMLSpanElement | null } | undefined
|
|
392
392
|
|
|
393
393
|
const ExampleComponent = Shade({
|
|
394
|
-
|
|
394
|
+
customElementName: 'use-ref-unmount-test',
|
|
395
395
|
render: ({ useRef, useObservable }) => {
|
|
396
396
|
const [show] = useObservable('showChild', showChild)
|
|
397
397
|
const spanRef = useRef<HTMLSpanElement>('span')
|
|
@@ -423,7 +423,7 @@ describe('useRef integration tests', () => {
|
|
|
423
423
|
const counter = new ObservableValue(0)
|
|
424
424
|
|
|
425
425
|
const ExampleComponent = Shade({
|
|
426
|
-
|
|
426
|
+
customElementName: 'use-ref-onchange-test',
|
|
427
427
|
render: ({ useRef, useObservable }) => {
|
|
428
428
|
const spanRef = useRef<HTMLSpanElement>('counterSpan')
|
|
429
429
|
useObservable('counter', counter, {
|
|
@@ -35,7 +35,7 @@ describe('Shade Resources integration tests', () => {
|
|
|
35
35
|
</div>
|
|
36
36
|
)
|
|
37
37
|
},
|
|
38
|
-
|
|
38
|
+
customElementName: 'shades-example-resource',
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
expect(obs1.getObservers().length).toBe(0)
|
|
@@ -99,7 +99,7 @@ describe('Shade Resources integration tests', () => {
|
|
|
99
99
|
renderCounter()
|
|
100
100
|
return <div id="val">{value}</div>
|
|
101
101
|
},
|
|
102
|
-
|
|
102
|
+
customElementName: 'shades-example-custom-onchange',
|
|
103
103
|
})
|
|
104
104
|
|
|
105
105
|
initializeShadeRoot({
|
|
@@ -162,11 +162,12 @@ describe('Shade Resources integration tests', () => {
|
|
|
162
162
|
renderCounter()
|
|
163
163
|
return (
|
|
164
164
|
<div ref={valRef} id="manual-val">
|
|
165
|
+
{/* eslint-disable-next-line furystack/no-direct-get-value-in-render -- Test: verifying manual DOM update pattern with onChange callback */}
|
|
165
166
|
{obs.getValue()}
|
|
166
167
|
</div>
|
|
167
168
|
)
|
|
168
169
|
},
|
|
169
|
-
|
|
170
|
+
customElementName: 'shades-example-manual-dom-update',
|
|
170
171
|
})
|
|
171
172
|
|
|
172
173
|
initializeShadeRoot({
|
|
@@ -227,7 +228,7 @@ describe('Shade Resources integration tests', () => {
|
|
|
227
228
|
</div>
|
|
228
229
|
)
|
|
229
230
|
},
|
|
230
|
-
|
|
231
|
+
customElementName: 'shades-example-batching',
|
|
231
232
|
})
|
|
232
233
|
|
|
233
234
|
initializeShadeRoot({
|
|
@@ -272,7 +273,7 @@ describe('Shade Resources integration tests', () => {
|
|
|
272
273
|
renderCounter()
|
|
273
274
|
return <div>content</div>
|
|
274
275
|
},
|
|
275
|
-
|
|
276
|
+
customElementName: 'shades-example-update-batching',
|
|
276
277
|
})
|
|
277
278
|
|
|
278
279
|
initializeShadeRoot({
|
package/src/shade.spec.tsx
CHANGED
|
@@ -13,18 +13,18 @@ describe('Shade edge cases', () => {
|
|
|
13
13
|
document.body.innerHTML = ''
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
describe('duplicate
|
|
17
|
-
it('should throw an error when registering a duplicate
|
|
16
|
+
describe('duplicate customElementName error', () => {
|
|
17
|
+
it('should throw an error when registering a duplicate customElementName', () => {
|
|
18
18
|
// First registration should succeed
|
|
19
19
|
Shade({
|
|
20
|
-
|
|
20
|
+
customElementName: 'shade-duplicate-test',
|
|
21
21
|
render: () => <div>First</div>,
|
|
22
22
|
})
|
|
23
23
|
|
|
24
24
|
// Second registration with the same name should throw
|
|
25
25
|
expect(() => {
|
|
26
26
|
Shade({
|
|
27
|
-
|
|
27
|
+
customElementName: 'shade-duplicate-test',
|
|
28
28
|
render: () => <div>Second</div>,
|
|
29
29
|
})
|
|
30
30
|
}).toThrow("A custom shade with name 'shade-duplicate-test' has already been registered!")
|
|
@@ -34,13 +34,13 @@ describe('Shade edge cases', () => {
|
|
|
34
34
|
const uniqueName = `shade-duplicate-name-in-error-${Date.now()}`
|
|
35
35
|
|
|
36
36
|
Shade({
|
|
37
|
-
|
|
37
|
+
customElementName: uniqueName,
|
|
38
38
|
render: () => <div>First</div>,
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
try {
|
|
42
42
|
Shade({
|
|
43
|
-
|
|
43
|
+
customElementName: uniqueName,
|
|
44
44
|
render: () => <div>Second</div>,
|
|
45
45
|
})
|
|
46
46
|
// Should not reach here
|
|
@@ -61,7 +61,7 @@ describe('Shade edge cases', () => {
|
|
|
61
61
|
let childCapturedInjector: Injector | undefined
|
|
62
62
|
|
|
63
63
|
const ChildComponent = Shade<{ injector?: Injector }>({
|
|
64
|
-
|
|
64
|
+
customElementName: 'shade-injector-child-props-test',
|
|
65
65
|
render: ({ injector }) => {
|
|
66
66
|
childCapturedInjector = injector
|
|
67
67
|
return <div>Child</div>
|
|
@@ -69,7 +69,7 @@ describe('Shade edge cases', () => {
|
|
|
69
69
|
})
|
|
70
70
|
|
|
71
71
|
const ParentComponent = Shade({
|
|
72
|
-
|
|
72
|
+
customElementName: 'shade-injector-parent-props-test',
|
|
73
73
|
render: ({ injector, children }) => {
|
|
74
74
|
parentCapturedInjector = injector
|
|
75
75
|
return <div>{children}</div>
|
|
@@ -105,7 +105,7 @@ describe('Shade edge cases', () => {
|
|
|
105
105
|
const renderCounter = vi.fn()
|
|
106
106
|
|
|
107
107
|
const ExampleComponent = Shade({
|
|
108
|
-
|
|
108
|
+
customElementName: 'shade-no-render-after-disconnect',
|
|
109
109
|
render: () => {
|
|
110
110
|
renderCounter()
|
|
111
111
|
return <div>content</div>
|
|
@@ -140,7 +140,7 @@ describe('Shade edge cases', () => {
|
|
|
140
140
|
const renderCounter = vi.fn()
|
|
141
141
|
|
|
142
142
|
const ExampleComponent = Shade({
|
|
143
|
-
|
|
143
|
+
customElementName: 'shade-no-sync-render-after-disconnect',
|
|
144
144
|
render: () => {
|
|
145
145
|
renderCounter()
|
|
146
146
|
return <div>content</div>
|
|
@@ -174,7 +174,7 @@ describe('Shade edge cases', () => {
|
|
|
174
174
|
const obs = new ObservableValue(0)
|
|
175
175
|
|
|
176
176
|
const ExampleComponent = Shade({
|
|
177
|
-
|
|
177
|
+
customElementName: 'shade-no-render-during-disposal',
|
|
178
178
|
render: ({ useObservable, useDisposable }) => {
|
|
179
179
|
useObservable('obs', obs)
|
|
180
180
|
useDisposable('cleanup', () => ({
|
|
@@ -183,6 +183,7 @@ describe('Shade edge cases', () => {
|
|
|
183
183
|
},
|
|
184
184
|
}))
|
|
185
185
|
renderCounter()
|
|
186
|
+
// eslint-disable-next-line furystack/no-direct-get-value-in-render -- Test: verifying no re-render during disposal; already subscribed via useObservable above
|
|
186
187
|
return <div>{obs.getValue()}</div>
|
|
187
188
|
},
|
|
188
189
|
})
|
|
@@ -226,7 +227,7 @@ describe('Shade edge cases', () => {
|
|
|
226
227
|
const stateKey = 'broadcast-test-key'
|
|
227
228
|
|
|
228
229
|
const ExampleComponent = Shade({
|
|
229
|
-
|
|
230
|
+
customElementName: 'shade-broadcast-channel-test',
|
|
230
231
|
render: ({ useStoredState }) => {
|
|
231
232
|
const [value] = useStoredState(stateKey, 'initial', store)
|
|
232
233
|
return <div id="value">{value}</div>
|
|
@@ -270,7 +271,7 @@ describe('Shade edge cases', () => {
|
|
|
270
271
|
const stateKey = 'broadcast-filter-test-key'
|
|
271
272
|
|
|
272
273
|
const ExampleComponent = Shade({
|
|
273
|
-
|
|
274
|
+
customElementName: 'shade-broadcast-channel-filter-test',
|
|
274
275
|
render: ({ useStoredState }) => {
|
|
275
276
|
const [value] = useStoredState(stateKey, 'initial', store)
|
|
276
277
|
return <div id="value">{value}</div>
|
|
@@ -315,7 +316,7 @@ describe('Shade edge cases', () => {
|
|
|
315
316
|
const stateKey = 'broadcast-cleanup-test-key'
|
|
316
317
|
|
|
317
318
|
const ExampleComponent = Shade({
|
|
318
|
-
|
|
319
|
+
customElementName: 'shade-broadcast-channel-cleanup-test',
|
|
319
320
|
render: ({ useStoredState }) => {
|
|
320
321
|
const [value] = useStoredState(stateKey, 'initial', store)
|
|
321
322
|
return <div id="value">{value}</div>
|
package/src/shade.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type ShadeOptions<TProps, TElementBase extends HTMLElement> = {
|
|
|
17
17
|
*
|
|
18
18
|
* @example 'my-button', 'shade-dialog', 'app-header'
|
|
19
19
|
*/
|
|
20
|
-
|
|
20
|
+
customElementName: string
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Render hook, this method will be executed on each and every render.
|
|
@@ -68,8 +68,7 @@ export type ShadeOptions<TProps, TElementBase extends HTMLElement> = {
|
|
|
68
68
|
export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
|
|
69
69
|
o: ShadeOptions<TProps, TElementBase>,
|
|
70
70
|
) => {
|
|
71
|
-
|
|
72
|
-
const customElementName = o.shadowDomName
|
|
71
|
+
const { customElementName } = o
|
|
73
72
|
|
|
74
73
|
const existing = customElements.get(customElementName)
|
|
75
74
|
if (!existing) {
|
|
@@ -438,7 +437,7 @@ export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
|
|
|
438
437
|
o.elementBaseName ? { extends: o.elementBaseName } : undefined,
|
|
439
438
|
)
|
|
440
439
|
} else {
|
|
441
|
-
throw Error(`A custom shade with name '${o.
|
|
440
|
+
throw Error(`A custom shade with name '${o.customElementName}' has already been registered!`)
|
|
442
441
|
}
|
|
443
442
|
|
|
444
443
|
return (props: TProps & PartialElement<TElementBase>, children?: ChildrenList) => {
|
|
@@ -18,7 +18,7 @@ describe('Shades integration tests', () => {
|
|
|
18
18
|
await usingAsync(new Injector(), async (injector) => {
|
|
19
19
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
20
20
|
|
|
21
|
-
const ExampleComponent = Shade({ render: () => <div>Hello</div>,
|
|
21
|
+
const ExampleComponent = Shade({ render: () => <div>Hello</div>, customElementName: 'shades-example' })
|
|
22
22
|
|
|
23
23
|
initializeShadeRoot({
|
|
24
24
|
injector,
|
|
@@ -34,7 +34,7 @@ describe('Shades integration tests', () => {
|
|
|
34
34
|
await usingAsync(new Injector(), async (injector) => {
|
|
35
35
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
36
36
|
|
|
37
|
-
const ExampleComponent = Shade({ render: () => 'Hello',
|
|
37
|
+
const ExampleComponent = Shade({ render: () => 'Hello', customElementName: 'shades-string-render-result' })
|
|
38
38
|
|
|
39
39
|
initializeShadeRoot({
|
|
40
40
|
injector,
|
|
@@ -52,7 +52,7 @@ describe('Shades integration tests', () => {
|
|
|
52
52
|
await usingAsync(new Injector(), async (injector) => {
|
|
53
53
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
54
54
|
|
|
55
|
-
const ExampleComponent = Shade({ render: () => null,
|
|
55
|
+
const ExampleComponent = Shade({ render: () => null, customElementName: 'shades-null-render-result' })
|
|
56
56
|
|
|
57
57
|
initializeShadeRoot({
|
|
58
58
|
injector,
|
|
@@ -77,7 +77,7 @@ describe('Shades integration tests', () => {
|
|
|
77
77
|
<p>2</p>
|
|
78
78
|
</>
|
|
79
79
|
),
|
|
80
|
-
|
|
80
|
+
customElementName: 'shades-fragment-render-result',
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
initializeShadeRoot({
|
|
@@ -105,7 +105,7 @@ describe('Shades integration tests', () => {
|
|
|
105
105
|
</>
|
|
106
106
|
</p>
|
|
107
107
|
),
|
|
108
|
-
|
|
108
|
+
customElementName: 'shades-fragment-render-result-nested',
|
|
109
109
|
})
|
|
110
110
|
|
|
111
111
|
initializeShadeRoot({
|
|
@@ -125,7 +125,7 @@ describe('Shades integration tests', () => {
|
|
|
125
125
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
126
126
|
|
|
127
127
|
const CustomComponent = Shade({
|
|
128
|
-
|
|
128
|
+
customElementName: 'shades-fragment-test-custom-component',
|
|
129
129
|
render: () => <p>Hello</p>,
|
|
130
130
|
})
|
|
131
131
|
|
|
@@ -136,7 +136,7 @@ describe('Shades integration tests', () => {
|
|
|
136
136
|
<CustomComponent />
|
|
137
137
|
</>
|
|
138
138
|
),
|
|
139
|
-
|
|
139
|
+
customElementName: 'shades-fragment-render-result-2',
|
|
140
140
|
})
|
|
141
141
|
|
|
142
142
|
initializeShadeRoot({
|
|
@@ -157,12 +157,12 @@ describe('Shades integration tests', () => {
|
|
|
157
157
|
|
|
158
158
|
const ExampleComponent = Shade({
|
|
159
159
|
render: ({ children }) => <div>{children}</div>,
|
|
160
|
-
|
|
160
|
+
customElementName: 'shades-example-2',
|
|
161
161
|
})
|
|
162
162
|
|
|
163
163
|
const ExampleSubs = Shade<{ no: number }>({
|
|
164
164
|
render: ({ props }) => <div>{props.no}</div>,
|
|
165
|
-
|
|
165
|
+
customElementName: 'shades-example-sub',
|
|
166
166
|
})
|
|
167
167
|
|
|
168
168
|
initializeShadeRoot({
|
|
@@ -190,7 +190,7 @@ describe('Shades integration tests', () => {
|
|
|
190
190
|
const setup = vi.fn()
|
|
191
191
|
|
|
192
192
|
const ExampleComponent = Shade({
|
|
193
|
-
|
|
193
|
+
customElementName: 'example-component-1',
|
|
194
194
|
render: ({ useDisposable }) => {
|
|
195
195
|
useDisposable('test', () => {
|
|
196
196
|
setup()
|
|
@@ -219,7 +219,7 @@ describe('Shades integration tests', () => {
|
|
|
219
219
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
220
220
|
|
|
221
221
|
const ExampleComponent = Shade({
|
|
222
|
-
|
|
222
|
+
customElementName: 'example-component-3',
|
|
223
223
|
render: ({ useState }) => {
|
|
224
224
|
const [count, setCount] = useState('count', 0)
|
|
225
225
|
return (
|
|
@@ -290,7 +290,7 @@ describe('Shades integration tests', () => {
|
|
|
290
290
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
291
291
|
|
|
292
292
|
const ExampleComponent = Shade({
|
|
293
|
-
|
|
293
|
+
customElementName: 'example-component-3-stored-state',
|
|
294
294
|
render: ({ useStoredState }) => {
|
|
295
295
|
const [count, setCount] = useStoredState('count', 0, store)
|
|
296
296
|
return (
|
|
@@ -348,7 +348,7 @@ describe('Shades integration tests', () => {
|
|
|
348
348
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
349
349
|
|
|
350
350
|
const ExampleComponent = Shade({
|
|
351
|
-
|
|
351
|
+
customElementName: 'example-component-3-search-state',
|
|
352
352
|
render: ({ useSearchState }) => {
|
|
353
353
|
const [count, setCount] = useSearchState('count', 0)
|
|
354
354
|
return (
|
|
@@ -403,7 +403,7 @@ describe('Shades integration tests', () => {
|
|
|
403
403
|
await usingAsync(new Injector(), async (injector) => {
|
|
404
404
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
405
405
|
const Parent = Shade({
|
|
406
|
-
|
|
406
|
+
customElementName: 'shade-remount-parent',
|
|
407
407
|
render: ({ children, useState }) => {
|
|
408
408
|
const [areChildrenVisible, setAreChildrenVisible] = useState('areChildrenVisible', true)
|
|
409
409
|
return (
|
|
@@ -423,7 +423,7 @@ describe('Shades integration tests', () => {
|
|
|
423
423
|
})
|
|
424
424
|
|
|
425
425
|
const Child = Shade({
|
|
426
|
-
|
|
426
|
+
customElementName: 'example-remount-child',
|
|
427
427
|
render: ({ useState }) => {
|
|
428
428
|
const [count, setCount] = useState('count', 0)
|
|
429
429
|
|
|
@@ -493,14 +493,14 @@ describe('Shades integration tests', () => {
|
|
|
493
493
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
494
494
|
|
|
495
495
|
const ChildComponent = Shade<{ isDisabled: boolean }>({
|
|
496
|
-
|
|
496
|
+
customElementName: 'shade-prop-child',
|
|
497
497
|
render: ({ props }) => {
|
|
498
498
|
return <input type="checkbox" disabled={props.isDisabled} data-testid="inner-input" />
|
|
499
499
|
},
|
|
500
500
|
})
|
|
501
501
|
|
|
502
502
|
const ParentComponent = Shade({
|
|
503
|
-
|
|
503
|
+
customElementName: 'shade-prop-parent',
|
|
504
504
|
render: ({ useState }) => {
|
|
505
505
|
const [isDisabled, setIsDisabled] = useState('isDisabled', false)
|
|
506
506
|
return (
|
|
@@ -181,7 +181,7 @@ describe('StyleManager', () => {
|
|
|
181
181
|
describe('Shade integration', () => {
|
|
182
182
|
it('should register CSS styles when Shade component is created with css property', () => {
|
|
183
183
|
Shade({
|
|
184
|
-
|
|
184
|
+
customElementName: 'shade-css-test-component',
|
|
185
185
|
css: {
|
|
186
186
|
color: 'red',
|
|
187
187
|
padding: '10px',
|
|
@@ -200,7 +200,7 @@ describe('StyleManager', () => {
|
|
|
200
200
|
|
|
201
201
|
it('should register CSS with attribute selector for customized built-in elements', () => {
|
|
202
202
|
Shade({
|
|
203
|
-
|
|
203
|
+
customElementName: 'shade-css-test-button',
|
|
204
204
|
elementBase: HTMLButtonElement,
|
|
205
205
|
elementBaseName: 'button',
|
|
206
206
|
css: {
|
|
@@ -219,7 +219,7 @@ describe('StyleManager', () => {
|
|
|
219
219
|
|
|
220
220
|
it('should not register styles when Shade component has no css property', () => {
|
|
221
221
|
Shade({
|
|
222
|
-
|
|
222
|
+
customElementName: 'shade-no-css-component',
|
|
223
223
|
render: () => null,
|
|
224
224
|
})
|
|
225
225
|
|
package/src/style-manager.ts
CHANGED
|
@@ -27,7 +27,7 @@ class StyleManagerClass {
|
|
|
27
27
|
* Registers CSS styles for a component.
|
|
28
28
|
* Styles are only injected once per component (based on the custom element name).
|
|
29
29
|
*
|
|
30
|
-
* @param
|
|
30
|
+
* @param customElementName - The custom element tag name (used as CSS selector)
|
|
31
31
|
* @param cssObject - The CSSObject containing styles and nested selectors
|
|
32
32
|
* @param elementBaseName - Optional base element name for customized built-in elements (e.g., 'a', 'button').
|
|
33
33
|
* When provided, generates selector like `a[is="component-name"]` instead of `component-name`
|
|
@@ -49,17 +49,17 @@ class StyleManagerClass {
|
|
|
49
49
|
* // Generates: a[is="my-link"] { color: blue; }
|
|
50
50
|
* ```
|
|
51
51
|
*/
|
|
52
|
-
public registerComponentStyles(
|
|
53
|
-
if (this.registeredComponents.has(
|
|
52
|
+
public registerComponentStyles(customElementName: string, cssObject: CSSObject, elementBaseName?: string): boolean {
|
|
53
|
+
if (this.registeredComponents.has(customElementName)) {
|
|
54
54
|
return false
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const selector = elementBaseName ? `${elementBaseName}[is="${
|
|
57
|
+
const selector = elementBaseName ? `${elementBaseName}[is="${customElementName}"]` : customElementName
|
|
58
58
|
const css = generateCSS(selector, cssObject)
|
|
59
59
|
if (css) {
|
|
60
60
|
const styleElement = this.getStyleElement()
|
|
61
|
-
styleElement.textContent += `\n/* ${
|
|
62
|
-
this.registeredComponents.add(
|
|
61
|
+
styleElement.textContent += `\n/* ${customElementName} */\n${css}\n`
|
|
62
|
+
this.registeredComponents.add(customElementName)
|
|
63
63
|
return true
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -68,11 +68,11 @@ class StyleManagerClass {
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Checks if a component's styles have already been registered
|
|
71
|
-
* @param
|
|
71
|
+
* @param customElementName - The component identifier to check
|
|
72
72
|
* @returns True if styles are already registered
|
|
73
73
|
*/
|
|
74
|
-
public isRegistered(
|
|
75
|
-
return this.registeredComponents.has(
|
|
74
|
+
public isRegistered(customElementName: string): boolean {
|
|
75
|
+
return this.registeredComponents.has(customElementName)
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|