Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions apps/app/src/components/ui/dialog.browser-dimming.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,23 @@ it("an app Dialog dims the browser through the shared-ui env seam", async () =>
expect(screen.getByTestId("dim").textContent).toBe("clear"),
);
});

it("a plugin Dialog dims the browser while its portal is open", async () => {
render(<DimProbe />);
const pluginDialog = document.createElement("div");
pluginDialog.setAttribute("data-bb-plugin-root", "");
pluginDialog.setAttribute("data-bb-portaled-overlay", "");
pluginDialog.setAttribute("role", "dialog");
pluginDialog.setAttribute("data-state", "open");
document.body.append(pluginDialog);

await waitFor(() =>
expect(screen.getByTestId("dim").textContent).toBe("dimmed"),
);

pluginDialog.setAttribute("data-state", "closed");
await waitFor(() =>
expect(screen.getByTestId("dim").textContent).toBe("clear"),
);
pluginDialog.remove();
});
75 changes: 73 additions & 2 deletions apps/app/src/hooks/useBrowserDimmingModal.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,72 @@
import { useEffect } from "react";
import { useEffect, useSyncExternalStore } from "react";
import { atom, useAtomValue, useSetAtom } from "jotai";

const browserDimmingModalCountAtom = atom(0);
const pluginPortalSelector = "[data-bb-plugin-root][data-bb-portaled-overlay]";
const pluginDialogSelector = `${pluginPortalSelector}[role="dialog"][data-state="open"]`;
const pluginDialogListeners = new Set<() => void>();
let pluginDialogObserver: MutationObserver | null = null;

function affectsPluginDialog(records: MutationRecord[]): boolean {
for (const record of records) {
if (record.type === "attributes") {
if (
record.attributeName === "data-bb-plugin-root" ||
record.attributeName === "data-bb-portaled-overlay" ||
(record.target instanceof Element &&
record.target.matches(pluginPortalSelector))
) {
return true;
}
continue;
}
for (const node of [...record.addedNodes, ...record.removedNodes]) {
if (
node instanceof Element &&
(node.matches(pluginPortalSelector) ||
node.querySelector(pluginPortalSelector) !== null)
) {
return true;
}
}
}
return false;
}

function subscribeToPluginDialogs(listener: () => void): () => void {
pluginDialogListeners.add(listener);
if (pluginDialogObserver === null && typeof document !== "undefined") {
pluginDialogObserver = new MutationObserver((records) => {
if (!affectsPluginDialog(records)) return;
for (const notify of pluginDialogListeners) notify();
});
pluginDialogObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: [
"data-bb-plugin-root",
"data-bb-portaled-overlay",
"data-state",
"role",
],
});
}
return () => {
pluginDialogListeners.delete(listener);
if (pluginDialogListeners.size === 0) {
pluginDialogObserver?.disconnect();
pluginDialogObserver = null;
}
};
}

function isPluginDialogOpen(): boolean {
return (
typeof document !== "undefined" &&
document.querySelector(pluginDialogSelector) !== null
);
}

export function useBrowserDimmingOverlay(active: boolean): void {
const setCount = useSetAtom(browserDimmingModalCountAtom);
Expand All @@ -19,5 +84,11 @@ export function useBrowserDimmingModal(active: boolean): void {
}

export function useIsBrowserDimmingModalOpen(): boolean {
return useAtomValue(browserDimmingModalCountAtom) > 0;
const appDialogOpen = useAtomValue(browserDimmingModalCountAtom) > 0;
const pluginDialogOpen = useSyncExternalStore(
subscribeToPluginDialogs,
isPluginDialogOpen,
() => false,
);
return appDialogOpen || pluginDialogOpen;
}
Loading