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 assertionpull/28788/head^2
parent
48ae16b5a5
commit
e63072e21f
|
@ -59,8 +59,9 @@ describe("Pills", () => {
|
||||||
// find the pill in the timeline and click it
|
// find the pill in the timeline and click it
|
||||||
cy.get(".mx_EventTile_body .mx_Pill").click();
|
cy.get(".mx_EventTile_body .mx_Pill").click();
|
||||||
|
|
||||||
|
const localUrl = `/#/room/#${targetLocalpart}:`;
|
||||||
// verify we landed at a sane place
|
// verify we landed at a sane place
|
||||||
cy.url().should("contain", `/#/room/#${targetLocalpart}:`);
|
cy.url().should("contain", localUrl);
|
||||||
|
|
||||||
cy.wait(250); // let the room list settle
|
cy.wait(250); // let the room list settle
|
||||||
|
|
||||||
|
@ -69,7 +70,7 @@ describe("Pills", () => {
|
||||||
cy.get(".mx_EventTile_body .mx_Pill .mx_Pill_linkText")
|
cy.get(".mx_EventTile_body .mx_Pill .mx_Pill_linkText")
|
||||||
.should("have.css", "pointer-events", "none")
|
.should("have.css", "pointer-events", "none")
|
||||||
.click({ force: true }); // force is to ensure we bypass pointer-events
|
.click({ force: true }); // force is to ensure we bypass pointer-events
|
||||||
cy.url().should("contain", `https://matrix.to/#/#${targetLocalpart}:`);
|
cy.url().should("contain", localUrl);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -432,11 +432,17 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||||
* to start with (e.g. pills, links in the content).
|
* to start with (e.g. pills, links in the content).
|
||||||
*/
|
*/
|
||||||
private onBodyLinkClick = (e: MouseEvent): void => {
|
private onBodyLinkClick = (e: MouseEvent): void => {
|
||||||
const target = e.target as Element;
|
let target = e.target as HTMLLinkElement;
|
||||||
if (target.nodeName !== "A" || target.classList.contains(linkifyOpts.className)) return;
|
// links processed by linkifyjs have their own handler so don't handle those here
|
||||||
const { href } = target as HTMLLinkElement;
|
if (target.classList.contains(linkifyOpts.className)) return;
|
||||||
const localHref = tryTransformPermalinkToLocalHref(href);
|
if (target.nodeName !== "A") {
|
||||||
if (localHref !== href) {
|
// Jump to parent as the `<a>` may contain children, e.g. an anchor wrapping an inline code section
|
||||||
|
target = target.closest<HTMLLinkElement>("a");
|
||||||
|
}
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
const localHref = tryTransformPermalinkToLocalHref(target.href);
|
||||||
|
if (localHref !== target.href) {
|
||||||
// it could be converted to a localHref -> therefore handle locally
|
// it could be converted to a localHref -> therefore handle locally
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.location.hash = localHref;
|
window.location.hash = localHref;
|
||||||
|
|
|
@ -39,9 +39,7 @@ export function tooltipifyLinks(rootNodes: ArrayLike<Element>, ignoredNodes: Ele
|
||||||
let node = rootNodes[0];
|
let node = rootNodes[0];
|
||||||
|
|
||||||
while (node) {
|
while (node) {
|
||||||
let tooltipified = false;
|
if (ignoredNodes.includes(node) || containers.includes(node)) {
|
||||||
|
|
||||||
if (ignoredNodes.indexOf(node) >= 0) {
|
|
||||||
node = node.nextSibling as Element;
|
node = node.nextSibling as Element;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -49,20 +47,18 @@ export function tooltipifyLinks(rootNodes: ArrayLike<Element>, ignoredNodes: Ele
|
||||||
if (node.tagName === "A" && node.getAttribute("href")
|
if (node.tagName === "A" && node.getAttribute("href")
|
||||||
&& node.getAttribute("href") !== node.textContent.trim()
|
&& node.getAttribute("href") !== node.textContent.trim()
|
||||||
) {
|
) {
|
||||||
const container = document.createElement("span");
|
|
||||||
const href = node.getAttribute("href");
|
const href = node.getAttribute("href");
|
||||||
|
|
||||||
|
// The node's innerHTML was already sanitized before being rendered in the first place, here we are just
|
||||||
|
// wrapping the link with the LinkWithTooltip component, keeping the same children. Ideally we'd do this
|
||||||
|
// without the superfluous span but this is not something React trivially supports at this time.
|
||||||
const tooltip = <LinkWithTooltip tooltip={new URL(href, window.location.href).toString()}>
|
const tooltip = <LinkWithTooltip tooltip={new URL(href, window.location.href).toString()}>
|
||||||
{ node.innerHTML }
|
<span dangerouslySetInnerHTML={{ __html: node.innerHTML }} />
|
||||||
</LinkWithTooltip>;
|
</LinkWithTooltip>;
|
||||||
|
|
||||||
ReactDOM.render(tooltip, container);
|
ReactDOM.render(tooltip, node);
|
||||||
node.replaceChildren(container);
|
containers.push(node);
|
||||||
containers.push(container);
|
} else if (node.childNodes?.length) {
|
||||||
tooltipified = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.childNodes?.length && !tooltipified) {
|
|
||||||
tooltipifyLinks(node.childNodes as NodeListOf<Element>, ignoredNodes, containers);
|
tooltipifyLinks(node.childNodes as NodeListOf<Element>, ignoredNodes, containers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,4 +57,19 @@ describe('tooltipify', () => {
|
||||||
expect(containers).toHaveLength(0);
|
expect(containers).toHaveLength(0);
|
||||||
expect(root.outerHTML).toEqual(originalHtml);
|
expect(root.outerHTML).toEqual(originalHtml);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not re-wrap if called multiple times", () => {
|
||||||
|
const component = mount(<div><a href="/foo">click</a></div>);
|
||||||
|
const root = component.getDOMNode();
|
||||||
|
const containers: Element[] = [];
|
||||||
|
tooltipifyLinks([root], [], containers);
|
||||||
|
tooltipifyLinks([root], [], containers);
|
||||||
|
tooltipifyLinks([root], [], containers);
|
||||||
|
tooltipifyLinks([root], [], containers);
|
||||||
|
expect(containers).toHaveLength(1);
|
||||||
|
const anchor = root.querySelector("a");
|
||||||
|
expect(anchor?.getAttribute("href")).toEqual("/foo");
|
||||||
|
const tooltip = anchor.querySelector(".mx_TextWithTooltip_target");
|
||||||
|
expect(tooltip).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue