@remix-run/ui 0.1.1 → 0.2.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 (243) hide show
  1. package/README.md +33 -10
  2. package/dist/animation/animate-layout-mixin.js +66 -66
  3. package/dist/animation/animate-layout-mixin.js.map +1 -1
  4. package/dist/animation/spring.d.ts +6 -35
  5. package/dist/animation/spring.js +9 -8
  6. package/dist/animation/spring.js.map +1 -1
  7. package/dist/components/accordion/accordion.d.ts +7 -7
  8. package/dist/components/accordion/accordion.js +0 -2
  9. package/dist/components/accordion/accordion.js.map +1 -1
  10. package/dist/components/accordion/demos/card.demo.d.ts +6 -0
  11. package/dist/components/accordion/demos/card.demo.js +58 -0
  12. package/dist/components/accordion/demos/card.demo.js.map +1 -0
  13. package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
  14. package/dist/components/accordion/demos/multiple.demo.js +19 -0
  15. package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
  16. package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
  17. package/dist/components/accordion/demos/overview.demo.js +19 -0
  18. package/dist/components/accordion/demos/overview.demo.js.map +1 -0
  19. package/dist/components/breadcrumbs/breadcrumbs.d.ts +1 -1
  20. package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
  21. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
  22. package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
  23. package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
  24. package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
  25. package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
  26. package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
  27. package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
  28. package/dist/components/button/button.d.ts +86 -4
  29. package/dist/components/button/button.js +63 -3
  30. package/dist/components/button/button.js.map +1 -1
  31. package/dist/components/button/demos/aliases.demo.d.ts +6 -0
  32. package/dist/components/button/demos/aliases.demo.js +18 -0
  33. package/dist/components/button/demos/aliases.demo.js.map +1 -0
  34. package/dist/components/button/demos/basic.demo.d.ts +6 -0
  35. package/dist/components/button/demos/basic.demo.js +19 -0
  36. package/dist/components/button/demos/basic.demo.js.map +1 -0
  37. package/dist/components/button/demos/states.demo.d.ts +6 -0
  38. package/dist/components/button/demos/states.demo.js +29 -0
  39. package/dist/components/button/demos/states.demo.js.map +1 -0
  40. package/dist/components/combobox/combobox.d.ts +11 -11
  41. package/dist/components/combobox/combobox.js +0 -2
  42. package/dist/components/combobox/combobox.js.map +1 -1
  43. package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
  44. package/dist/components/combobox/demos/overview.demo.js +153 -0
  45. package/dist/components/combobox/demos/overview.demo.js.map +1 -0
  46. package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
  47. package/dist/components/listbox/demos/overview.demo.js +64 -0
  48. package/dist/components/listbox/demos/overview.demo.js.map +1 -0
  49. package/dist/components/listbox/listbox.d.ts +4 -9
  50. package/dist/components/listbox/listbox.js +21 -33
  51. package/dist/components/listbox/listbox.js.map +1 -1
  52. package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
  53. package/dist/components/menu/demos/bubbling.demo.js +15 -0
  54. package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
  55. package/dist/components/menu/demos/overview.demo.d.ts +6 -0
  56. package/dist/components/menu/demos/overview.demo.js +32 -0
  57. package/dist/components/menu/demos/overview.demo.js.map +1 -0
  58. package/dist/components/menu/menu.d.ts +15 -24
  59. package/dist/components/menu/menu.js +23 -38
  60. package/dist/components/menu/menu.js.map +1 -1
  61. package/dist/components/popover/demos/overview.demo.d.ts +6 -0
  62. package/dist/components/popover/demos/overview.demo.js +64 -0
  63. package/dist/components/popover/demos/overview.demo.js.map +1 -0
  64. package/dist/components/popover/popover.d.ts +5 -5
  65. package/dist/components/popover/popover.js.map +1 -1
  66. package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
  67. package/dist/components/select/demos/deconstructed.demo.js +53 -0
  68. package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
  69. package/dist/components/select/demos/overview.demo.d.ts +5 -0
  70. package/dist/components/select/demos/overview.demo.js +27 -0
  71. package/dist/components/select/demos/overview.demo.js.map +1 -0
  72. package/dist/components/select/select.d.ts +11 -11
  73. package/dist/components/select/select.js +17 -26
  74. package/dist/components/select/select.js.map +1 -1
  75. package/dist/components/tabs/tabs.d.ts +6 -6
  76. package/dist/components/tabs/tabs.js +0 -2
  77. package/dist/components/tabs/tabs.js.map +1 -1
  78. package/dist/index.d.ts +2 -2
  79. package/dist/index.js.map +1 -1
  80. package/dist/interactions/keydown/keydown.d.ts +1 -1
  81. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
  82. package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
  83. package/dist/runtime/component.d.ts +18 -12
  84. package/dist/runtime/component.js +93 -80
  85. package/dist/runtime/component.js.map +1 -1
  86. package/dist/runtime/core/attributes.d.ts +14 -0
  87. package/dist/runtime/core/attributes.js +103 -0
  88. package/dist/runtime/core/attributes.js.map +1 -0
  89. package/dist/runtime/core/children.d.ts +7 -0
  90. package/dist/runtime/core/children.js +30 -0
  91. package/dist/runtime/core/children.js.map +1 -0
  92. package/dist/runtime/core/props.d.ts +9 -0
  93. package/dist/runtime/core/props.js +170 -0
  94. package/dist/runtime/core/props.js.map +1 -0
  95. package/dist/runtime/core/vnode.d.ts +3 -0
  96. package/dist/runtime/core/vnode.js +39 -0
  97. package/dist/runtime/core/vnode.js.map +1 -0
  98. package/dist/runtime/create-element.js +2 -1
  99. package/dist/runtime/create-element.js.map +1 -1
  100. package/dist/runtime/diff-dom.js +47 -14
  101. package/dist/runtime/diff-dom.js.map +1 -1
  102. package/dist/runtime/diff-props.d.ts +1 -2
  103. package/dist/runtime/diff-props.js +2 -181
  104. package/dist/runtime/diff-props.js.map +1 -1
  105. package/dist/runtime/dom.d.ts +20 -9
  106. package/dist/runtime/frame.d.ts +23 -2
  107. package/dist/runtime/frame.js +41 -47
  108. package/dist/runtime/frame.js.map +1 -1
  109. package/dist/runtime/jsx.d.ts +1 -1
  110. package/dist/runtime/jsx.js +2 -28
  111. package/dist/runtime/jsx.js.map +1 -1
  112. package/dist/runtime/mixins/link-mixin.d.ts +5 -1
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  114. package/dist/runtime/mixins/mixin.d.ts +2 -1
  115. package/dist/runtime/mixins/mixin.js +9 -4
  116. package/dist/runtime/mixins/mixin.js.map +1 -1
  117. package/dist/runtime/mixins/on-mixin.d.ts +15 -4
  118. package/dist/runtime/mixins/on-mixin.js +9 -2
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -1
  120. package/dist/runtime/mixins/ref-mixin.d.ts +2 -1
  121. package/dist/runtime/mixins/ref-mixin.js.map +1 -1
  122. package/dist/runtime/navigation.js +9 -1
  123. package/dist/runtime/navigation.js.map +1 -1
  124. package/dist/runtime/reconcile.d.ts +1 -1
  125. package/dist/runtime/reconcile.js +452 -197
  126. package/dist/runtime/reconcile.js.map +1 -1
  127. package/dist/runtime/run.d.ts +17 -2
  128. package/dist/runtime/run.js.map +1 -1
  129. package/dist/runtime/scheduler.d.ts +10 -10
  130. package/dist/runtime/scheduler.js +1 -1
  131. package/dist/runtime/scheduler.js.map +1 -1
  132. package/dist/runtime/stream-protocol.d.ts +8 -0
  133. package/dist/runtime/stream-protocol.js +20 -0
  134. package/dist/runtime/stream-protocol.js.map +1 -0
  135. package/dist/runtime/svg-attributes.js +1 -0
  136. package/dist/runtime/svg-attributes.js.map +1 -1
  137. package/dist/runtime/to-vnode.js +7 -11
  138. package/dist/runtime/to-vnode.js.map +1 -1
  139. package/dist/runtime/vnode.d.ts +11 -5
  140. package/dist/runtime/vnode.js +6 -3
  141. package/dist/runtime/vnode.js.map +1 -1
  142. package/dist/server/stream.d.ts +2 -0
  143. package/dist/server/stream.js +154 -114
  144. package/dist/server/stream.js.map +1 -1
  145. package/dist/style/css-mixin.d.ts +2 -1
  146. package/dist/style/css-mixin.js.map +1 -1
  147. package/dist/style/index.d.ts +1 -1
  148. package/dist/style/layers.d.ts +2 -0
  149. package/dist/style/layers.js +3 -0
  150. package/dist/style/layers.js.map +1 -0
  151. package/dist/style/stylesheet.d.ts +13 -11
  152. package/dist/style/stylesheet.js +69 -8
  153. package/dist/style/stylesheet.js.map +1 -1
  154. package/dist/theme/contract.d.ts +24 -222
  155. package/dist/theme/contract.js +2 -2
  156. package/dist/theme/contract.js.map +1 -1
  157. package/dist/theme/glyph-contract.d.ts +1 -3
  158. package/dist/theme/glyph-contract.js.map +1 -1
  159. package/dist/theme/presets/rmx-01/index.d.ts +4 -2
  160. package/dist/theme/presets/rmx-01/index.js.map +1 -1
  161. package/dist/theme/runtime.js +4 -1
  162. package/dist/theme/runtime.js.map +1 -1
  163. package/dist/utils/scroll-lock.d.ts +3 -3
  164. package/dist/utils/scroll-lock.js.map +1 -1
  165. package/package.json +3 -3
  166. package/src/animation/README.md +316 -0
  167. package/src/animation/animate-layout-mixin.ts +79 -71
  168. package/src/animation/spring.ts +21 -9
  169. package/src/components/accordion/README.md +166 -0
  170. package/src/components/accordion/accordion.tsx +14 -14
  171. package/src/components/accordion/demos/card.demo.tsx +113 -0
  172. package/src/components/accordion/demos/multiple.demo.tsx +56 -0
  173. package/src/components/accordion/demos/overview.demo.tsx +56 -0
  174. package/src/components/anchor/README.md +153 -0
  175. package/src/components/anchor/anchor.ts +1 -1
  176. package/src/components/breadcrumbs/README.md +55 -0
  177. package/src/components/breadcrumbs/breadcrumbs.tsx +1 -3
  178. package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
  179. package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
  180. package/src/components/button/README.md +4 -4
  181. package/src/components/button/button.tsx +118 -20
  182. package/src/components/button/demos/aliases.demo.tsx +27 -0
  183. package/src/components/button/demos/basic.demo.tsx +30 -0
  184. package/src/components/button/demos/states.demo.tsx +52 -0
  185. package/src/components/combobox/README.md +1 -1
  186. package/src/components/combobox/combobox.tsx +37 -21
  187. package/src/components/combobox/demos/overview.demo.tsx +199 -0
  188. package/src/components/glyph/README.md +72 -0
  189. package/src/components/glyph/glyph.tsx +2 -2
  190. package/src/components/listbox/README.md +115 -0
  191. package/src/components/listbox/demos/overview.demo.tsx +101 -0
  192. package/src/components/listbox/listbox.ts +106 -110
  193. package/src/components/menu/README.md +96 -0
  194. package/src/components/menu/demos/bubbling.demo.tsx +34 -0
  195. package/src/components/menu/demos/overview.demo.tsx +76 -0
  196. package/src/components/menu/menu.tsx +61 -55
  197. package/src/components/popover/README.md +3 -3
  198. package/src/components/popover/demos/overview.demo.tsx +143 -0
  199. package/src/components/popover/popover.ts +90 -79
  200. package/src/components/select/README.md +107 -0
  201. package/src/components/select/demos/deconstructed.demo.tsx +95 -0
  202. package/src/components/select/demos/overview.demo.tsx +51 -0
  203. package/src/components/select/select.tsx +53 -36
  204. package/src/components/tabs/tabs.tsx +10 -5
  205. package/src/index.ts +2 -1
  206. package/src/runtime/component.ts +122 -88
  207. package/src/runtime/core/attributes.ts +118 -0
  208. package/src/runtime/core/children.ts +42 -0
  209. package/src/runtime/core/props.ts +187 -0
  210. package/src/runtime/core/vnode.ts +48 -0
  211. package/src/runtime/create-element.ts +2 -1
  212. package/src/runtime/diff-dom.ts +59 -12
  213. package/src/runtime/diff-props.ts +2 -187
  214. package/src/runtime/dom.ts +20 -9
  215. package/src/runtime/frame.ts +71 -55
  216. package/src/runtime/jsx.ts +3 -30
  217. package/src/runtime/mixins/link-mixin.ts +88 -86
  218. package/src/runtime/mixins/mixin.ts +18 -5
  219. package/src/runtime/mixins/on-mixin.ts +20 -12
  220. package/src/runtime/mixins/ref-mixin.ts +4 -5
  221. package/src/runtime/navigation.ts +10 -1
  222. package/src/runtime/reconcile.ts +563 -223
  223. package/src/runtime/run.ts +17 -1
  224. package/src/runtime/scheduler.ts +20 -20
  225. package/src/runtime/stream-protocol.ts +27 -0
  226. package/src/runtime/svg-attributes.ts +1 -0
  227. package/src/runtime/to-vnode.ts +7 -10
  228. package/src/runtime/vnode.ts +16 -9
  229. package/src/server/README.md +90 -0
  230. package/src/server/stream.ts +176 -124
  231. package/src/style/css-mixin.ts +6 -1
  232. package/src/style/index.ts +1 -2
  233. package/src/style/layers.ts +2 -0
  234. package/src/style/stylesheet.ts +88 -11
  235. package/src/test/README.md +107 -0
  236. package/src/test/utils.ts +3 -1
  237. package/src/theme/README.md +103 -0
  238. package/src/theme/contract.ts +37 -5
  239. package/src/theme/glyph-contract.ts +3 -1
  240. package/src/theme/presets/rmx-01/index.ts +4 -2
  241. package/src/theme/runtime.ts +4 -1
  242. package/src/utils/scroll-lock/README.md +33 -0
  243. package/src/utils/scroll-lock.ts +7 -3
