@@ -631,51 +631,40 @@ const Tooltip = ({
631631 ] )
632632
633633 useEffect ( ( ) => {
634+ /**
635+ * TODO(V6): break down observer callback for clarity
636+ * - `handleAddedAnchors()`
637+ * - `handleRemovedAnchors()`
638+ */
634639 let selector = imperativeOptions ?. anchorSelect ?? anchorSelect ?? ''
635640 if ( ! selector && id ) {
636641 selector = `[data-tooltip-id='${ id } ']`
637642 }
638643 const documentObserverCallback : MutationCallback = ( mutationList ) => {
639- const newAnchors : HTMLElement [ ] = [ ]
640- const removedAnchors : HTMLElement [ ] = [ ]
644+ const addedAnchors = new Set < HTMLElement > ( )
645+ const removedAnchors = new Set < HTMLElement > ( )
641646 mutationList . forEach ( ( mutation ) => {
642647 if ( mutation . type === 'attributes' && mutation . attributeName === 'data-tooltip-id' ) {
643- const newId = ( mutation . target as HTMLElement ) . getAttribute ( 'data-tooltip-id' )
648+ const target = mutation . target as HTMLElement
649+ const newId = target . getAttribute ( 'data-tooltip-id' )
644650 if ( newId === id ) {
645- newAnchors . push ( mutation . target as HTMLElement )
651+ addedAnchors . add ( target )
646652 } else if ( mutation . oldValue === id ) {
647653 // data-tooltip-id has now been changed, so we need to remove this anchor
648- removedAnchors . push ( mutation . target as HTMLElement )
654+ removedAnchors . add ( target )
649655 }
650656 }
651657 if ( mutation . type !== 'childList' ) {
652658 return
653659 }
660+ const removedNodes = [ ...mutation . removedNodes ] . filter ( ( node ) => node . nodeType === 1 )
654661 if ( activeAnchor ) {
655- const elements = [ ...mutation . removedNodes ] . filter ( ( node ) => node . nodeType === 1 )
656- if ( selector ) {
657- try {
658- removedAnchors . push (
659- // the element itself is an anchor
660- ...( elements . filter ( ( element ) =>
661- ( element as HTMLElement ) . matches ( selector ) ,
662- ) as HTMLElement [ ] ) ,
663- )
664- removedAnchors . push (
665- // the element has children which are anchors
666- ...elements . flatMap (
667- ( element ) =>
668- [ ...( element as HTMLElement ) . querySelectorAll ( selector ) ] as HTMLElement [ ] ,
669- ) ,
670- )
671- } catch {
672- /**
673- * invalid CSS selector.
674- * already warned on tooltip controller
675- */
676- }
677- }
678- elements . some ( ( node ) => {
662+ removedNodes . some ( ( node ) => {
663+ /**
664+ * TODO(V6)
665+ * - isn't `!activeAnchor.isConnected` better?
666+ * - maybe move to `handleDisconnectedAnchor()`
667+ */
679668 if ( node ?. contains ?.( activeAnchor ) ) {
680669 setRendered ( false )
681670 handleShow ( false )
@@ -695,31 +684,63 @@ const Tooltip = ({
695684 return
696685 }
697686 try {
698- const elements = [ ...mutation . addedNodes ] . filter ( ( node ) => node . nodeType === 1 )
699- newAnchors . push (
700- // the element itself is an anchor
701- ...( elements . filter ( ( element ) =>
702- ( element as HTMLElement ) . matches ( selector ) ,
703- ) as HTMLElement [ ] ) ,
704- )
705- newAnchors . push (
706- // the element has children which are anchors
707- ...elements . flatMap (
708- ( element ) =>
709- [ ...( element as HTMLElement ) . querySelectorAll ( selector ) ] as HTMLElement [ ] ,
710- ) ,
711- )
687+ removedNodes . forEach ( ( node ) => {
688+ const element = node as HTMLElement
689+ if ( element . matches ( selector ) ) {
690+ // the element itself is an anchor
691+ removedAnchors . add ( element )
692+ } else {
693+ /**
694+ * TODO(V6): do we care if an element which is an anchor,
695+ * has children which are also anchors?
696+ * (i.e. should we remove `else` and always do this)
697+ */
698+ // the element has children which are anchors
699+ element
700+ . querySelectorAll ( selector )
701+ . forEach ( ( innerNode ) => removedAnchors . add ( innerNode as HTMLElement ) )
702+ }
703+ } )
712704 } catch {
713- /**
714- * invalid CSS selector.
715- * already warned on tooltip controller
716- */
705+ /* c8 ignore start */
706+ if ( ! process . env . NODE_ENV || process . env . NODE_ENV !== 'production' ) {
707+ // eslint-disable-next-line no-console
708+ console . warn ( `[react-tooltip] "${ selector } " is not a valid CSS selector` )
709+ }
710+ /* c8 ignore end */
711+ }
712+ try {
713+ const addedNodes = [ ...mutation . addedNodes ] . filter ( ( node ) => node . nodeType === 1 )
714+ addedNodes . forEach ( ( node ) => {
715+ const element = node as HTMLElement
716+ if ( element . matches ( selector ) ) {
717+ // the element itself is an anchor
718+ addedAnchors . add ( element )
719+ } else {
720+ /**
721+ * TODO(V6): do we care if an element which is an anchor,
722+ * has children which are also anchors?
723+ * (i.e. should we remove `else` and always do this)
724+ */
725+ // the element has children which are anchors
726+ element
727+ . querySelectorAll ( selector )
728+ . forEach ( ( innerNode ) => addedAnchors . add ( innerNode as HTMLElement ) )
729+ }
730+ } )
731+ } catch {
732+ /* c8 ignore start */
733+ if ( ! process . env . NODE_ENV || process . env . NODE_ENV !== 'production' ) {
734+ // eslint-disable-next-line no-console
735+ console . warn ( `[react-tooltip] "${ selector } " is not a valid CSS selector` )
736+ }
737+ /* c8 ignore end */
717738 }
718739 } )
719- if ( newAnchors . length || removedAnchors . length ) {
740+ if ( addedAnchors . size || removedAnchors . size ) {
720741 setAnchorElements ( ( anchors ) => [
721- ...anchors . filter ( ( anchor ) => ! removedAnchors . includes ( anchor ) ) ,
722- ...newAnchors ,
742+ ...anchors . filter ( ( anchor ) => ! removedAnchors . has ( anchor ) ) ,
743+ ...addedAnchors ,
723744 ] )
724745 }
725746 }
0 commit comments