diff --git a/apps/web/src/Modal.test.tsx b/apps/web/src/Modal.test.tsx
new file mode 100644
index 0000000000..ccc414276e
--- /dev/null
+++ b/apps/web/src/Modal.test.tsx
@@ -0,0 +1,44 @@
+/*
+Copyright 2026 Element Creations Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+// @vitest-environment happy-dom
+
+import React from "react";
+import { describe, it, expect, beforeEach, afterEach } from "vitest";
+import { LinkedText } from "@element-hq/web-shared-components";
+import { screen } from "test-utils-rtl";
+import { flushPromises, stubClient } from "test-utils";
+import type { I18nApi } from "@element-hq/web-shared-components";
+
+import Modal from "./Modal";
+import { type ModuleApi } from "./modules/Api";
+
+function LinkedTextDialog({ onFinished }: { onFinished(): void }): React.JSX.Element {
+ return Check out https://matrix.org for more info;
+}
+
+describe("Modal", () => {
+ let originalMxModuleApi: ModuleApi;
+
+ beforeEach(() => {
+ stubClient();
+ // Required for the i18n context around the dialog to work.
+ originalMxModuleApi = window.mxModuleApi;
+ window.mxModuleApi = { i18n: {} as I18nApi } as ModuleApi;
+ });
+
+ afterEach(() => {
+ Modal.forceCloseAllModals();
+ window.mxModuleApi = originalMxModuleApi;
+ });
+
+ it("provides LinkedTextContext to dialogs rendered in the separate dialog root", async () => {
+ Modal.createDialog(LinkedTextDialog);
+ await flushPromises();
+ expect(screen.getByRole("link")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/src/Modal.tsx b/apps/web/src/Modal.tsx
index 8ba4313576..79cf22859d 100644
--- a/apps/web/src/Modal.tsx
+++ b/apps/web/src/Modal.tsx
@@ -12,7 +12,7 @@ import { createRoot, type Root } from "react-dom/client";
import classNames from "classnames";
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
import { Glass, TooltipProvider } from "@vector-im/compound-web";
-import { I18nContext } from "@element-hq/web-shared-components";
+import { I18nContext, LinkedTextContext } from "@element-hq/web-shared-components";
import defaultDispatcher from "./dispatcher/dispatcher";
import AsyncWrapper from "./AsyncWrapper";
@@ -20,6 +20,7 @@ import { type Defaultize } from "./@types/common";
import { type ActionPayload } from "./dispatcher/payloads";
import { filterBoolean } from "./utils/arrays.ts";
import { SDKContext } from "./contexts/SDKContext.ts";
+import { LinkedTextConfiguration } from "./Linkify";
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
@@ -439,22 +440,24 @@ export class ModalManager extends TypedEventEmitter
- {/* Provide I18nContext for shared-components used inside dialogs rendered in a separate root. */}
+ {/* Provide I18nContext and LinkedTextContext for shared-components used inside dialogs rendered in a separate root. */}
-
-
-
-
{this.staticModal.elem}
-
- {/* We break the rule here as this is a mouse-only interaction */}
- {/* oxlint-disable-next-line jsx-a11y/click-events-have-key-events */}
-
-
-
+
+
+
+
+
{this.staticModal.elem}
+
+ {/* We break the rule here as this is a mouse-only interaction */}
+ {/* oxlint-disable-next-line jsx-a11y/click-events-have-key-events */}
+
+
+
+
@@ -475,22 +478,24 @@ export class ModalManager extends TypedEventEmitter
- {/* Provide I18nContext for shared-components used inside dialogs rendered in a separate root. */}
+ {/* Provide I18nContext and LinkedTextContext for shared-components used inside dialogs rendered in a separate root. */}
-
-
-
-
{modal.elem}
-
- {/* We break the rule here as this is a mouse-only interaction */}
- {/* oxlint-disable-next-line jsx-a11y/click-events-have-key-events */}
-
-
-
+
+
+
+
+
{modal.elem}
+
+ {/* We break the rule here as this is a mouse-only interaction */}
+ {/* oxlint-disable-next-line jsx-a11y/click-events-have-key-events */}
+
+