@@ -20,11 +20,12 @@ import {
20
20
  isCommittedTextNode,
21
21
  isFragmentNode,
22
22
  isHostNode,
23
+ isNonRenderNode,
23
24
  isTextNode,
24
25
  findContextFromAncestry,
25
26
  } from './vnode.ts'
26
27
  import { invariant } from './invariant.ts'
27
- import { diffHostProps } from './diff-props.ts'
28
+ import { patchHostProps } from './core/props.ts'
28
29
  import type { StyleManager } from '../style/index.ts'
29
30
  import type { ElementProps } from './jsx.ts'
30
31
  import { skipComments, logHydrationMismatch } from './client-entries.ts'
@@ -42,13 +43,10 @@ import {
42
43
  type MixinRuntimeBinding,
43
44
  type MixinRuntimeState,
44
45
  } from './mixins/mixin.ts'
46
+ import { isOnMixinDescriptor, type OnMixinDescriptor } from './mixins/on-mixin.ts'
45
47
 
46
48
  const SVG_NS = 'http://www.w3.org/2000/svg'
47
49
 
48
- // Internal diffing flags (modeled after Preact)
49
- const INSERT_VNODE = 1 << 0
50
- const MATCHED = 1 << 1
51
-
52
50
  let idCounter = 0
53
51
  let persistedRemovalToken = 0
