@@ -624,51 +624,40 @@ const Tooltip = ({
624624 ] )
625625
626626 useEffect ( ( ) => {
627+ /**
628+ * TODO(V6): break down observer callback for clarity
629+ * - `handleAddedAnchors()`
630+ * - `handleRemovedAnchors()`
631+ */
627632 let selector = imperativeOptions ?. anchorSelect ?? anchorSelect ?? ''
628633 if ( ! selector && id ) {
629634 selector = `[data-tooltip-id='${ id . replace ( / ' / g, "\\'" ) } ']`
630635 }
631636 const documentObserverCallback : MutationCallback = ( mutationList ) => {
632- const newAnchors : HTMLElement [ ] = [ ]
633- const removedAnchors : HTMLElement [ ] = [ ]
637+ const addedAnchors = new Set < HTMLElement > ( )
638+ const removedAnchors = new Set < HTMLElement > ( )
634639 mutationList . forEach ( ( mutation ) => {
635640 if ( mutation . type === 'attributes' && mutation . attributeName === 'data-tooltip-id' ) {
636- const newId = ( mutation . target as HTMLElement ) . getAttribute ( 'data-tooltip-id' )
641+ const target = mutation . target as HTMLElement
642+ const newId = target . getAttribute ( 'data-tooltip-id' )
637643 if ( newId === id ) {
638- newAnchors . push ( mutation . target as HTMLElement )
644+ addedAnchors . add ( target )
639645 } else if ( mutation . oldValue === id ) {
640646 // data-tooltip-id has now been changed, so we need to remove this anchor
641- removedAnchors . push ( mutation . target as HTMLElement )
647+ removedAnchors . add ( target )
642648 }
643649 }
644650 if ( mutation . type !== 'childList' ) {
645651 return
646652 }
653+ const removedNodes = [ ...mutation . removedNodes ] . filter ( ( node ) => node . nodeType === 1 )
647654 if ( activeAnchor ) {
648- const elements = [ ...mutation . removedNodes ] . filter ( ( node ) => node . nodeType === 1 )
649- if ( selector ) {
650- try {
651- removedAnchors . push (
652- // the element itself is an anchor
653- ...( elements . filter ( ( element ) =>
654- ( element as HTMLElement ) . matches ( selector ) ,
655- ) as HTMLElement [ ] ) ,
656- )
657- removedAnchors . push (
658- // the element has children which are anchors
659- ...elements . flatMap (
660- ( element ) =>
661- [ ...( element as HTMLElement ) . querySelectorAll ( selector ) ] as HTMLElement [ ] ,
662- ) ,
663- )
664- } catch {
665- /**
666- * invalid CSS selector.
667- * already warned on tooltip controller
668- */
669- }
670- }
671- elements . some ( ( node ) => {
655+ removedNodes . some ( ( node ) => {
656+ /**
657+ * TODO(V6)
658+ * - isn't `!activeAnchor.isConnected` better?
659+ * - maybe move to `handleDisconnectedAnchor()`
660+ */
672661 if ( node ?. contains ?.( activeAnchor ) ) {
673662 setRendered ( false )
674663 handleShow ( false )
@@ -684,31 +673,63 @@ const Tooltip = ({
684673 return
685674 }
686675 try {
687- const elements = [ ...mutation . addedNodes ] . filter ( ( node ) => node . nodeType === 1 )
688- newAnchors . push (
689- // the element itself is an anchor
690- ...( elements . filter ( ( element ) =>
691- ( element as HTMLElement ) . matches ( selector ) ,
692- ) as HTMLElement [ ] ) ,
693- )
694- newAnchors . push (
695- // the element has children which are anchors
696- ...elements . flatMap (
697- ( element ) =>
698- [ ...( element as HTMLElement ) . querySelectorAll ( selector ) ] as HTMLElement [ ] ,
699- ) ,
700- )
676+ removedNodes . forEach ( ( node ) => {
677+ const element = node as HTMLElement
678+ if ( element . matches ( selector ) ) {
679+ // the element itself is an anchor
680+ removedAnchors . add ( element )
681+ } else {
682+ /**
683+ * TODO(V6): do we care if an element which is an anchor,
684+ * has children which are also anchors?
685+ * (i.e. should we remove `else` and always do this)
686+ */
687+ // the element has children which are anchors
688+ element
689+ . querySelectorAll ( selector )
690+ . forEach ( ( innerNode ) => removedAnchors . add ( innerNode as HTMLElement ) )
691+ }
692+ } )
701693 } catch {
702- /**
703- * invalid CSS selector.
704- * already warned on tooltip controller
705- */
694+ /* c8 ignore start */
695+ if ( ! process . env . NODE_ENV || process . env . NODE_ENV !== 'production' ) {
696+ // eslint-disable-next-line no-console
697+ console . warn ( `[react-tooltip] "${ selector } " is not a valid CSS selector` )
698+ }
699+ /* c8 ignore end */
700+ }
701+ try {
702+ const addedNodes = [ ...mutation . addedNodes ] . filter ( ( node ) => node . nodeType === 1 )
703+ addedNodes . forEach ( ( node ) => {
704+ const element = node as HTMLElement
705+ if ( element . matches ( selector ) ) {
706+ // the element itself is an anchor
707+ addedAnchors . add ( element )
708+ } else {
709+ /**
710+ * TODO(V6): do we care if an element which is an anchor,
711+ * has children which are also anchors?
712+ * (i.e. should we remove `else` and always do this)
713+ */
714+ // the element has children which are anchors
715+ element
716+ . querySelectorAll ( selector )
717+ . forEach ( ( innerNode ) => addedAnchors . add ( innerNode as HTMLElement ) )
718+ }
719+ } )
720+ } catch {
721+ /* c8 ignore start */
722+ if ( ! process . env . NODE_ENV || process . env . NODE_ENV !== 'production' ) {
723+ // eslint-disable-next-line no-console
724+ console . warn ( `[react-tooltip] "${ selector } " is not a valid CSS selector` )
725+ }
726+ /* c8 ignore end */
706727 }
707728 } )
708- if ( newAnchors . length || removedAnchors . length ) {
729+ if ( addedAnchors . size || removedAnchors . size ) {
709730 setAnchorElements ( ( anchors ) => [
710- ...anchors . filter ( ( anchor ) => ! removedAnchors . includes ( anchor ) ) ,
711- ...newAnchors ,
731+ ...anchors . filter ( ( anchor ) => ! removedAnchors . has ( anchor ) ) ,
732+ ...addedAnchors ,
712733 ] )
713734 }
714735 }
0 commit comments