@@ -624,51 +624,40 @@ const Tooltip = ({
624
624
] )
625
625
626
626
useEffect ( ( ) => {
627
+ /**
628
+ * TODO(V6): break down observer callback for clarity
629
+ * - `handleAddedAnchors()`
630
+ * - `handleRemovedAnchors()`
631
+ */
627
632
let selector = imperativeOptions ?. anchorSelect ?? anchorSelect ?? ''
628
633
if ( ! selector && id ) {
629
634
selector = `[data-tooltip-id='${ id . replace ( / ' / g, "\\'" ) } ']`
630
635
}
631
636
const documentObserverCallback : MutationCallback = ( mutationList ) => {
632
- const newAnchors : HTMLElement [ ] = [ ]
633
- const removedAnchors : HTMLElement [ ] = [ ]
637
+ const addedAnchors = new Set < HTMLElement > ( )
638
+ const removedAnchors = new Set < HTMLElement > ( )
634
639
mutationList . forEach ( ( mutation ) => {
635
640
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' )
637
643
if ( newId === id ) {
638
- newAnchors . push ( mutation . target as HTMLElement )
644
+ addedAnchors . add ( target )
639
645
} else if ( mutation . oldValue === id ) {
640
646
// 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 )
642
648
}
643
649
}
644
650
if ( mutation . type !== 'childList' ) {
645
651
return
646
652
}
653
+ const removedNodes = [ ...mutation . removedNodes ] . filter ( ( node ) => node . nodeType === 1 )
647
654
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
+ */
672
661
if ( node ?. contains ?.( activeAnchor ) ) {
673
662
setRendered ( false )
674
663
handleShow ( false )
@@ -684,31 +673,63 @@ const Tooltip = ({
684
673
return
685
674
}
686
675
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
+ } )
701
693
} 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 */
706
727
}
707
728
} )
708
- if ( newAnchors . length || removedAnchors . length ) {
729
+ if ( addedAnchors . size || removedAnchors . size ) {
709
730
setAnchorElements ( ( anchors ) => [
710
- ...anchors . filter ( ( anchor ) => ! removedAnchors . includes ( anchor ) ) ,
711
- ...newAnchors ,
731
+ ...anchors . filter ( ( anchor ) => ! removedAnchors . has ( anchor ) ) ,
732
+ ...addedAnchors ,
712
733
] )
713
734
}
714
735
}
0 commit comments