@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/esm/components/index.d.ts +1 -0
  3. package/esm/components/index.d.ts.map +1 -1
  4. package/esm/components/index.js +1 -0
  5. package/esm/components/index.js.map +1 -1
  6. package/esm/components/nested-navigate.d.ts +46 -0
  7. package/esm/components/nested-navigate.d.ts.map +1 -0
  8. package/esm/components/nested-navigate.js +44 -0
  9. package/esm/components/nested-navigate.js.map +1 -0
  10. package/esm/components/nested-navigate.spec.d.ts +2 -0
  11. package/esm/components/nested-navigate.spec.d.ts.map +1 -0
  12. package/esm/components/nested-navigate.spec.js +76 -0
  13. package/esm/components/nested-navigate.spec.js.map +1 -0
  14. package/esm/components/nested-route-link.d.ts +1 -1
  15. package/esm/components/nested-route-link.d.ts.map +1 -1
  16. package/esm/components/nested-route-link.js.map +1 -1
  17. package/esm/components/nested-route-link.spec.js +38 -0
  18. package/esm/components/nested-route-link.spec.js.map +1 -1
  19. package/esm/components/nested-route-types.d.ts +2 -2
  20. package/esm/components/nested-route-types.d.ts.map +1 -1
  21. package/esm/vnode.d.ts +3 -3
  22. package/esm/vnode.d.ts.map +1 -1
  23. package/esm/vnode.js +14 -23
  24. package/esm/vnode.js.map +1 -1
  25. package/esm/vnode.spec.js +35 -14
  26. package/esm/vnode.spec.js.map +1 -1
  27. package/package.json +6 -6
  28. package/src/components/index.ts +1 -0
  29. package/src/components/nested-navigate.spec.ts +143 -0
  30. package/src/components/nested-navigate.ts +60 -0
  31. package/src/components/nested-route-link.spec.tsx +52 -0
  32. package/src/components/nested-route-link.tsx +1 -1
  33. package/src/components/nested-route-types.ts +2 -2
  34. package/src/vnode.spec.ts +42 -14
  35. 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 handle null comparisons', () => {
126
- expect(shallowEqual(null, null)).toBe(true)
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: null, children: [], _el: existing }
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: null, children: [] }
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, null, { id: 'new' })
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, null, { 'data-testid': 'foo' })
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, null, { width: '100', height: '50', rx: '5' })
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, null, { className: 'my-group' })
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, null, { onclick: handler })
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: null, children: [], _el: real }]
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: null, children: [], _el: real }]
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> | null
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: null, children: [], _el: child })
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 } : null,
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
- if (vnode.props) delete vnode.props[name]
140
+ delete vnode.props[name]
142
141
  }
143
142
  v.getAttribute = (name: string) => {
144
- return (vnode.props?.[name] as string) ?? null
143
+ return (vnode.props[name] as string) ?? null
145
144
  }
146
145
  v.hasAttribute = (name: string) => {
147
- return vnode.props ? name in vnode.props : false
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: null, children: [], _el: child })
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> | null, b: Record<string, unknown> | null): boolean => {
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: null,
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: null, children: [], _el: renderResult }]
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> | null): void => {
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(oldP)) {
314
- if (!(key in newP)) {
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(newP)) {
310
+ for (const [key, value] of Object.entries(newProps)) {
321
311
  if (key === 'style') {
322
- patchStyle(el, oldP.style as Record<string, string> | undefined, value as Record<string, string> | undefined)
323
- } else if (oldP[key] !== value) {
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 || {}, child.children as unknown as ChildrenList)
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