@@ -246,16 +246,11 @@ export const patchSlotInsertAdjacentElement = (HostElementPrototype: HTMLElement
246246
247247/**
248248 * Patches the text content of an unnamed slotted node inside a scoped component
249+ *
249250 * @param hostElementPrototype the `Element` to be patched
250251 */
251252export const patchTextContent = ( hostElementPrototype : HTMLElement ) : void => {
252- let descriptor = globalThis . Node && Object . getOwnPropertyDescriptor ( Node . prototype , 'textContent' ) ;
253-
254- if ( ! descriptor ) {
255- // for mock-doc
256- descriptor = Object . getOwnPropertyDescriptor ( hostElementPrototype , 'textContent' ) ;
257- }
258- if ( descriptor ) Object . defineProperty ( hostElementPrototype , '__textContent' , descriptor ) ;
253+ patchHostOriginalAccessor ( 'textContent' , hostElementPrototype ) ;
259254
260255 Object . defineProperty ( hostElementPrototype , 'textContent' , {
261256 get : function ( ) {
@@ -282,20 +277,7 @@ export const patchChildSlotNodes = (elm: HTMLElement) => {
282277 }
283278 }
284279
285- let childNodesFn = globalThis . Node && Object . getOwnPropertyDescriptor ( Node . prototype , 'childNodes' ) ;
286- if ( ! childNodesFn ) {
287- // for mock-doc
288- childNodesFn = Object . getOwnPropertyDescriptor ( elm , 'childNodes' ) ;
289- }
290- if ( childNodesFn ) Object . defineProperty ( elm , '__childNodes' , childNodesFn ) ;
291-
292- let childrenFn = Object . getOwnPropertyDescriptor ( Element . prototype , 'children' ) ;
293- if ( ! childrenFn ) {
294- // for mock-doc
295- childrenFn = Object . getOwnPropertyDescriptor ( elm , 'children' ) ;
296- }
297- if ( childrenFn ) Object . defineProperty ( elm , '__children' , childrenFn ) ;
298-
280+ patchHostOriginalAccessor ( 'children' , elm ) ;
299281 Object . defineProperty ( elm , 'children' , {
300282 get ( ) {
301283 return this . childNodes . filter ( ( n : any ) => n . nodeType === 1 ) ;
@@ -308,8 +290,21 @@ export const patchChildSlotNodes = (elm: HTMLElement) => {
308290 } ,
309291 } ) ;
310292
311- if ( ! childNodesFn ) return ;
293+ patchHostOriginalAccessor ( 'firstChild' , elm ) ;
294+ Object . defineProperty ( elm , 'firstChild' , {
295+ get ( ) {
296+ return this . childNodes [ 0 ] ;
297+ } ,
298+ } ) ;
299+
300+ patchHostOriginalAccessor ( 'lastChild' , elm ) ;
301+ Object . defineProperty ( elm , 'lastChild' , {
302+ get ( ) {
303+ return this . childNodes [ this . childNodes . length - 1 ] ;
304+ } ,
305+ } ) ;
312306
307+ patchHostOriginalAccessor ( 'childNodes' , elm ) ;
313308 Object . defineProperty ( elm , 'childNodes' , {
314309 get ( ) {
315310 if (
@@ -327,12 +322,163 @@ export const patchChildSlotNodes = (elm: HTMLElement) => {
327322 } ) ;
328323} ;
329324
325+ /// SLOTTED NODES ///
326+
327+ /**
328+ * Patches sibling accessors of a 'slotted' node within a non-shadow component.
329+ * Meaning whilst stepping through a non-shadow element's nodes, only the mock 'lightDOM' nodes are returned.
330+ * Especially relevant when rendering components via SSR... Frameworks will often try to reconcile their
331+ * VDOM with the real DOM by stepping through nodes with 'nextSibling' et al.
332+ * - `nextSibling`
333+ * - `nextElementSibling`
334+ * - `previousSibling`
335+ * - `previousElementSibling`
336+ *
337+ * @param node the slotted node to be patched
338+ */
339+ export const patchNextPrev = ( node : Node ) => {
340+ if ( ! node || ( node as any ) . __nextSibling || ! globalThis . Node ) return ;
341+
342+ patchNextSibling ( node ) ;
343+ patchPreviousSibling ( node ) ;
344+
345+ if ( node . nodeType === Node . ELEMENT_NODE ) {
346+ patchNextElementSibling ( node as Element ) ;
347+ patchPreviousElementSibling ( node as Element ) ;
348+ }
349+ } ;
350+
351+ /**
352+ * Patches the `nextSibling` accessor of a non-shadow slotted node
353+ *
354+ * @param node the slotted node to be patched
355+ * Required during during testing / mock environnement.
356+ */
357+ const patchNextSibling = ( node : Node ) => {
358+ // already been patched? return
359+ if ( ! node || ( node as any ) . __nextSibling ) return ;
360+
361+ patchHostOriginalAccessor ( 'nextSibling' , node ) ;
362+ Object . defineProperty ( node , 'nextSibling' , {
363+ get : function ( ) {
364+ const parentNodes = this [ 's-ol' ] ?. parentNode . childNodes ;
365+ const index = parentNodes ?. indexOf ( this ) ;
366+ if ( parentNodes && index > - 1 ) {
367+ return parentNodes [ index + 1 ] ;
368+ }
369+ return this . __nextSibling ;
370+ } ,
371+ } ) ;
372+ } ;
373+
374+ /**
375+ * Patches the `nextElementSibling` accessor of a non-shadow slotted node
376+ *
377+ * @param element the slotted element node to be patched
378+ * Required during during testing / mock environnement.
379+ */
380+ const patchNextElementSibling = ( element : Element ) => {
381+ if ( ! element || ( element as any ) . __nextElementSibling ) return ;
382+
383+ patchHostOriginalAccessor ( 'nextElementSibling' , element ) ;
384+ Object . defineProperty ( element , 'nextElementSibling' , {
385+ get : function ( ) {
386+ const parentEles = this [ 's-ol' ] ?. parentNode . children ;
387+ const index = parentEles ?. indexOf ( this ) ;
388+ if ( parentEles && index > - 1 ) {
389+ return parentEles [ index + 1 ] ;
390+ }
391+ return this . __nextElementSibling ;
392+ } ,
393+ } ) ;
394+ } ;
395+
396+ /**
397+ * Patches the `previousSibling` accessor of a non-shadow slotted node
398+ *
399+ * @param node the slotted node to be patched
400+ * Required during during testing / mock environnement.
401+ */
402+ const patchPreviousSibling = ( node : Node ) => {
403+ if ( ! node || ( node as any ) . __previousSibling ) return ;
404+
405+ patchHostOriginalAccessor ( 'previousSibling' , node ) ;
406+ Object . defineProperty ( node , 'previousSibling' , {
407+ get : function ( ) {
408+ const parentNodes = this [ 's-ol' ] ?. parentNode . childNodes ;
409+ const index = parentNodes ?. indexOf ( this ) ;
410+ if ( parentNodes && index > - 1 ) {
411+ return parentNodes [ index - 1 ] ;
412+ }
413+ return this . __previousSibling ;
414+ } ,
415+ } ) ;
416+ } ;
417+
418+ /**
419+ * Patches the `previousElementSibling` accessor of a non-shadow slotted node
420+ *
421+ * @param element the slotted element node to be patched
422+ * Required during during testing / mock environnement.
423+ */
424+ const patchPreviousElementSibling = ( element : Element ) => {
425+ if ( ! element || ( element as any ) . __previousElementSibling ) return ;
426+
427+ patchHostOriginalAccessor ( 'previousElementSibling' , element ) ;
428+ Object . defineProperty ( element , 'previousElementSibling' , {
429+ get : function ( ) {
430+ const parentNodes = this [ 's-ol' ] ?. parentNode . children ;
431+ const index = parentNodes ?. indexOf ( this ) ;
432+
433+ if ( parentNodes && index > - 1 ) {
434+ return parentNodes [ index - 1 ] ;
435+ }
436+ return this . __previousElementSibling ;
437+ } ,
438+ } ) ;
439+ } ;
440+
330441/// UTILS ///
331442
443+ const validElementPatches = [ 'children' , 'nextElementSibling' , 'previousElementSibling' ] as const ;
444+ const validNodesPatches = [
445+ 'childNodes' ,
446+ 'firstChild' ,
447+ 'lastChild' ,
448+ 'nextSibling' ,
449+ 'previousSibling' ,
450+ 'textContent' ,
451+ ] as const ;
452+
453+ /**
454+ * Patches a node or element; making it's original accessor method available under a new name.
455+ * e.g. `nextSibling` -> `__nextSibling`
456+ *
457+ * @param accessorName - the name of the accessor to patch
458+ * @param node - the node to patch
459+ */
460+ function patchHostOriginalAccessor (
461+ accessorName : ( typeof validElementPatches ) [ number ] | ( typeof validNodesPatches ) [ number ] ,
462+ node : Node ,
463+ ) {
464+ let accessor ;
465+ if ( validElementPatches . includes ( accessorName as any ) ) {
466+ accessor = Object . getOwnPropertyDescriptor ( Element . prototype , accessorName ) ;
467+ } else if ( validNodesPatches . includes ( accessorName as any ) ) {
468+ accessor = Object . getOwnPropertyDescriptor ( Node . prototype , accessorName ) ;
469+ }
470+ if ( ! accessor ) {
471+ // for mock-doc
472+ accessor = Object . getOwnPropertyDescriptor ( node , accessorName ) ;
473+ }
474+ if ( accessor ) Object . defineProperty ( node , '__' + accessorName , accessor ) ;
475+ }
476+
332477/**
333478 * Creates an empty text node to act as a forwarding address to a slotted node:
334479 * 1) When non-shadow components re-render, they need a place to temporarily put 'lightDOM' elements.
335480 * 2) Patched dom methods and accessors use this node to calculate what 'lightDOM' nodes are in the host.
481+ *
336482 * @param newChild a node that's going to be added to the component
337483 * @param slotNode the slot node that the node will be added to
338484 * @param prepend move the slotted location node to the beginning of the host
@@ -387,6 +533,7 @@ export const addSlotRelocateNode = (
387533 * Get's the child nodes of a component that are actually slotted.
388534 * This is only required until all patches are unified
389535 * either under 'experimentalSlotFixes' or on by default
536+ *
390537 * @param childNodes all 'internal' child nodes of the component
391538 * @returns An array of slotted reference nodes.
392539 */
@@ -406,6 +553,7 @@ const getSlotName = (node: d.RenderNode) =>
406553
407554/**
408555 * Recursively searches a series of child nodes for a slot with the provided name.
556+ *
409557 * @param childNodes the nodes to search for a slot with a specific name.
410558 * @param slotName the name of the slot to match on.
411559 * @param hostName the host name of the slot to match on.
0 commit comments