Skip to content

Commit 956c253

Browse files
gabrieljablonskidanielbarion
authored andcommitted
refactor: added/removed nodes effect
1 parent 00a17e0 commit 956c253

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
@@ -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

Comments
 (0)