mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
fix(vom): overlay
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
import { useTranslation } from "@browser-skill/i18n/react";
|
||||
import { RiStopCircleLine } from "@remixicon/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import logoUrl from "../../assets/logo.png";
|
||||
|
||||
export interface ControlOverlayProps {
|
||||
visible: boolean;
|
||||
interrupting: boolean;
|
||||
automationBypass: boolean;
|
||||
onInterrupt: () => void;
|
||||
}
|
||||
|
||||
export function ControlOverlay({
|
||||
visible,
|
||||
interrupting,
|
||||
automationBypass,
|
||||
onInterrupt,
|
||||
}: ControlOverlayProps) {
|
||||
const { t } = useTranslation("extension");
|
||||
const [show, setShow] = useState(false);
|
||||
const blockerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const raf = requestAnimationFrame(() => setShow(true));
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}
|
||||
setShow(false);
|
||||
}, [visible]);
|
||||
|
||||
useEffect(() => {
|
||||
const blocker = blockerRef.current;
|
||||
if (!blocker) return;
|
||||
const stopScroll = (event: WheelEvent | TouchEvent) => {
|
||||
if (automationBypass) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
blocker.addEventListener("wheel", stopScroll, { passive: false });
|
||||
blocker.addEventListener("touchmove", stopScroll, { passive: false });
|
||||
return () => {
|
||||
blocker.removeEventListener("wheel", stopScroll);
|
||||
blocker.removeEventListener("touchmove", stopScroll);
|
||||
};
|
||||
}, [automationBypass]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || automationBypass) return;
|
||||
const stopScroll = (event: WheelEvent | TouchEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
window.addEventListener("wheel", stopScroll, { capture: true, passive: false });
|
||||
window.addEventListener("touchmove", stopScroll, { capture: true, passive: false });
|
||||
return () => {
|
||||
window.removeEventListener("wheel", stopScroll, { capture: true });
|
||||
window.removeEventListener("touchmove", stopScroll, { capture: true });
|
||||
};
|
||||
}, [visible, automationBypass]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
const pointerEvents = automationBypass ? "none" : "auto";
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
@keyframes bsk-breathe {
|
||||
0%, 100% {
|
||||
box-shadow: inset 0 0 20px 4px rgba(249,115,22,0.25);
|
||||
}
|
||||
50% {
|
||||
box-shadow: inset 0 0 40px 8px rgba(249,115,22,0.5);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div
|
||||
data-slot="control-overlay"
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 2147483646,
|
||||
pointerEvents: "none",
|
||||
animation: "bsk-breathe 3s ease-in-out infinite",
|
||||
opacity: show ? 1 : 0,
|
||||
transition: "opacity 300ms ease-out",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={blockerRef}
|
||||
data-slot="control-overlay-blocker"
|
||||
onPointerDown={(event) => {
|
||||
if (automationBypass) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onClick={(event) => {
|
||||
if (automationBypass) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 2147483646,
|
||||
pointerEvents,
|
||||
background: "transparent",
|
||||
opacity: show ? 1 : 0,
|
||||
transition: "opacity 300ms ease-out",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
data-slot="control-overlay-pill"
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: 32,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 2147483647,
|
||||
pointerEvents: "auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: 9999,
|
||||
padding: "10px 10px 10px 20px",
|
||||
boxShadow: "0 8px 32px rgba(124,45,18,0.16), 0 2px 8px rgba(0,0,0,0.1)",
|
||||
opacity: show ? 1 : 0,
|
||||
transition: "opacity 300ms ease-out",
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt="browser-skill"
|
||||
style={{ width: 24, height: 24, borderRadius: 4, flexShrink: 0 }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: 500,
|
||||
color: "#333",
|
||||
whiteSpace: "nowrap",
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
{t("controlOverlay.status")}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
data-slot="control-overlay-stop-all"
|
||||
disabled={interrupting}
|
||||
onClick={onInterrupt}
|
||||
style={{
|
||||
pointerEvents: "auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
border: "none",
|
||||
borderRadius: 9999,
|
||||
padding: "8px 20px 8px 16px",
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
color: "#fff",
|
||||
backgroundColor: interrupting ? "#9ca3af" : "#f97316",
|
||||
cursor: interrupting ? "default" : "pointer",
|
||||
opacity: interrupting ? 0.7 : 1,
|
||||
transition: "background-color 150ms ease-out, opacity 150ms ease-out",
|
||||
whiteSpace: "nowrap",
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
<RiStopCircleLine size={18} color="#fff" />
|
||||
{interrupting ? t("controlOverlay.interrupting") : t("controlOverlay.interrupt")}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -48,6 +48,7 @@ const VIEWPORT_MARGIN = 16;
|
||||
const PANEL_WIDTH = 420;
|
||||
const FALLBACK_PANEL_H = 180;
|
||||
const FALLBACK_PANEL_H_COLLAPSED = 44;
|
||||
const EMPTY_SELECTORS: string[] = [];
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
@@ -177,7 +178,7 @@ function measure(selectors: string[]): Box[] {
|
||||
export function HelpRequestOverlay({ request }: Props) {
|
||||
const { t } = useTranslation("extension");
|
||||
const isCompact = request?.displayMode === "compact";
|
||||
const effectiveSelectors = isCompact ? [] : (request?.selectors ?? []);
|
||||
const effectiveSelectors = isCompact ? EMPTY_SELECTORS : (request?.selectors ?? EMPTY_SELECTORS);
|
||||
const [note, setNote] = useState("");
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [boxes, setBoxes] = useState<Box[]>([]);
|
||||
@@ -226,7 +227,7 @@ export function HelpRequestOverlay({ request }: Props) {
|
||||
// Keep highlight boxes aligned with the page as it scrolls / resizes.
|
||||
useEffect(() => {
|
||||
if (!request) {
|
||||
setBoxes([]);
|
||||
setBoxes((prev) => (prev.length === 0 ? prev : []));
|
||||
return;
|
||||
}
|
||||
const update = () => {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ControlOverlay } from "../ControlOverlay";
|
||||
|
||||
describe("ControlOverlay", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("keeps page blocker none under automationBypass but Interrupt stays clickable", () => {
|
||||
const { container } = render(
|
||||
<ControlOverlay
|
||||
visible={true}
|
||||
interrupting={false}
|
||||
automationBypass={true}
|
||||
onInterrupt={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const blocker = container.querySelector("[data-slot='control-overlay-blocker']");
|
||||
expect(blocker).toBeTruthy();
|
||||
expect((blocker as HTMLElement).style.pointerEvents).toBe("none");
|
||||
|
||||
const pill = container.querySelector("[data-slot='control-overlay-pill']");
|
||||
expect(pill).toBeTruthy();
|
||||
expect((pill as HTMLElement).style.pointerEvents).toBe("auto");
|
||||
|
||||
const stopBtn = container.querySelector("[data-slot='control-overlay-stop-all']");
|
||||
expect(stopBtn).toBeTruthy();
|
||||
expect((stopBtn as HTMLElement).style.pointerEvents).toBe("auto");
|
||||
});
|
||||
|
||||
it("uses pointer-events auto on blocker when automationBypass is false", () => {
|
||||
const { container } = render(
|
||||
<ControlOverlay
|
||||
visible={true}
|
||||
interrupting={false}
|
||||
automationBypass={false}
|
||||
onInterrupt={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const blocker = container.querySelector("[data-slot='control-overlay-blocker']");
|
||||
expect(blocker).toBeTruthy();
|
||||
expect((blocker as HTMLElement).style.pointerEvents).toBe("auto");
|
||||
});
|
||||
|
||||
it("calls onInterrupt from the stop button", () => {
|
||||
const onInterrupt = vi.fn();
|
||||
const { container } = render(
|
||||
<ControlOverlay
|
||||
visible={true}
|
||||
interrupting={false}
|
||||
automationBypass={false}
|
||||
onInterrupt={onInterrupt}
|
||||
/>,
|
||||
);
|
||||
|
||||
const stopBtn = container.querySelector("[data-slot='control-overlay-stop-all']");
|
||||
(stopBtn as HTMLButtonElement).click();
|
||||
|
||||
expect(onInterrupt).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { createElement } from "react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { type HelpRequestData, HelpRequestOverlay } from "../HelpRequestOverlay";
|
||||
|
||||
function renderOverlay(req: HelpRequestData) {
|
||||
function renderOverlay(req: HelpRequestData | null) {
|
||||
return render(
|
||||
createElement(I18nextProvider, { i18n }, createElement(HelpRequestOverlay, { request: req })),
|
||||
);
|
||||
@@ -38,6 +38,16 @@ describe("HelpRequestOverlay", () => {
|
||||
expect(screen.getByText("Please complete the captcha")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps the inactive render stable", () => {
|
||||
const { container, rerender } = renderOverlay(null);
|
||||
|
||||
rerender(
|
||||
createElement(I18nextProvider, { i18n }, createElement(HelpRequestOverlay, { request: null })),
|
||||
);
|
||||
|
||||
expect(container.querySelector("[data-slot='help-request-banner']")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders a compact status without full request controls", () => {
|
||||
const el = document.createElement("div");
|
||||
el.id = "login";
|
||||
|
||||
@@ -1,2 +1,16 @@
|
||||
@import "tailwindcss";
|
||||
@source "../content";
|
||||
|
||||
:host([data-bsk-overlay-surface]) {
|
||||
position: fixed !important;
|
||||
inset: 0 !important;
|
||||
display: block !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
z-index: 2147483647 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
:host([data-bsk-overlay-surface][data-bsk-overlay-blocking]) {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { i18n } from "@browser-skill/i18n";
|
||||
import { I18nextProvider } from "@browser-skill/i18n/react";
|
||||
import React from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BorrowConfirmationOverlay } from "@/content/BorrowConfirmationOverlay";
|
||||
import { ControlOverlay } from "@/content/ControlOverlay";
|
||||
import { HelpRequestOverlay } from "@/content/HelpRequestOverlay";
|
||||
import overlayCss from "@/content/overlay.css?inline";
|
||||
import { OverlayController, shouldShowAgentControlOverlay } from "@/content/overlay-controller";
|
||||
@@ -45,7 +47,6 @@ import type {
|
||||
BorrowRequestMessage,
|
||||
BorrowResponseMessage,
|
||||
} from "@/tools/borrow-confirmation";
|
||||
import logoUrl from "../../assets/logo.png";
|
||||
|
||||
// Run at document_end so the overlay does not block first paint. Only attach
|
||||
// in the top-level frame so iframes do not double-render overlays.
|
||||
@@ -64,7 +65,6 @@ export default defineContentScript({
|
||||
let reactRoot: ReactDOM.Root | null = null;
|
||||
let overlayHost: HTMLElement | null = null;
|
||||
let overlayContainer: HTMLElement | null = null;
|
||||
let controlRoot: HTMLElement | null = null;
|
||||
let activeAgentState: OverlayAgentStateMessage | null = null;
|
||||
let hostLossReported = false;
|
||||
let remountInProgress = false;
|
||||
@@ -91,29 +91,23 @@ export default defineContentScript({
|
||||
onRemove(root) {
|
||||
overlayHost = null;
|
||||
overlayContainer = null;
|
||||
controlRoot = null;
|
||||
root?.unmount();
|
||||
reactRoot = null;
|
||||
},
|
||||
});
|
||||
|
||||
function removeControlOverlay(): void {
|
||||
controlRoot?.remove();
|
||||
controlRoot = null;
|
||||
}
|
||||
|
||||
function setControlSurfaceActive(active: boolean): void {
|
||||
function setOverlaySurfaceState(active: boolean, blocking: boolean): void {
|
||||
const host = overlayHost;
|
||||
const container = overlayContainer;
|
||||
if (!host || !container) return;
|
||||
|
||||
if (active) {
|
||||
Object.assign(host.style, {
|
||||
position: "fixed",
|
||||
inset: "0",
|
||||
zIndex: "2147483647",
|
||||
pointerEvents: "auto",
|
||||
});
|
||||
host.setAttribute("data-bsk-overlay-surface", "");
|
||||
if (blocking) {
|
||||
host.setAttribute("data-bsk-overlay-blocking", "");
|
||||
} else {
|
||||
host.removeAttribute("data-bsk-overlay-blocking");
|
||||
}
|
||||
Object.assign(container.style, {
|
||||
position: "fixed",
|
||||
inset: "0",
|
||||
@@ -122,226 +116,73 @@ export default defineContentScript({
|
||||
return;
|
||||
}
|
||||
|
||||
host.style.removeProperty("position");
|
||||
host.style.removeProperty("inset");
|
||||
host.style.removeProperty("z-index");
|
||||
host.style.removeProperty("pointer-events");
|
||||
host.removeAttribute("data-bsk-overlay-surface");
|
||||
host.removeAttribute("data-bsk-overlay-blocking");
|
||||
container.style.removeProperty("position");
|
||||
container.style.removeProperty("inset");
|
||||
container.style.removeProperty("pointer-events");
|
||||
}
|
||||
|
||||
function ensureControlOverlayRoot(): HTMLElement | null {
|
||||
if (!overlayContainer) return null;
|
||||
if (!controlRoot) {
|
||||
controlRoot = document.createElement("div");
|
||||
controlRoot.className = "bsk-control-overlay";
|
||||
controlRoot.style.opacity = "0";
|
||||
controlRoot.style.transition = "opacity 300ms ease-out";
|
||||
overlayContainer.append(controlRoot);
|
||||
window.requestAnimationFrame(() => {
|
||||
if (controlRoot) controlRoot.style.opacity = "1";
|
||||
});
|
||||
}
|
||||
return controlRoot;
|
||||
}
|
||||
function setOverlayHostHiddenFromAccessibility(hidden: boolean): void {
|
||||
const host = overlayHost;
|
||||
if (!host) return;
|
||||
|
||||
function createStopCircleIcon(): SVGSVGElement {
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("width", "18");
|
||||
svg.setAttribute("height", "18");
|
||||
svg.setAttribute("viewBox", "0 0 24 24");
|
||||
svg.setAttribute("fill", "currentColor");
|
||||
svg.setAttribute("aria-hidden", "true");
|
||||
svg.style.color = "#fff";
|
||||
svg.style.flexShrink = "0";
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
path.setAttribute(
|
||||
"d",
|
||||
"M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10Zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM9 9h6v6H9V9Z",
|
||||
);
|
||||
svg.append(path);
|
||||
return svg;
|
||||
}
|
||||
|
||||
function renderControlOverlay(): void {
|
||||
const overlayState = overlays.snapshot();
|
||||
if (!shouldShowAgentControlOverlay(overlayState)) {
|
||||
removeControlOverlay();
|
||||
setControlSurfaceActive(false);
|
||||
if (!hidden) {
|
||||
host.removeAttribute("aria-hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
const root = ensureControlOverlayRoot();
|
||||
if (!root) return;
|
||||
|
||||
const pointerEvents = overlayState.automationBypassCount > 0 ? "none" : "auto";
|
||||
setControlSurfaceActive(true);
|
||||
root.innerHTML = "";
|
||||
Object.assign(root.style, {
|
||||
position: "fixed",
|
||||
inset: "0",
|
||||
pointerEvents: "none",
|
||||
});
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
@keyframes bsk-breathe {
|
||||
0%, 100% { box-shadow: inset 0 0 20px 4px rgba(249,115,22,0.25); }
|
||||
50% { box-shadow: inset 0 0 40px 8px rgba(249,115,22,0.5); }
|
||||
}
|
||||
`;
|
||||
|
||||
const border = document.createElement("div");
|
||||
border.setAttribute("data-slot", "control-overlay");
|
||||
Object.assign(border.style, {
|
||||
position: "fixed",
|
||||
inset: "0",
|
||||
zIndex: "2147483646",
|
||||
pointerEvents: "none",
|
||||
animation: "bsk-breathe 3s ease-in-out infinite",
|
||||
});
|
||||
|
||||
const blocker = document.createElement("div");
|
||||
blocker.setAttribute("data-slot", "control-overlay-blocker");
|
||||
Object.assign(blocker.style, {
|
||||
position: "fixed",
|
||||
inset: "0",
|
||||
zIndex: "2147483646",
|
||||
pointerEvents,
|
||||
background: "transparent",
|
||||
});
|
||||
blocker.addEventListener("pointerdown", (event) => {
|
||||
if (overlayState.automationBypassCount > 0) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
blocker.addEventListener("click", (event) => {
|
||||
if (overlayState.automationBypassCount > 0) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
blocker.addEventListener(
|
||||
"wheel",
|
||||
(event) => {
|
||||
if (overlayState.automationBypassCount > 0) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
blocker.addEventListener(
|
||||
"touchmove",
|
||||
(event) => {
|
||||
if (overlayState.automationBypassCount > 0) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
|
||||
const pill = document.createElement("div");
|
||||
pill.setAttribute("data-slot", "control-overlay-pill");
|
||||
Object.assign(pill.style, {
|
||||
position: "fixed",
|
||||
bottom: "32px",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: "2147483647",
|
||||
pointerEvents: "auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
borderRadius: "999px",
|
||||
backgroundColor: "#fff",
|
||||
padding: "10px 10px 10px 20px",
|
||||
boxShadow: "0 8px 32px rgba(124,45,18,0.16), 0 2px 8px rgba(0,0,0,0.1)",
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||
});
|
||||
|
||||
const logo = document.createElement("img");
|
||||
logo.src = logoUrl;
|
||||
logo.alt = "browser-skill";
|
||||
Object.assign(logo.style, {
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
borderRadius: "4px",
|
||||
flexShrink: "0",
|
||||
});
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.textContent = i18n.t("controlOverlay.status", { ns: "extension" });
|
||||
Object.assign(label.style, {
|
||||
fontSize: "16px",
|
||||
fontWeight: "500",
|
||||
color: "#333",
|
||||
whiteSpace: "nowrap",
|
||||
userSelect: "none",
|
||||
});
|
||||
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.setAttribute("data-slot", "control-overlay-stop-all");
|
||||
button.disabled = overlayState.interrupting;
|
||||
Object.assign(button.style, {
|
||||
pointerEvents: "auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
border: "none",
|
||||
borderRadius: "999px",
|
||||
padding: "8px 20px 8px 16px",
|
||||
fontSize: "15px",
|
||||
fontWeight: "600",
|
||||
color: "#fff",
|
||||
backgroundColor: overlayState.interrupting ? "#9ca3af" : "#f97316",
|
||||
cursor: overlayState.interrupting ? "default" : "pointer",
|
||||
opacity: overlayState.interrupting ? "0.7" : "1",
|
||||
transition: "background-color 150ms ease-out, opacity 150ms ease-out",
|
||||
whiteSpace: "nowrap",
|
||||
lineHeight: "1",
|
||||
});
|
||||
button.append(
|
||||
createStopCircleIcon(),
|
||||
document.createTextNode(
|
||||
overlayState.interrupting
|
||||
? i18n.t("controlOverlay.interrupting", { ns: "extension" })
|
||||
: i18n.t("controlOverlay.interrupt", { ns: "extension" }),
|
||||
),
|
||||
);
|
||||
button.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleInterrupt();
|
||||
});
|
||||
|
||||
pill.append(logo, label, button);
|
||||
root.append(style, border, blocker, pill);
|
||||
const focusedInShadow = host.shadowRoot?.activeElement;
|
||||
if (focusedInShadow instanceof HTMLElement) {
|
||||
focusedInShadow.blur();
|
||||
}
|
||||
if (document.activeElement === host) {
|
||||
host.blur();
|
||||
}
|
||||
host.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
|
||||
function renderReactOverlays(): void {
|
||||
const overlayState = overlays.snapshot();
|
||||
reactRoot?.render(
|
||||
React.createElement(
|
||||
I18nextProvider,
|
||||
{ i18n },
|
||||
React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
React.createElement(BorrowConfirmationOverlay, {
|
||||
requests: overlayState.borrowRequests,
|
||||
}),
|
||||
React.createElement(HelpRequestOverlay, { request: overlayState.activeHelp }),
|
||||
React.createElement(RecordOverlay, { request: overlayState.activeRecord }),
|
||||
),
|
||||
),
|
||||
const controlOverlayVisible = shouldShowAgentControlOverlay(overlayState);
|
||||
const interactiveOverlayVisible =
|
||||
overlayState.borrowRequests.length > 0 ||
|
||||
overlayState.activeHelp !== null ||
|
||||
overlayState.activeRecord !== null;
|
||||
setOverlayHostHiddenFromAccessibility(!interactiveOverlayVisible);
|
||||
setOverlaySurfaceState(
|
||||
controlOverlayVisible,
|
||||
controlOverlayVisible && overlayState.automationBypassCount === 0,
|
||||
);
|
||||
const root = reactRoot;
|
||||
if (!root) return;
|
||||
flushSync(() => {
|
||||
root.render(
|
||||
React.createElement(
|
||||
I18nextProvider,
|
||||
{ i18n },
|
||||
React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
React.createElement(BorrowConfirmationOverlay, {
|
||||
requests: overlayState.borrowRequests,
|
||||
}),
|
||||
React.createElement(HelpRequestOverlay, { request: overlayState.activeHelp }),
|
||||
React.createElement(RecordOverlay, { request: overlayState.activeRecord }),
|
||||
React.createElement(ControlOverlay, {
|
||||
visible: controlOverlayVisible,
|
||||
interrupting: overlayState.interrupting,
|
||||
automationBypass: overlayState.automationBypassCount > 0,
|
||||
onInterrupt: handleInterrupt,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function renderAll(): void {
|
||||
renderReactOverlays();
|
||||
renderControlOverlay();
|
||||
}
|
||||
|
||||
async function waitForRenderedOverlayUpdate(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user