Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit e63072e

Browse files
authored
Fixes around URL tooltips and in-app matrix.to link handling (#9139)
* Add regression test for tooltipify exposing raw HTML * Handle m.to links involving children better * Comments * Fix mistaken assertion
1 parent 48ae16b commit e63072e

File tree

4 files changed

+37
-19
lines changed

4 files changed

+37
-19
lines changed

cypress/e2e/regression-tests/pills-click-in-app.spec.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ describe("Pills", () => {
5959
// find the pill in the timeline and click it
6060
cy.get(".mx_EventTile_body .mx_Pill").click();
6161

62+
const localUrl = `/#/room/#${targetLocalpart}:`;
6263
// verify we landed at a sane place
63-
cy.url().should("contain", `/#/room/#${targetLocalpart}:`);
64+
cy.url().should("contain", localUrl);
6465

6566
cy.wait(250); // let the room list settle
6667

@@ -69,7 +70,7 @@ describe("Pills", () => {
6970
cy.get(".mx_EventTile_body .mx_Pill .mx_Pill_linkText")
7071
.should("have.css", "pointer-events", "none")
7172
.click({ force: true }); // force is to ensure we bypass pointer-events
72-
cy.url().should("contain", `https://matrix.to/#/#${targetLocalpart}:`);
73+
cy.url().should("contain", localUrl);
7374
});
7475
});
7576
});

src/components/views/messages/TextualBody.tsx

+11-5
Original file line numberDiff line numberDiff line change
@@ -432,11 +432,17 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
432432
* to start with (e.g. pills, links in the content).
433433
*/
434434
private onBodyLinkClick = (e: MouseEvent): void => {
435-
const target = e.target as Element;
436-
if (target.nodeName !== "A" || target.classList.contains(linkifyOpts.className)) return;
437-
const { href } = target as HTMLLinkElement;
438-
const localHref = tryTransformPermalinkToLocalHref(href);
439-
if (localHref !== href) {
435+
let target = e.target as HTMLLinkElement;
436+
// links processed by linkifyjs have their own handler so don't handle those here
437+
if (target.classList.contains(linkifyOpts.className)) return;
438+
if (target.nodeName !== "A") {
439+
// Jump to parent as the `<a>` may contain children, e.g. an anchor wrapping an inline code section
440+
target = target.closest<HTMLLinkElement>("a");
441+
}
442+
if (!target) return;
443+
444+
const localHref = tryTransformPermalinkToLocalHref(target.href);
445+
if (localHref !== target.href) {
440446
// it could be converted to a localHref -> therefore handle locally
441447
e.preventDefault();
442448
window.location.hash = localHref;

src/utils/tooltipify.tsx

+8-12
Original file line numberDiff line numberDiff line change
@@ -39,30 +39,26 @@ export function tooltipifyLinks(rootNodes: ArrayLike<Element>, ignoredNodes: Ele
3939
let node = rootNodes[0];
4040

4141
while (node) {
42-
let tooltipified = false;
43-
44-
if (ignoredNodes.indexOf(node) >= 0) {
42+
if (ignoredNodes.includes(node) || containers.includes(node)) {
4543
node = node.nextSibling as Element;
4644
continue;
4745
}
4846

4947
if (node.tagName === "A" && node.getAttribute("href")
5048
&& node.getAttribute("href") !== node.textContent.trim()
5149
) {
52-
const container = document.createElement("span");
5350
const href = node.getAttribute("href");
5451

52+
// The node's innerHTML was already sanitized before being rendered in the first place, here we are just
53+
// wrapping the link with the LinkWithTooltip component, keeping the same children. Ideally we'd do this
54+
// without the superfluous span but this is not something React trivially supports at this time.
5555
const tooltip = <LinkWithTooltip tooltip={new URL(href, window.location.href).toString()}>
56-
{ node.innerHTML }
56+
<span dangerouslySetInnerHTML={{ __html: node.innerHTML }} />
5757
</LinkWithTooltip>;
5858

59-
ReactDOM.render(tooltip, container);
60-
node.replaceChildren(container);
61-
containers.push(container);
62-
tooltipified = true;
63-
}
64-
65-
if (node.childNodes?.length && !tooltipified) {
59+
ReactDOM.render(tooltip, node);
60+
containers.push(node);
61+
} else if (node.childNodes?.length) {
6662
tooltipifyLinks(node.childNodes as NodeListOf<Element>, ignoredNodes, containers);
6763
}
6864

test/utils/tooltipify-test.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,19 @@ describe('tooltipify', () => {
5757
expect(containers).toHaveLength(0);
5858
expect(root.outerHTML).toEqual(originalHtml);
5959
});
60+
61+
it("does not re-wrap if called multiple times", () => {
62+
const component = mount(<div><a href="/foo">click</a></div>);
63+
const root = component.getDOMNode();
64+
const containers: Element[] = [];
65+
tooltipifyLinks([root], [], containers);
66+
tooltipifyLinks([root], [], containers);
67+
tooltipifyLinks([root], [], containers);
68+
tooltipifyLinks([root], [], containers);
69+
expect(containers).toHaveLength(1);
70+
const anchor = root.querySelector("a");
71+
expect(anchor?.getAttribute("href")).toEqual("/foo");
72+
const tooltip = anchor.querySelector(".mx_TextWithTooltip_target");
73+
expect(tooltip).toBeDefined();
74+
});
6075
});

0 commit comments

Comments
 (0)