Skip to content

Commit 97bbbbd

Browse files
refactor: added/removed nodes effect
1 parent 5cabb44 commit 97bbbbd

File tree

1 file changed

+71
-50
lines changed

1 file changed

+71
-50
lines changed

src/components/Tooltip/Tooltip.tsx

Lines changed: 71 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)