@furystack/shades 13.1.2 → 13.2.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 +40 -0
- package/esm/components/index.d.ts +1 -0
- package/esm/components/index.d.ts.map +1 -1
- package/esm/components/index.js +1 -0
- package/esm/components/index.js.map +1 -1
- package/esm/components/nested-navigate.d.ts +46 -0
- package/esm/components/nested-navigate.d.ts.map +1 -0
- package/esm/components/nested-navigate.js +44 -0
- package/esm/components/nested-navigate.js.map +1 -0
- package/esm/components/nested-navigate.spec.d.ts +2 -0
- package/esm/components/nested-navigate.spec.d.ts.map +1 -0
- package/esm/components/nested-navigate.spec.js +76 -0
- package/esm/components/nested-navigate.spec.js.map +1 -0
- package/esm/components/nested-route-link.d.ts +1 -1
- package/esm/components/nested-route-link.d.ts.map +1 -1
- package/esm/components/nested-route-link.js.map +1 -1
- package/esm/components/nested-route-link.spec.js +38 -0
- package/esm/components/nested-route-link.spec.js.map +1 -1
- package/esm/components/nested-route-types.d.ts +2 -2
- package/esm/components/nested-route-types.d.ts.map +1 -1
- package/esm/vnode.d.ts +3 -3
- package/esm/vnode.d.ts.map +1 -1
- package/esm/vnode.js +14 -23
- package/esm/vnode.js.map +1 -1
- package/esm/vnode.spec.js +35 -14
- package/esm/vnode.spec.js.map +1 -1
- package/package.json +6 -6
- package/src/components/index.ts +1 -0
- package/src/components/nested-navigate.spec.ts +143 -0
- package/src/components/nested-navigate.ts +60 -0
- package/src/components/nested-route-link.spec.tsx +52 -0
- package/src/components/nested-route-link.tsx +1 -1
- package/src/components/nested-route-types.ts +2 -2
- package/src/vnode.spec.ts +42 -14
- package/src/vnode.ts +18 -28
package/src/vnode.spec.ts
CHANGED
|
@@ -27,7 +27,7 @@ const vtext = (text: string): VTextNode => ({ _brand: 'vtext', text })
|
|
|
27
27
|
const vel = (tag: string, props: Record<string, unknown> | null, ...children: VChild[]): VNode => ({
|
|
28
28
|
_brand: 'vnode',
|
|
29
29
|
type: tag,
|
|
30
|
-
props,
|
|
30
|
+
props: props ?? {},
|
|
31
31
|
children,
|
|
32
32
|
})
|
|
33
33
|
|
|
@@ -77,6 +77,12 @@ describe('vnode', () => {
|
|
|
77
77
|
expect(vnode.children).toHaveLength(1)
|
|
78
78
|
expect((vnode.children[0] as VTextNode).text).toBe('42')
|
|
79
79
|
})
|
|
80
|
+
|
|
81
|
+
it('should normalize null props to empty object', () => {
|
|
82
|
+
const vnode = createVNode('div', null)
|
|
83
|
+
expect(vnode.props).toEqual({})
|
|
84
|
+
expect(vnode.props).not.toBeNull()
|
|
85
|
+
})
|
|
80
86
|
})
|
|
81
87
|
|
|
82
88
|
describe('flattenVChildren', () => {
|
|
@@ -122,10 +128,8 @@ describe('vnode', () => {
|
|
|
122
128
|
expect(shallowEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false)
|
|
123
129
|
})
|
|
124
130
|
|
|
125
|
-
it('should
|
|
126
|
-
expect(shallowEqual(
|
|
127
|
-
expect(shallowEqual(null, {})).toBe(false)
|
|
128
|
-
expect(shallowEqual({}, null)).toBe(false)
|
|
131
|
+
it('should return true for two empty objects', () => {
|
|
132
|
+
expect(shallowEqual({}, {})).toBe(true)
|
|
129
133
|
})
|
|
130
134
|
})
|
|
131
135
|
|
|
@@ -213,7 +217,7 @@ describe('vnode', () => {
|
|
|
213
217
|
const parent = document.createElement('div')
|
|
214
218
|
const existing = document.createElement('span')
|
|
215
219
|
existing.textContent = 'existing'
|
|
216
|
-
const child: VNode = { _brand: 'vnode', type: EXISTING_NODE, props:
|
|
220
|
+
const child: VNode = { _brand: 'vnode', type: EXISTING_NODE, props: {}, children: [], _el: existing }
|
|
217
221
|
mountChild(child, parent)
|
|
218
222
|
expect(parent.firstChild).toBe(existing)
|
|
219
223
|
})
|
|
@@ -268,7 +272,7 @@ describe('vnode', () => {
|
|
|
268
272
|
|
|
269
273
|
it('should not mount EXISTING_NODE when _el is undefined', () => {
|
|
270
274
|
const parent = document.createElement('div')
|
|
271
|
-
const child: VNode = { _brand: 'vnode', type: EXISTING_NODE, props:
|
|
275
|
+
const child: VNode = { _brand: 'vnode', type: EXISTING_NODE, props: {}, children: [] }
|
|
272
276
|
const result = mountChild(child, parent)
|
|
273
277
|
expect(result).toBeUndefined()
|
|
274
278
|
expect(parent.childNodes.length).toBe(0)
|
|
@@ -307,7 +311,7 @@ describe('vnode', () => {
|
|
|
307
311
|
describe('patchProps', () => {
|
|
308
312
|
it('should add new props', () => {
|
|
309
313
|
const el = document.createElement('div')
|
|
310
|
-
patchProps(el,
|
|
314
|
+
patchProps(el, {}, { id: 'new' })
|
|
311
315
|
expect(el.id).toBe('new')
|
|
312
316
|
})
|
|
313
317
|
|
|
@@ -343,7 +347,7 @@ describe('vnode', () => {
|
|
|
343
347
|
|
|
344
348
|
it('should set data attributes', () => {
|
|
345
349
|
const el = document.createElement('div')
|
|
346
|
-
patchProps(el,
|
|
350
|
+
patchProps(el, {}, { 'data-testid': 'foo' })
|
|
347
351
|
expect(el.getAttribute('data-testid')).toBe('foo')
|
|
348
352
|
})
|
|
349
353
|
|
|
@@ -357,7 +361,7 @@ describe('vnode', () => {
|
|
|
357
361
|
describe('SVG elements', () => {
|
|
358
362
|
it('should set attributes via setAttribute on SVG elements', () => {
|
|
359
363
|
const el = document.createElementNS(SVG_NS, 'rect')
|
|
360
|
-
patchProps(el,
|
|
364
|
+
patchProps(el, {}, { width: '100', height: '50', rx: '5' })
|
|
361
365
|
expect(el.getAttribute('width')).toBe('100')
|
|
362
366
|
expect(el.getAttribute('height')).toBe('50')
|
|
363
367
|
expect(el.getAttribute('rx')).toBe('5')
|
|
@@ -365,7 +369,7 @@ describe('vnode', () => {
|
|
|
365
369
|
|
|
366
370
|
it('should set className as class attribute on SVG elements', () => {
|
|
367
371
|
const el = document.createElementNS(SVG_NS, 'g')
|
|
368
|
-
patchProps(el,
|
|
372
|
+
patchProps(el, {}, { className: 'my-group' })
|
|
369
373
|
expect(el.getAttribute('class')).toBe('my-group')
|
|
370
374
|
})
|
|
371
375
|
|
|
@@ -393,7 +397,7 @@ describe('vnode', () => {
|
|
|
393
397
|
it('should set event handlers as properties on SVG elements', () => {
|
|
394
398
|
const el = document.createElementNS(SVG_NS, 'rect')
|
|
395
399
|
const handler = vi.fn()
|
|
396
|
-
patchProps(el,
|
|
400
|
+
patchProps(el, {}, { onclick: handler })
|
|
397
401
|
expect((el as unknown as Record<string, unknown>).onclick).toBe(handler)
|
|
398
402
|
})
|
|
399
403
|
})
|
|
@@ -495,11 +499,11 @@ describe('vnode', () => {
|
|
|
495
499
|
const real = document.createElement('span')
|
|
496
500
|
real.textContent = 'real'
|
|
497
501
|
|
|
498
|
-
const old: VChild[] = [{ _brand: 'vnode', type: EXISTING_NODE, props:
|
|
502
|
+
const old: VChild[] = [{ _brand: 'vnode', type: EXISTING_NODE, props: {}, children: [], _el: real }]
|
|
499
503
|
patchChildren(parent, [], old)
|
|
500
504
|
expect(parent.firstChild).toBe(real)
|
|
501
505
|
|
|
502
|
-
const updated: VChild[] = [{ _brand: 'vnode', type: EXISTING_NODE, props:
|
|
506
|
+
const updated: VChild[] = [{ _brand: 'vnode', type: EXISTING_NODE, props: {}, children: [], _el: real }]
|
|
503
507
|
patchChildren(parent, old, updated)
|
|
504
508
|
expect(parent.firstChild).toBe(real)
|
|
505
509
|
})
|
|
@@ -553,6 +557,30 @@ describe('vnode', () => {
|
|
|
553
557
|
expect(fakeShadeEl.props).toEqual({ count: 2 })
|
|
554
558
|
})
|
|
555
559
|
|
|
560
|
+
it('should set empty object (not null) on Shade when props transition to none', () => {
|
|
561
|
+
const parent = document.createElement('div')
|
|
562
|
+
|
|
563
|
+
const fakeShadeEl = document.createElement('my-shade-3') as unknown as JSX.Element
|
|
564
|
+
const updateFn = vi.fn()
|
|
565
|
+
;(fakeShadeEl as unknown as Record<string, unknown>).updateComponentSync = updateFn
|
|
566
|
+
;(fakeShadeEl as unknown as Record<string, unknown>).props = { elevation: 2 }
|
|
567
|
+
;(fakeShadeEl as unknown as Record<string, unknown>).shadeChildren = undefined
|
|
568
|
+
|
|
569
|
+
const factory = vi.fn(() => fakeShadeEl as unknown as JSX.Element)
|
|
570
|
+
|
|
571
|
+
const old: VChild[] = [
|
|
572
|
+
{ _brand: 'vnode', type: factory, props: { elevation: 2 }, children: [], _el: fakeShadeEl },
|
|
573
|
+
]
|
|
574
|
+
parent.appendChild(fakeShadeEl)
|
|
575
|
+
|
|
576
|
+
const updated: VChild[] = [{ _brand: 'vnode', type: factory, props: {}, children: [] }]
|
|
577
|
+
patchChildren(parent, old, updated)
|
|
578
|
+
|
|
579
|
+
expect(updateFn).toHaveBeenCalledOnce()
|
|
580
|
+
expect(fakeShadeEl.props).toEqual({})
|
|
581
|
+
expect(fakeShadeEl.props).not.toBeNull()
|
|
582
|
+
})
|
|
583
|
+
|
|
556
584
|
it('should NOT call updateComponentSync when props are unchanged', () => {
|
|
557
585
|
const parent = document.createElement('div')
|
|
558
586
|
|
package/src/vnode.ts
CHANGED
|
@@ -39,7 +39,7 @@ export const EXISTING_NODE: unique symbol = Symbol('existing-node')
|
|
|
39
39
|
export type VNode = {
|
|
40
40
|
_brand: typeof VNODE_BRAND
|
|
41
41
|
type: string | ((...args: unknown[]) => unknown) | typeof FRAGMENT | typeof EXISTING_NODE
|
|
42
|
-
props: Record<string, unknown>
|
|
42
|
+
props: Record<string, unknown>
|
|
43
43
|
children: VChild[]
|
|
44
44
|
_el?: Node
|
|
45
45
|
}
|
|
@@ -99,7 +99,7 @@ export const flattenVChildren = (raw: unknown[]): VChild[] => {
|
|
|
99
99
|
} else if (child instanceof Node) {
|
|
100
100
|
// Real DOM node from shadeChildren (created outside render mode).
|
|
101
101
|
// Wrap it so the reconciler can track it.
|
|
102
|
-
result.push({ _brand: VNODE_BRAND, type: EXISTING_NODE, props:
|
|
102
|
+
result.push({ _brand: VNODE_BRAND, type: EXISTING_NODE, props: {}, children: [], _el: child })
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
return result
|
|
@@ -125,7 +125,7 @@ export const createVNode = (
|
|
|
125
125
|
const vnode: VNode = {
|
|
126
126
|
_brand: VNODE_BRAND,
|
|
127
127
|
type: type === null ? FRAGMENT : type,
|
|
128
|
-
props: props ? { ...props } :
|
|
128
|
+
props: props ? { ...props } : {},
|
|
129
129
|
children,
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -134,21 +134,20 @@ export const createVNode = (
|
|
|
134
134
|
if (typeof type === 'string') {
|
|
135
135
|
const v = vnode as unknown as Record<string, unknown>
|
|
136
136
|
v.setAttribute = (name: string, value: string) => {
|
|
137
|
-
if (!vnode.props) vnode.props = {}
|
|
138
137
|
vnode.props[name] = value
|
|
139
138
|
}
|
|
140
139
|
v.removeAttribute = (name: string) => {
|
|
141
|
-
|
|
140
|
+
delete vnode.props[name]
|
|
142
141
|
}
|
|
143
142
|
v.getAttribute = (name: string) => {
|
|
144
|
-
return (vnode.props
|
|
143
|
+
return (vnode.props[name] as string) ?? null
|
|
145
144
|
}
|
|
146
145
|
v.hasAttribute = (name: string) => {
|
|
147
|
-
return
|
|
146
|
+
return name in vnode.props
|
|
148
147
|
}
|
|
149
148
|
v.appendChild = (child: unknown) => {
|
|
150
149
|
if (child instanceof Node) {
|
|
151
|
-
vnode.children.push({ _brand: VNODE_BRAND, type: EXISTING_NODE, props:
|
|
150
|
+
vnode.children.push({ _brand: VNODE_BRAND, type: EXISTING_NODE, props: {}, children: [], _el: child })
|
|
152
151
|
} else if (isVNode(child) || isVTextNode(child)) {
|
|
153
152
|
vnode.children.push(child)
|
|
154
153
|
}
|
|
@@ -168,9 +167,8 @@ export const createVNode = (
|
|
|
168
167
|
/**
|
|
169
168
|
* Shallow-compares two props objects. Returns true if all keys and values match.
|
|
170
169
|
*/
|
|
171
|
-
export const shallowEqual = (a: Record<string, unknown
|
|
170
|
+
export const shallowEqual = (a: Record<string, unknown>, b: Record<string, unknown>): boolean => {
|
|
172
171
|
if (a === b) return true
|
|
173
|
-
if (!a || !b) return false
|
|
174
172
|
const keysA = Object.keys(a)
|
|
175
173
|
const keysB = Object.keys(b)
|
|
176
174
|
if (keysA.length !== keysB.length) return false
|
|
@@ -201,13 +199,13 @@ export const toVChildArray = (renderResult: unknown): VChild[] => {
|
|
|
201
199
|
return Array.from(renderResult.childNodes).map((node) => ({
|
|
202
200
|
_brand: VNODE_BRAND as typeof VNODE_BRAND,
|
|
203
201
|
type: EXISTING_NODE,
|
|
204
|
-
props:
|
|
202
|
+
props: {} as Record<string, unknown>,
|
|
205
203
|
children: [] as VChild[],
|
|
206
204
|
_el: node,
|
|
207
205
|
}))
|
|
208
206
|
}
|
|
209
207
|
if (renderResult instanceof Node) {
|
|
210
|
-
return [{ _brand: VNODE_BRAND, type: EXISTING_NODE, props:
|
|
208
|
+
return [{ _brand: VNODE_BRAND, type: EXISTING_NODE, props: {}, children: [], _el: renderResult }]
|
|
211
209
|
}
|
|
212
210
|
return []
|
|
213
211
|
}
|
|
@@ -291,8 +289,7 @@ const patchStyle = (
|
|
|
291
289
|
/**
|
|
292
290
|
* Applies all props to a freshly created element (initial mount).
|
|
293
291
|
*/
|
|
294
|
-
const applyProps = (el: Element, props: Record<string, unknown>
|
|
295
|
-
if (!props) return
|
|
292
|
+
const applyProps = (el: Element, props: Record<string, unknown>): void => {
|
|
296
293
|
for (const [key, value] of Object.entries(props)) {
|
|
297
294
|
setProp(el, key, value)
|
|
298
295
|
}
|
|
@@ -301,26 +298,19 @@ const applyProps = (el: Element, props: Record<string, unknown> | null): void =>
|
|
|
301
298
|
/**
|
|
302
299
|
* Diffs old and new props and applies minimal updates to a live DOM element.
|
|
303
300
|
*/
|
|
304
|
-
export const patchProps = (
|
|
305
|
-
el: Element,
|
|
306
|
-
oldProps: Record<string, unknown> | null,
|
|
307
|
-
newProps: Record<string, unknown> | null,
|
|
308
|
-
): void => {
|
|
309
|
-
const oldP = oldProps || {}
|
|
310
|
-
const newP = newProps || {}
|
|
311
|
-
|
|
301
|
+
export const patchProps = (el: Element, oldProps: Record<string, unknown>, newProps: Record<string, unknown>): void => {
|
|
312
302
|
// Remove props that no longer exist
|
|
313
|
-
for (const key of Object.keys(
|
|
314
|
-
if (!(key in
|
|
303
|
+
for (const key of Object.keys(oldProps)) {
|
|
304
|
+
if (!(key in newProps)) {
|
|
315
305
|
removeProp(el, key)
|
|
316
306
|
}
|
|
317
307
|
}
|
|
318
308
|
|
|
319
309
|
// Add / update props
|
|
320
|
-
for (const [key, value] of Object.entries(
|
|
310
|
+
for (const [key, value] of Object.entries(newProps)) {
|
|
321
311
|
if (key === 'style') {
|
|
322
|
-
patchStyle(el,
|
|
323
|
-
} else if (
|
|
312
|
+
patchStyle(el, oldProps.style as Record<string, string> | undefined, value as Record<string, string> | undefined)
|
|
313
|
+
} else if (oldProps[key] !== value) {
|
|
324
314
|
setProp(el, key, value)
|
|
325
315
|
}
|
|
326
316
|
}
|
|
@@ -352,7 +342,7 @@ export const mountChild = (child: VChild, parent: Node | null): Node => {
|
|
|
352
342
|
// Shade component
|
|
353
343
|
if (typeof child.type === 'function') {
|
|
354
344
|
const factory = child.type as (props: unknown, children?: ChildrenList) => JSX.Element
|
|
355
|
-
const el = factory(child.props
|
|
345
|
+
const el = factory(child.props, child.children as unknown as ChildrenList)
|
|
356
346
|
child._el = el
|
|
357
347
|
if (parent) parent.appendChild(el)
|
|
358
348
|
return el
|