54
52
  const persistedMixinNodes = new Set<CommittedHostNode>()
@@ -116,6 +114,20 @@ type ControlledReflectionState = {
116
114
  onChange: () => void
117
115
  }
118
116
 
117
+ type DirectEventBinding = {
118
+ type: string
119
+ handler: (event: Event, signal: AbortSignal) => void | Promise<void>
120
+ capture: boolean
121
+ stableHandler: ((event: Event) => void) | null
122
+ reentry: AbortController | null
123
+ }
124
+
125
+ type DirectEventState = {
126
+ bindings: DirectEventBinding[]
127
+ }
128
+
129
+ const EMPTY_DIRECT_EVENT_DESCRIPTORS: OnMixinDescriptor[] = []
130
+
119
131
  function shouldRestoreControlledReflectionOnInput(
120
132
  node: CommittedHostNode,
121
133
  state: ControlledReflectionState,
@@ -258,6 +270,18 @@ function resolveNodeMixProps(
258
270
  state?: MixinRuntimeState,
259
271
  ): ElementProps {
260
272
  let mix = node.props.mix
273
+ let directEventDescriptors = resolveDirectEventDescriptors(mix)
274
+ if (directEventDescriptors) {
275
+ if (state) {
276
+ teardownMixins(state)
277
+ }
278
+ node._mixState = undefined
279
+ node._mixedProps = node.props
280
+ node._directEventDescriptors = directEventDescriptors
281
+ return node.props
282
+ }
283
+
284
+ node._directEventDescriptors = undefined
261
285
  if (state == null && (mix == null || (Array.isArray(mix) && mix.length === 0))) {
262
286
  node._mixState = undefined
263
287
  node._mixedProps = node.props
@@ -283,6 +307,22 @@ function resolveNodeMixProps(
283
307
  return resolved.props
284
308
  }
285
309
 
310
+ function resolveDirectEventDescriptors(mix: ElementProps['mix']): OnMixinDescriptor[] | null {
311
+ if (!mix) return EMPTY_DIRECT_EVENT_DESCRIPTORS
312
+ if (!Array.isArray(mix)) {
313
+ return isOnMixinDescriptor(mix) ? [mix] : null
314
+ }
315
+
316
+ return areOnMixinDescriptors(mix) ? mix : null
317
+ }
318
+
319
+ function areOnMixinDescriptors(descriptors: unknown[]): descriptors is OnMixinDescriptor[] {
320
+ for (let item of descriptors) {
321
+ if (!isOnMixinDescriptor(item)) return false
322
+ }
323
+ return true
324
+ }
325
+
286
326
  function enqueueMixinBindingUpdate(
287
327
  this: MixinRuntimeBinding,
288
328
  done: (signal: AbortSignal) => void,
@@ -299,7 +339,7 @@ function enqueueMixinBindingUpdate(
299
339
  dispatchMixinBeforeUpdate(state)
300
340
  let prevProps = getHostProps(node)
301
341
  let nextProps = resolveNodeMixProps(node, this.frame, this.scheduler, state)
302
- diffHostProps(prevProps, nextProps, this.node)
342
+ patchHostProps(prevProps, nextProps, this.node)
303
343
 
304
344
  dispatchMixinCommit(state)
305
345
  done(state ? getMixinRuntimeSignal(state) : AbortSignal.abort())
@@ -382,6 +422,10 @@ export function diffVNodes(
382
422
  return rootCursor
383
423
  }
384
424
 
425
+ if (isNonRenderNode(curr) && isNonRenderNode(next)) {
426
+ return rootCursor
427
+ }
428
+
385
429
  if (isCommittedTextNode(curr) && isTextNode(next)) {
386
430
  diffText(curr, next, vParent)
387
431
  return rootCursor
@@ -405,7 +449,7 @@ export function diffVNodes(
405
449
  frame,
406
450
  scheduler,
407
451
  styles,
408
- vParent,
452
+ next,
409
453
  rootTarget,
410
454
  undefined,
411
455
  anchor,
@@ -445,7 +489,7 @@ function replace(
445
489
  return
446
490
  }
447
491
 
448
- let replacementAnchor = findNextSiblingDomAnchor(curr, vParent) ?? anchor
492
+ let replacementAnchor = findNextSiblingDomAnchor(curr) ?? anchor
449
493
  remove(curr, domParent, scheduler, styles)
450
494
  insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replacementAnchor)
451
495
  }
@@ -490,19 +534,23 @@ function diffHost(
490
534
  next,
491
535
  rootTarget,
492
536
  )
493
- diffHostProps(currProps, nextProps, curr._dom)
537
+ patchHostProps(currProps, nextProps, curr._dom)
494
538
 
495
539
  next._dom = curr._dom
496
540
  next._parent = vParent
497
541
  next._controller = curr._controller
542
+ next._directEventState = curr._directEventState
498
543
  next._controlledState = curr._controlledState
544
+ syncDirectEventListeners(next as CommittedHostNode)
499
545
 
500
546
  if (next._controlledState || shouldTrackControlledReflection(nextProps)) {
501
547
  ensureControlledReflection(next as CommittedHostNode, scheduler)
502
548
  syncControlledReflection(next as CommittedHostNode, nextProps)
503
549
  }
504
550
 
505
- bindNodeMixRuntime(next as CommittedHostNode, frame, scheduler, styles)
551
+ if (next._mixState) {
552
+ bindNodeMixRuntime(next as CommittedHostNode, frame, scheduler, styles)
553
+ }
506
554
  if (shouldDispatchMixinLifecycle) {
507
555
  scheduler.enqueueCommitPhase([() => dispatchMixinCommit(nextMixState)])
508
556
  }
@@ -515,12 +563,115 @@ function setupHostNode(node: HostNode, dom: Element, scheduler: Scheduler): void
515
563
  let props = getHostProps(node)
516
564
  let committedNode = node as CommittedHostNode
517
565
 
566
+ syncDirectEventListeners(committedNode)
567
+
518
568
  if (shouldTrackControlledReflection(props)) {
519
569
  ensureControlledReflection(committedNode, scheduler)
520
570
  syncControlledReflection(committedNode, props)
521
571
  }
522
572
  }
523
573
 
574
+ function syncDirectEventListeners(node: CommittedHostNode): void {
575
+ let descriptors = node._directEventDescriptors as OnMixinDescriptor[] | undefined
576
+ if (!descriptors) {
577
+ teardownDirectEventListeners(node)
578
+ return
579
+ }
580
+ if (descriptors.length === 0) {
581
+ teardownDirectEventListeners(node)
582
+ return
583
+ }
584
+
585
+ let state = node._directEventState as DirectEventState | undefined
586
+ if (!state) {
587
+ state = { bindings: [] }
588
+ node._directEventState = state
589
+ }
590
+
591
+ let bindings = state.bindings
592
+ for (let index = 0; index < descriptors.length; index++) {
593
+ let descriptor = descriptors[index]
594
+ let [type, handler, captureBoolean = false] = descriptor.args
595
+ let binding = bindings[index]
596
+
597
+ if (!binding) {
598
+ binding = createDirectEventBinding(type, handler, captureBoolean)
599
+ bindings[index] = binding
600
+ attachDirectEventBinding(node._dom, binding)
601
+ continue
602
+ }
603
+
604
+ if (binding.type !== type || binding.capture !== captureBoolean) {
605
+ removeDirectEventBinding(node._dom, binding)
606
+ binding.type = type
607
+ binding.capture = captureBoolean
608
+ attachDirectEventBinding(node._dom, binding)
609
+ }
610
+
611
+ binding.handler = handler
612
+ }
613
+
614
+ for (let index = descriptors.length; index < bindings.length; index++) {
615
+ removeDirectEventBinding(node._dom, bindings[index])
616
+ }
617
+ bindings.length = descriptors.length
618
+ }
619
+
620
+ function createDirectEventBinding(
621
+ type: string,
622
+ handler: DirectEventBinding['handler'],
623
+ capture: boolean,
624
+ ): DirectEventBinding {
625
+ let binding: DirectEventBinding = {
626
+ type,
627
+ handler,
628
+ capture,
629
+ reentry: null,
630
+ stableHandler: null,
631
+ }
632
+
633
+ return binding
634
+ }
635
+
636
+ function getStableDirectEventHandler(binding: DirectEventBinding): (event: Event) => void {
637
+ if (binding.stableHandler) return binding.stableHandler
638
+
639
+ binding.stableHandler = (event) => {
640
+ invokeDirectEventBinding(binding, event)
641
+ }
642
+ return binding.stableHandler
643
+ }
644
+
645
+ function attachDirectEventBinding(dom: Element, binding: DirectEventBinding): void {
646
+ dom.addEventListener(binding.type, getStableDirectEventHandler(binding), binding.capture)
647
+ }
648
+
649
+ function removeDirectEventBinding(dom: Element, binding: DirectEventBinding): void {
650
+ if (binding.stableHandler) {
651
+ dom.removeEventListener(binding.type, binding.stableHandler, binding.capture)
652
+ }
653
+ binding.reentry?.abort(new DOMException('', 'AbortError'))
654
+ binding.reentry = null
655
+ }
656
+
657
+ function teardownDirectEventListeners(node: CommittedHostNode): void {
658
+ let state = node._directEventState as DirectEventState | undefined
659
+ if (!state) return
660
+
661
+ for (let binding of state.bindings) {
662
+ removeDirectEventBinding(node._dom, binding)
663
+ }
664
+
665
+ state.bindings.length = 0
666
+ node._directEventState = undefined
667
+ }
668
+
669
+ function invokeDirectEventBinding(binding: DirectEventBinding, event: Event): void {
670
+ binding.reentry?.abort(new DOMException('', 'EventReentry'))
671
+ binding.reentry = new AbortController()
672
+ void binding.handler(event, binding.reentry.signal)
673
+ }
674
+
524
675
  function diffText(curr: CommittedTextNode, next: TextNode, vParent: VNode) {
525
676
  if (curr._text !== next._text) {
526
677
  curr._dom.textContent = next._text
@@ -563,6 +714,10 @@ function insert(
563
714
  ? (dom: Node) => domParent.insertBefore(dom, anchor)
564
715
  : (dom: Node) => domParent.appendChild(dom)
565
716
 
717
+ if (isNonRenderNode(node)) {
718
+ return cursor
719
+ }
720
+
566
721
  if (isTextNode(node)) {
567
722
  if (cursor instanceof Text) {
568
723
  node._parent = vParent
@@ -621,9 +776,11 @@ function insert(
621
776
  rootTarget,
622
777
  childCursor,
623
778
  )
624
- diffHostProps({}, hostProps, targetHead)
779
+ patchHostProps({}, hostProps, targetHead)
625
780
  setupHostNode(node, targetHead, scheduler)
626
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
781
+ if (node._mixState) {
782
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
783
+ }
627
784
  return cursor
628
785
  }
629
786
  }
@@ -641,7 +798,7 @@ function insert(
641
798
  let cursorTag = node._svg ? cursor.tagName : cursor.tagName.toLowerCase()
642
799
  if (cursorTag === node.type) {
643
800
  let nextCursor = cursor.nextSibling
644
- diffHostProps({}, hostProps, cursor)
801
+ patchHostProps({}, hostProps, cursor)
645
802
 
646
803
  // Handle innerHTML prop
647
804
  if (hostProps.innerHTML != null) {
@@ -663,7 +820,9 @@ function insert(
663
820
  }
664
821
 
665
822
  setupHostNode(node, cursor, scheduler)
666
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
823
+ if (node._mixState) {
824
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
825
+ }
667
826
  return nextCursor
668
827
  } else {
669
828
  // Type mismatch - try single-advance retry to handle browser extension injections
@@ -674,7 +833,7 @@ function insert(
674
833
  if (nextTag === node.type) {
675
834
  let nextCursor = nextSibling.nextSibling
676
835
  // Found a match after skipping - adopt it and leave skipped node in place
677
- diffHostProps({}, hostProps, nextSibling)
836
+ patchHostProps({}, hostProps, nextSibling)
678
837
 
679
838
  if (hostProps.innerHTML != null) {
680
839
  nextSibling.innerHTML = hostProps.innerHTML as string
@@ -694,7 +853,9 @@ function insert(
694
853
  }
695
854
 
696
855
  setupHostNode(node, nextSibling, scheduler)
697
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
856
+ if (node._mixState) {
857
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
858
+ }
698
859
  return nextCursor
699
860
  }
700
861
  }
@@ -706,7 +867,7 @@ function insert(
706
867
  let dom = node._svg
707
868
  ? document.createElementNS(SVG_NS, node.type)
708
869
  : document.createElement(node.type)
709
- diffHostProps({}, hostProps, dom)
870
+ patchHostProps({}, hostProps, dom)
710
871
 
711
872
  // Handle innerHTML prop
712
873
  if (hostProps.innerHTML != null) {
@@ -716,7 +877,9 @@ function insert(
716
877
  }
717
878
 
718
879
  setupHostNode(node, dom, scheduler)
719
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles, false, domParent)
880
+ if (node._mixState) {
881
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles, false, domParent)
882
+ }
720
883
  doInsert(dom)
721
884
  return cursor
722
885
  }
@@ -724,17 +887,7 @@ function insert(
724
887
  if (isFragmentNode(node)) {
725
888
  // Insert fragment children in order before the same anchor
726
889
  for (let child of node._children) {
727
- cursor = insert(
728
- child,
729
- domParent,
730
- frame,
731
- scheduler,
732
- styles,
733
- vParent,
734
- rootTarget,
735
- anchor,
736
- cursor,
737
- )
890
+ cursor = insert(child, domParent, frame, scheduler, styles, node, rootTarget, anchor, cursor)
738
891
  }
739
892
  return cursor
740
893
  }
@@ -1121,6 +1274,8 @@ export function renderComponent(
1121
1274
  anchor?: Node,
1122
1275
  cursor?: Node | null,
1123
1276
  ): Node | null | undefined {
1277
+ if (handle.isRemoved()) return cursor
1278
+
1124
1279
  let [element, tasks] = handle.render(next.props)
1125
1280
  let content = toVNode(element)
1126
1281
  let newCursor = diffVNodes(
@@ -1226,6 +1381,7 @@ function cleanupDescendants(node: VNode, scheduler: Scheduler, styles: StyleMana
1226
1381
  }
1227
1382
 
1228
1383
  teardownMixins(node._mixState as MixinRuntimeState | undefined)
1384
+ teardownDirectEventListeners(node)
1229
1385
  teardownControlledReflection(node)
1230
1386
  if (node._controller) node._controller.abort()
1231
1387
  return
@@ -1323,6 +1479,7 @@ function performHostNodeRemoval(
1323
1479
  }
1324
1480
 
1325
1481
  teardownMixins(node._mixState as MixinRuntimeState | undefined)
1482
+ teardownDirectEventListeners(node)
1326
1483
  teardownControlledReflection(node)
1327
1484
  // Never remove the real document.head node when reconciling a <head> vnode.
1328
1485
  if (!isHeadHostNode(node)) {
@@ -1343,42 +1500,12 @@ function diffChildren(
1343
1500
  cursor?: Node | null,
1344
1501
  anchor?: Node,
1345
1502
  ) {
1346
- let nextLength = next.length
1503
+ let hasKeys = hasKeyedChildren(next)
1347
1504
 
1348
- // Warn when duplicate keys are present among siblings. Duplicate keys are
1349
- // still processed (last one wins), but they make keyed diffing ambiguous.
1350
- let hasKeys = false
1351
- let seenKeys: Set<string> | undefined
1352
- let duplicateKeys: Set<string> | undefined
1353
- for (let i = 0; i < nextLength; i++) {
1354
- let node = next[i]
1355
- if (node && node.key != null) {
1356
- hasKeys = true
1357
- if (!seenKeys) {
1358
- seenKeys = new Set([node.key])
1359
- continue
1360
- }
1361
- if (seenKeys.has(node.key)) {
1362
- if (!duplicateKeys) {
1363
- duplicateKeys = new Set()
1364
- }
1365
- duplicateKeys.add(node.key)
1366
- } else {
1367
- seenKeys.add(node.key)
1368
- }
1369
- }
1370
- }
1371
-
1372
- if (duplicateKeys?.size) {
1373
- let quotedKeys = Array.from(duplicateKeys, (key) => `"${key}"`)
1374
- console.warn(
1375
- `Duplicate keys detected in siblings: ${quotedKeys.join(', ')}. Keys should be unique.`,
1376
- )
1377
- }
1378
-
1379
- // Initial mount / hydration: delegate to insert() for each child so that
1380
- // hydration cursors and creation logic remain centralized there.
1381
1505
  if (curr === null) {
1506
+ if (hasKeys) {
1507
+ warnDuplicateKeys(next)
1508
+ }
1382
1509
  for (let node of next) {
1383
1510
  cursor = insert(
1384
1511
  node,
@@ -1396,14 +1523,23 @@ function diffChildren(
1396
1523
  return cursor
1397
1524
  }
1398
1525
 
1399
- let currLength = curr.length
1526
+ if (
1527
+ next.length === 0 &&
1528
+ anchor === undefined &&
1529
+ !parentUsesInnerHTML(vParent) &&
1530
+ canBulkClearChildren(curr)
1531
+ ) {
1532
+ for (let node of curr) {
1533
+ cleanupDescendants(node, scheduler, styles)
1534
+ }
1535
+ domParent.textContent = ''
1536
+ vParent._children = next
1537
+ return
1538
+ }
1400
1539
 
1401
- // Detect if any keys are present in the new children. If not, we can fall
1402
- // back to the simpler index-based diff which is cheaper and matches
1403
- // pre-existing behavior.
1404
1540
  if (!hasKeys) {
1405
- for (let i = 0; i < nextLength; i++) {
1406
- let currentNode = i < currLength ? curr[i] : null
1541
+ for (let i = 0; i < next.length; i++) {
1542
+ let currentNode = i < curr.length ? curr[i] : null
1407
1543
  diffVNodes(
1408
1544
  currentNode,
1409
1545
  next[i],
@@ -1418,8 +1554,8 @@ function diffChildren(
1418
1554
  )
1419
1555
  }
1420
1556
 
1421
- if (currLength > nextLength) {
1422
- for (let i = nextLength; i < currLength; i++) {
1557
+ if (curr.length > next.length) {
1558
+ for (let i = next.length; i < curr.length; i++) {
1423
1559
  let node = curr[i]
1424
1560
  if (node) remove(node, domParent, scheduler, styles)
1425
1561
  }
@@ -1429,160 +1565,137 @@ function diffChildren(
1429
1565
  return
1430
1566
  }
1431
1567
 
1432
- if (currLength === nextLength) {
1433
- let canDiffByPosition = true
1434
- for (let i = 0; i < nextLength; i++) {
1435
- let currentNode = curr[i]
1436
- let nextNode = next[i]
1437
- if (!currentNode || currentNode.key !== nextNode.key || currentNode.type !== nextNode.type) {
1438
- canDiffByPosition = false
1439
- break
1440
- }
1441
- }
1568
+ patchKeyedChildren(
1569
+ curr,
1570
+ next,
1571
+ domParent,
1572
+ frame,
1573
+ scheduler,
1574
+ styles,
1575
+ vParent,
1576
+ rootTarget,
1577
+ cursor,
1578
+ anchor,
1579
+ )
1442
1580
 
1443
- if (canDiffByPosition) {
1444
- for (let i = 0; i < nextLength; i++) {
1445
- diffVNodes(
1446
- curr[i],
1447
- next[i],
1448
- domParent,
1449
- frame,
1450
- scheduler,
1451
- styles,
1452
- vParent,
1453
- rootTarget,
1454
- anchor,
1455
- cursor,
1456
- )
1457
- }
1581
+ return
1582
+ }
1458
1583
 
1459
- vParent._children = next
1460
- return
1461
- }
1462
- }
1584
+ function parentUsesInnerHTML(parent: VNode): boolean {
1585
+ return isHostNode(parent) && getHostProps(parent).innerHTML != null
1586
+ }
1463
1587
 
1464
- // --- O(n + m) keyed diff with Map-based lookup ------------------------------
1588
+ function canBulkClearChildren(children: VNode[]): boolean {
1589
+ for (let child of children) {
1590
+ if (!canBulkClearNode(child)) return false
1591
+ }
1592
+ return true
1593
+ }
1465
1594
 
1466
- let oldChildren = curr
1467
- let oldChildrenLength = currLength
1468
- let remainingOldChildren = oldChildrenLength
1595
+ function canBulkClearNode(node: VNode): boolean {
1596
+ if (isCommittedTextNode(node)) return true
1469
1597
 
1470
- // Build key → index map for O(1) lookup: O(m)
1471
- let oldKeyMap = new Map<string, number>()
1472
- for (let i = 0; i < oldChildrenLength; i++) {
1473
- let c = oldChildren[i]
1474
- if (c) {
1475
- c._flags = 0
1476
- if (c.key != null) {
1477
- oldKeyMap.set(c.key, i)
1478
- }
1598
+ if (isCommittedHostNode(node)) {
1599
+ if (node._mixState) return false
1600
+ for (let child of node._children) {
1601
+ if (!canBulkClearNode(child)) return false
1479
1602
  }
1603
+ return true
1604
+ }
1605
+
1606
+ if (isFragmentNode(node)) {
1607
+ return canBulkClearChildren(node._children)
1480
1608
  }
1481
1609
 
1482
- let skew = 0
1483
- let newChildren: VNode[] = new Array(nextLength)
1610
+ if (isCommittedComponentNode(node)) {
1611
+ return canBulkClearNode(node._content)
1612
+ }
1484
1613
 
1485
- // First pass: match new children to old ones using Map lookup: O(n)
1486
- for (let i = 0; i < nextLength; i++) {
1487
- let childVNode = next[i]
1488
- if (!childVNode) {
1489
- newChildren[i] = childVNode
1490
- continue
1491
- }
1614
+ return false
1615
+ }
1492
1616
 
1493
- newChildren[i] = childVNode
1494
- childVNode._parent = vParent
1617
+ function hasKeyedChildren(children: VNode[]): boolean {
1618
+ for (let node of children) {
1619
+ if (node.key != null) return true
1620
+ }
1621
+ return false
1622
+ }
1495
1623
 
1496
- let skewedIndex = i + skew
1497
- let matchingIndex = -1
1624
+ function warnDuplicateKeys(children: VNode[]): void {
1625
+ let seenKeys: Set<string> | undefined
1626
+ let duplicateKeys: Set<string> | undefined
1498
1627
 
1499
- let key = childVNode.key
1500
- let type = childVNode.type
1628
+ for (let node of children) {
1629
+ if (node.key == null) continue
1501
1630
 
1502
- if (key != null) {
1503
- // O(1) Map lookup for keyed children
1504
- let mapIndex = oldKeyMap.get(key)
1505
- if (mapIndex !== undefined) {
1506
- let candidate = oldChildren[mapIndex]
1507
- let candidateFlags = candidate?._flags ?? 0
1508
- if (candidate && (candidateFlags & MATCHED) === 0 && candidate.type === type) {
1509
- matchingIndex = mapIndex
1510
- }
1511
- }
1512
- } else {
1513
- // Non-keyed children use positional identity only - no searching
1514
- let searchVNode = oldChildren[skewedIndex]
1515
- let searchFlags = searchVNode?._flags ?? 0
1516
- let available = searchVNode != null && (searchFlags & MATCHED) === 0
1517
- if (available && searchVNode.key == null && type === searchVNode.type) {
1518
- matchingIndex = skewedIndex
1519
- }
1631
+ if (!seenKeys) {
1632
+ seenKeys = new Set([node.key])
1633
+ continue
1520
1634
  }
1521
1635
 
1522
- childVNode._index = matchingIndex
1523
-
1524
- let matchedOldVNode: VNode | null = null
1525
- if (matchingIndex !== -1) {
1526
- matchedOldVNode = oldChildren[matchingIndex]
1527
- remainingOldChildren--
1528
- if (matchedOldVNode) {
1529
- matchedOldVNode._flags = (matchedOldVNode._flags ?? 0) | MATCHED
1530
- }
1636
+ if (seenKeys.has(node.key)) {
1637
+ duplicateKeys ??= new Set()
1638
+ duplicateKeys.add(node.key)
1639
+ } else {
1640
+ seenKeys.add(node.key)
1531
1641
  }
1642
+ }
1532
1643
 
1533
- // Determine whether this is a mount vs move and mark INSERT_VNODE
1534
- let oldDom = matchedOldVNode && findFirstDomAnchor(matchedOldVNode)
1535
- let isMounting = !matchedOldVNode || !oldDom
1536
- if (isMounting) {
1537
- if (matchingIndex === -1) {
1538
- // Adjust skew similar to Preact when lengths differ
1539
- if (nextLength > oldChildrenLength) {
1540
- skew--
1541
- } else if (nextLength < oldChildrenLength) {
1542
- skew++
1543
- }
1544
- }
1644
+ if (duplicateKeys?.size) {
1645
+ let quotedKeys = Array.from(duplicateKeys, (key) => `"${key}"`)
1646
+ console.warn(
1647
+ `Duplicate keys detected in siblings: ${quotedKeys.join(', ')}. Keys should be unique.`,
1648
+ )
1649
+ }
1650
+ }
1545
1651
 
1546
- childVNode._flags = (childVNode._flags ?? 0) | INSERT_VNODE
1547
- } else if (matchingIndex !== i + skew) {
1548
- if (matchingIndex === i + skew - 1) {
1549
- skew--
1550
- } else if (matchingIndex === i + skew + 1) {
1551
- skew++
1552
- } else {
1553
- if (matchingIndex! > i + skew) skew--
1554
- else skew++
1555
- childVNode._flags = (childVNode._flags ?? 0) | INSERT_VNODE
1652
+ function patchKeyedChildren(
1653
+ curr: VNode[],
1654
+ next: VNode[],
1655
+ domParent: ParentNode,
1656
+ frame: FrameHandle,
1657
+ scheduler: Scheduler,
1658
+ styles: StyleManager,
1659
+ vParent: VNode,
1660
+ rootTarget: EventTarget,
1661
+ cursor?: Node | null,
1662
+ anchor?: Node,
1663
+ ): void {
1664
+ let matches =
1665
+ matchKeyedChildrenInOrder(curr, next) ??
1666
+ matchKeyedChildrenAfterSingleRemoval(curr, next) ??
1667
+ matchKeyedChildrenAfterPairSwap(curr, next)
1668
+ if (!matches) {
1669
+ warnDuplicateKeys(next)
1670
+ matches = matchKeyedChildren(curr, next)
1671
+ }
1672
+
1673
+ let matchAnalysis = analyzeKeyedChildMatches(curr.length, matches)
1674
+ if (matchAnalysis.hasRemovals) {
1675
+ let usedOldIndexes = new Uint8Array(curr.length)
1676
+
1677
+ for (let match of matches) {
1678
+ if (match.oldIndex >= 0) {
1679
+ usedOldIndexes[match.oldIndex] = 1
1556
1680
  }
1557
1681
  }
1558
- }
1559
1682
 
1560
- // Unmount any old children that weren't matched
1561
- if (remainingOldChildren) {
1562
- for (let i = 0; i < oldChildrenLength; i++) {
1563
- let oldVNode = oldChildren[i]
1564
- if (oldVNode && ((oldVNode._flags ?? 0) & MATCHED) === 0) {
1565
- remove(oldVNode, domParent, scheduler, styles)
1683
+ for (let oldIndex = 0; oldIndex < curr.length; oldIndex++) {
1684
+ if (usedOldIndexes[oldIndex] === 0) {
1685
+ remove(curr[oldIndex], domParent, scheduler, styles)
1566
1686
  }
1567
1687
  }
1568
1688
  }
1569
1689
 
1570
- // Second pass: diff matched pairs and place/move DOM nodes in the correct
1571
- // order, similar to Preact's diffChildren + insert.
1572
- vParent._children = newChildren
1573
-
1574
- let lastPlaced: Node | null = null
1575
-
1576
- for (let i = 0; i < nextLength; i++) {
1577
- let childVNode = newChildren[i]
1578
- if (!childVNode) continue
1690
+ vParent._children = next
1579
1691
 
1580
- let idx = childVNode._index ?? -1
1581
- let oldVNode = idx >= 0 ? oldChildren[idx] : null
1692
+ for (let index = 0; index < next.length; index++) {
1693
+ let match = matches[index]
1694
+ let oldNode = match.oldIndex >= 0 ? curr[match.oldIndex] : null
1582
1695
 
1583
1696
  diffVNodes(
1584
- oldVNode,
1585
- childVNode,
1697
+ oldNode,
1698
+ next[index],
1586
1699
  domParent,
1587
1700
  frame,
1588
1701
  scheduler,
@@ -1592,40 +1705,255 @@ function diffChildren(
1592
1705
  anchor,
1593
1706
  cursor,
1594
1707
  )
1708
+ }
1595
1709
 
1596
- let shouldPlace = (childVNode._flags ?? 0) & INSERT_VNODE
1597
- let firstDom = findFirstDomAnchor(childVNode)
1598
- let lastDom = firstDom ? findLastDomAnchor(childVNode) : null
1599
- if (shouldPlace && firstDom && lastDom && firstDom.parentNode === domParent) {
1600
- let target: Node | null
1601
- if (lastPlaced === null) {
1602
- if (vParent._rangeStart && vParent._rangeStart.parentNode === domParent) {
1603
- target = vParent._rangeStart.nextSibling
1604
- } else {
1605
- target = domParent.firstChild
1710
+ if (matchAnalysis.canSkipPlacement) {
1711
+ return
1712
+ }
1713
+
1714
+ let stableIndexes = lisMatches(matches)
1715
+ let stableCursor = stableIndexes.length - 1
1716
+ let placementAnchor: Node | null = anchor ?? null
1717
+
1718
+ for (let index = next.length - 1; index >= 0; index--) {
1719
+ let nextNode = next[index]
1720
+ let isStable = stableIndexes[stableCursor] === index
1721
+
1722
+ if (isStable) {
1723
+ stableCursor--
1724
+ } else {
1725
+ placeVNode(nextNode, domParent, placementAnchor)
1726
+ }
1727
+
1728
+ placementAnchor = findFirstDomAnchor(nextNode) ?? placementAnchor
1729
+ }
1730
+ }
1731
+
1732
+ function matchKeyedChildren(curr: VNode[], next: VNode[]): Array<{ oldIndex: number }> {
1733
+ let oldKeyMap = new Map<string, number>()
1734
+ let usedOldIndexes = new Set<number>()
1735
+ let unkeyedSearchStart = 0
1736
+
1737
+ for (let index = 0; index < curr.length; index++) {
1738
+ let key = curr[index].key
1739
+ if (key != null) oldKeyMap.set(key, index)
1740
+ }
1741
+
1742
+ return next.map((nextNode) => {
1743
+ let oldIndex = -1
1744
+
1745
+ if (nextNode.key != null) {
1746
+ let keyedOldIndex = oldKeyMap.get(nextNode.key)
1747
+ if (keyedOldIndex !== undefined) {
1748
+ let oldNode = curr[keyedOldIndex]
1749
+ if (!usedOldIndexes.has(keyedOldIndex) && oldNode.type === nextNode.type) {
1750
+ oldIndex = keyedOldIndex
1606
1751
  }
1607
- } else {
1608
- target = lastPlaced.nextSibling
1609
1752
  }
1753
+ } else {
1754
+ for (let index = unkeyedSearchStart; index < curr.length; index++) {
1755
+ let oldNode = curr[index]
1756
+ if (usedOldIndexes.has(index) || oldNode.key != null || oldNode.type !== nextNode.type) {
1757
+ continue
1758
+ }
1759
+
1760
+ oldIndex = index
1761
+ unkeyedSearchStart = index + 1
1762
+ break
1763
+ }
1764
+ }
1610
1765
 
1611
- if (target === null && anchor) target = anchor
1766
+ if (oldIndex >= 0) usedOldIndexes.add(oldIndex)
1767
+ return { oldIndex }
1768
+ })
1769
+ }
1770
+
1771
+ function matchKeyedChildrenInOrder(
1772
+ curr: VNode[],
1773
+ next: VNode[],
1774
+ ): Array<{ oldIndex: number }> | null {
1775
+ let length = Math.min(curr.length, next.length)
1776
+ let matches: Array<{ oldIndex: number }> = []
1777
+
1778
+ for (let index = 0; index < length; index++) {
1779
+ let nextNode = next[index]
1780
+ if (nextNode.key == null) return null
1781
+
1782
+ let oldNode = curr[index]
1783
+ if (oldNode.key !== nextNode.key || oldNode.type !== nextNode.type) {
1784
+ return null
1785
+ }
1786
+
1787
+ matches.push({ oldIndex: index })
1788
+ }
1612
1789
 
1613
- // If target lies within the range we're moving, skip the move.
1614
- if (target && domRangeContainsNode(firstDom, lastDom, target)) {
1615
- // no-op
1616
- } else if (firstDom !== target) {
1617
- moveDomRange(domParent, firstDom, lastDom, target)
1790
+ for (let index = length; index < next.length; index++) {
1791
+ if (next[index].key == null) return null
1792
+ matches.push({ oldIndex: -1 })
1793
+ }
1794
+
1795
+ return matches
1796
+ }
1797
+
1798
+ function matchKeyedChildrenAfterSingleRemoval(
1799
+ curr: VNode[],
1800
+ next: VNode[],
1801
+ ): Array<{ oldIndex: number }> | null {
1802
+ if (curr.length !== next.length + 1) return null
1803
+
1804
+ let matches: Array<{ oldIndex: number }> = []
1805
+ let oldIndex = 0
1806
+ let skippedOldNode = false
1807
+
1808
+ for (let nextIndex = 0; nextIndex < next.length; nextIndex++) {
1809
+ let nextNode = next[nextIndex]
1810
+ if (nextNode.key == null) return null
1811
+
1812
+ let oldNode = curr[oldIndex]
1813
+ if (oldNode.key === nextNode.key && oldNode.type === nextNode.type) {
1814
+ matches.push({ oldIndex })
1815
+ oldIndex++
1816
+ continue
1817
+ }
1818
+
1819
+ if (skippedOldNode) return null
1820
+ skippedOldNode = true
1821
+ oldIndex++
1822
+
1823
+ oldNode = curr[oldIndex]
1824
+ if (oldNode.key !== nextNode.key || oldNode.type !== nextNode.type) {
1825
+ return null
1826
+ }
1827
+
1828
+ matches.push({ oldIndex })
1829
+ oldIndex++
1830
+ }
1831
+
1832
+ return matches
1833
+ }
1834
+
1835
+ function matchKeyedChildrenAfterPairSwap(
1836
+ curr: VNode[],
1837
+ next: VNode[],
1838
+ ): Array<{ oldIndex: number }> | null {
1839
+ if (curr.length !== next.length) return null
1840
+
1841
+ let matches: Array<{ oldIndex: number }> = []
1842
+ let firstMismatch = -1
1843
+ let secondMismatch = -1
1844
+
1845
+ for (let index = 0; index < next.length; index++) {
1846
+ let nextNode = next[index]
1847
+ if (nextNode.key == null) return null
1848
+
1849
+ let oldNode = curr[index]
1850
+ if (oldNode.key === nextNode.key && oldNode.type === nextNode.type) {
1851
+ matches.push({ oldIndex: index })
1852
+ continue
1853
+ }
1854
+
1855
+ if (firstMismatch === -1) {
1856
+ firstMismatch = index
1857
+ } else if (secondMismatch === -1) {
1858
+ secondMismatch = index
1859
+ } else {
1860
+ return null
1861
+ }
1862
+ matches.push({ oldIndex: -1 })
1863
+ }
1864
+
1865
+ if (firstMismatch === -1) return matches
1866
+ if (secondMismatch === -1) return null
1867
+
1868
+ let firstOldNode = curr[firstMismatch]
1869
+ let secondOldNode = curr[secondMismatch]
1870
+ let firstNextNode = next[firstMismatch]
1871
+ let secondNextNode = next[secondMismatch]
1872
+
1873
+ if (
1874
+ firstOldNode.key !== secondNextNode.key ||
1875
+ firstOldNode.type !== secondNextNode.type ||
1876
+ secondOldNode.key !== firstNextNode.key ||
1877
+ secondOldNode.type !== firstNextNode.type
1878
+ ) {
1879
+ return null
1880
+ }
1881
+
1882
+ matches[firstMismatch] = { oldIndex: secondMismatch }
1883
+ matches[secondMismatch] = { oldIndex: firstMismatch }
1884
+ return matches
1885
+ }
1886
+
1887
+ function analyzeKeyedChildMatches(
1888
+ currentLength: number,
1889
+ matches: ReadonlyArray<{ oldIndex: number }>,
1890
+ ): { hasRemovals: boolean; canSkipPlacement: boolean } {
1891
+ let hasRemovals = matches.length !== currentLength
1892
+ let canSkipPlacement = true
1893
+ let lastOldIndex = -1
1894
+ let sawNewNode = false
1895
+
1896
+ for (let match of matches) {
1897
+ if (match.oldIndex < 0) {
1898
+ hasRemovals = true
1899
+ sawNewNode = true
1900
+ continue
1901
+ }
1902
+
1903
+ if (sawNewNode || match.oldIndex < lastOldIndex) {
1904
+ canSkipPlacement = false
1905
+ }
1906
+ lastOldIndex = match.oldIndex
1907
+ }
1908
+
1909
+ return { hasRemovals, canSkipPlacement }
1910
+ }
1911
+
1912
+ function lisMatches(matches: ReadonlyArray<{ oldIndex: number }>): number[] {
1913
+ let predecessors = Array.from<number>({ length: matches.length })
1914
+ let tails: number[] = []
1915
+
1916
+ for (let index = 0; index < matches.length; index++) {
1917
+ let value = matches[index].oldIndex + 1
1918
+ if (value === 0) continue
1919
+
1920
+ let low = 0
1921
+ let high = tails.length
1922
+
1923
+ while (low < high) {
1924
+ let middle = (low + high) >> 1
1925
+
1926
+ if (matches[tails[middle]].oldIndex + 1 < value) {
1927
+ low = middle + 1
1928
+ } else {
1929
+ high = middle
1618
1930
  }
1619
1931
  }
1620
1932
 
1621
- if (lastDom) lastPlaced = lastDom
1933
+ predecessors[index] = low > 0 ? tails[low - 1] : -1
1934
+ tails[low] = index
1935
+ }
1936
+
1937
+ let cursor = tails.at(-1) ?? -1
1622
1938
 
1623
- // Clear internal flags for next diff
1624
- childVNode._flags = 0
1625
- childVNode._index = undefined
1939
+ for (let index = tails.length - 1; index >= 0; index--) {
1940
+ tails[index] = cursor
1941
+ cursor = predecessors[cursor] ?? -1
1626
1942
  }
1627
1943
 
1628
- return
1944
+ return tails
1945
+ }
1946
+
1947
+ function placeVNode(node: VNode, domParent: ParentNode, anchor: Node | null): void {
1948
+ let firstDom = findFirstDomAnchor(node)
1949
+ if (!firstDom || firstDom.parentNode !== domParent) return
1950
+
1951
+ let lastDom = findLastDomAnchor(node)
1952
+ if (!lastDom) return
1953
+ if (anchor && domRangeContainsNode(firstDom, lastDom, anchor)) return
1954
+ if (firstDom === anchor) return
1955
+
1956
+ moveDomRange(domParent, firstDom, lastDom, anchor)
1629
1957
  }
1630
1958
 
1631
1959
  export function findFirstDomAnchor(node: VNode | null | undefined): Node | null {
@@ -1693,15 +2021,23 @@ function shouldDispatchInlineMixinLifecycle(node: Node): boolean {
1693
2021
  return true
1694
2022
  }
1695
2023
 
1696
- export function findNextSiblingDomAnchor(curr: VNode, vParent?: VNode): Node | null {
2024
+ export function findNextSiblingDomAnchor(curr: VNode): Node | null {
2025
+ let vParent = curr._parent
1697
2026
  if (!vParent || !Array.isArray(vParent._children)) return null
1698
2027
  let children = vParent._children
2028
+ if (children.length === 0) return findNextSiblingDomAnchor(vParent)
2029
+
1699
2030
  let idx = children.indexOf(curr)
1700
2031
  if (idx === -1) return null
1701
2032
  for (let i = idx + 1; i < children.length; i++) {
1702
2033
  let dom = findFirstDomAnchor(children[i])
1703
2034
  if (dom) return dom
1704
2035
  }
2036
+
2037
+ if (isFragmentNode(vParent)) {
2038
+ return findNextSiblingDomAnchor(vParent)
2039
+ }
2040
+
1705
2041
  return null
1706
2042
  }
1707
2043
 
@@ -1721,6 +2057,7 @@ function reclaimPersistedMixinNode(
1721
2057
  newNode._parent = vParent
1722
2058
  newNode._controller = persistedNode._controller
1723
2059
  newNode._mixState = persistedNode._mixState
2060
+ newNode._directEventState = persistedNode._directEventState
1724
2061
  newNode._controlledState = persistedNode._controlledState
1725
2062
 
1726
2063
  let prevProps = getHostProps(persistedNode)
@@ -1733,7 +2070,8 @@ function reclaimPersistedMixinNode(
1733
2070
  if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {
1734
2071
  dispatchMixinBeforeUpdate(newNode._mixState as MixinRuntimeState | undefined)
1735
2072
  }
1736
- diffHostProps(prevProps, nextProps, persistedNode._dom)
2073
+ patchHostProps(prevProps, nextProps, persistedNode._dom)
2074
+ syncDirectEventListeners(newNode as CommittedHostNode)
1737
2075
  ensureControlledReflection(newNode as CommittedHostNode, scheduler)
1738
2076
  syncControlledReflection(newNode as CommittedHostNode, nextProps)
1739
2077
 
@@ -1748,7 +2086,9 @@ function reclaimPersistedMixinNode(
1748
2086
  rootTarget,
1749
2087
  )
1750
2088
 
1751
- bindNodeMixRuntime(newNode as CommittedHostNode, frame, scheduler, styles, true)
2089
+ if (newNode._mixState) {
2090
+ bindNodeMixRuntime(newNode as CommittedHostNode, frame, scheduler, styles, true)
2091
+ }
1752
2092
  if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {
1753
2093
  scheduler.enqueueCommitPhase([
1754
2094
  () => dispatchMixinCommit(newNode._mixState as MixinRuntimeState | undefined),