diff --git a/apps/builder/app/builder/features/inspector/inspector.tsx b/apps/builder/app/builder/features/inspector/inspector.tsx index 49c980d7a3..c39c9284f3 100644 --- a/apps/builder/app/builder/features/inspector/inspector.tsx +++ b/apps/builder/app/builder/features/inspector/inspector.tsx @@ -24,6 +24,7 @@ import { $selectedInstance, $registeredComponentMetas, $dragAndDropState, + $inspectorLastInputTime, } from "~/shared/nano-states"; import { NavigatorTree } from "~/builder/shared/navigator-tree"; import type { Settings } from "~/builder/shared/client-settings"; @@ -70,6 +71,11 @@ const contentStyle = { const $isDragging = computed([$dragAndDropState], (state) => state.isDragging); +const handleInspectorInput = () => { + // Notify canvas of input changes, related to setInert on iframe + $inspectorLastInputTime.set(Date.now()); +}; + export const Inspector = ({ navigatorLayout }: InspectorProps) => { const selectedInstance = useStore($selectedInstance); const tabsRef = useRef(null); @@ -110,57 +116,63 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => { > - - - - {isStyleTabVisible && ( +
+ + + + {isStyleTabVisible && ( + +
+ Style +
+
+ )}
- Style + + Settings +
- )} - + + -
- - Settings - -
-
-
- - - - - - - - - - -
-
+ + + + + + + + + + +
diff --git a/apps/builder/app/canvas/canvas.tsx b/apps/builder/app/canvas/canvas.tsx index 1328eafcbb..d658b6bd55 100644 --- a/apps/builder/app/canvas/canvas.tsx +++ b/apps/builder/app/canvas/canvas.tsx @@ -56,6 +56,7 @@ import { subscribeCommands } from "~/canvas/shared/commands"; import { updateCollaborativeInstanceRect } from "./collaborative-instance"; import { $params } from "./stores"; import { useScrollNewInstanceIntoView } from "./shared/use-scroll-new-instance-into-view"; +import { subscribeInspectorEdits } from "./inspector-edits"; registerContainers(); @@ -129,6 +130,7 @@ const DesignMode = ({ params }: { params: Params }) => { useEffect(updateCollaborativeInstanceRect, []); useEffect(subscribeInstanceSelection, []); useEffect(subscribeInstanceHovering, []); + useEffect(subscribeInspectorEdits, []); return null; }; diff --git a/apps/builder/app/canvas/inspector-edits.ts b/apps/builder/app/canvas/inspector-edits.ts new file mode 100644 index 0000000000..50ab830e0e --- /dev/null +++ b/apps/builder/app/canvas/inspector-edits.ts @@ -0,0 +1,12 @@ +import { $inspectorLastInputTime, $props } from "~/shared/nano-states"; +import { setInert } from "./shared/inert"; + +export const subscribeInspectorEdits = () => { + const unsubscribeInputTime = $inspectorLastInputTime.listen(setInert); + const unsubscribeProps = $props.listen(setInert); + + return () => { + unsubscribeInputTime(); + unsubscribeProps(); + }; +}; diff --git a/apps/builder/app/canvas/shared/inert.ts b/apps/builder/app/canvas/shared/inert.ts new file mode 100644 index 0000000000..23d29930d3 --- /dev/null +++ b/apps/builder/app/canvas/shared/inert.ts @@ -0,0 +1,33 @@ +let resetTimeoutHandle: number | undefined = undefined; + +const resetAutoDisposeInert = () => { + document.body.removeAttribute("inert"); + clearTimeout(resetTimeoutHandle); + resetTimeoutHandle = undefined; +}; + +// 1000 ms is a reasonable time for the preview to reset. +// Anyway should never happen after user has finished preview changes (can happen during preview changes) +const AUTO_DISPOSE_INERT_TIMEOUT = 1000; + +// A brief delay to ensure mutation observers within the focus scope are activated by the preview changes. +const DISPOSE_INERT_TIMEOUT = 300; + +const setAutoDisposeInert = (timeout: number) => { + document.body.setAttribute("inert", "true"); + + // To prevent a completely non-interactive canvas due to edge cases, + // make sure to clean up preview changes if preview styles fail to reset correctly. + clearTimeout(resetTimeoutHandle); + + resetTimeoutHandle = window.setTimeout(resetAutoDisposeInert, timeout); +}; + +/** + * Controls (e.g., radix focus scope) may inadvertently shift focus from inputs. + * Example: When the user modifies styles or content in the settings panel, the use of a mutation observer with Radix causes the focus to shift to the Radix dialog. + * Currently, there's no way to block focus shifts inside iframes (see https:*github.com/w3c/webappsec-permissions-policy/issues/273 for future updates). + * Workaround: use the `inert` attribute on iframe body to prevent focus changes. + */ +export const setInert = () => setAutoDisposeInert(AUTO_DISPOSE_INERT_TIMEOUT); +export const resetInert = () => setAutoDisposeInert(DISPOSE_INERT_TIMEOUT); diff --git a/apps/builder/app/canvas/shared/styles.ts b/apps/builder/app/canvas/shared/styles.ts index 83af6ff562..f76dbe0168 100644 --- a/apps/builder/app/canvas/shared/styles.ts +++ b/apps/builder/app/canvas/shared/styles.ts @@ -34,6 +34,7 @@ import { compareMedia, } from "@webstudio-is/css-engine"; import { $ephemeralStyles } from "../stores"; +import { resetInert, setInert } from "./inert"; const userSheet = createRegularStyleSheet({ name: "user-styles" }); const helpersSheet = createRegularStyleSheet({ name: "helpers" }); @@ -118,40 +119,11 @@ const subscribeEphemeralStyle = (params: Params) => { // track custom properties added on previous ephemeral styles change const addedCustomProperties = new Set(); - let timeoutHandle: number | undefined = undefined; - - const resetInert = () => { - document.body.removeAttribute("inert"); - clearTimeout(timeoutHandle); - timeoutHandle = undefined; - }; - - // 1000 ms is a reasonable time for the preview to reset. - // Anyway should never happen after user has finished preview changes (can happen during preview changes) - const AUTO_DISPOSE_INERT_TIMEOUT = 1000; - - // A brief delay to ensure mutation observers within the focus scope are activated by the preview changes. - const DISPOSE_INERT_TIMEOUT = 300; - - const setAutoDisposeInert = (timeout: number) => { - document.body.setAttribute("inert", "true"); - - // To prevent a completely non-interactive canvas due to edge cases, - // make sure to clean up preview changes if preview styles fail to reset correctly. - clearTimeout(timeoutHandle); - - timeoutHandle = window.setTimeout(resetInert, timeout); - }; - return $ephemeralStyles.subscribe((ephemeralStyles) => { - // Controls (e.g., radix focus scope) may inadvertently shift focus from inputs. - // Currently, there's no way to block focus shifts inside iframes (see https://github.com/w3c/webappsec-permissions-policy/issues/273 for future updates). - // Workaround: use the `inert` attribute on iframe body to prevent focus changes. if (ephemeralStyles.length > 0) { - setAutoDisposeInert(AUTO_DISPOSE_INERT_TIMEOUT); + setInert(); } else { - // Delayed reset same as set with smaller timeout - setAutoDisposeInert(DISPOSE_INERT_TIMEOUT); + resetInert(); } // track custom properties not set on this change diff --git a/apps/builder/app/shared/nano-states/nano-states.ts b/apps/builder/app/shared/nano-states/nano-states.ts index 37227b5796..efd3c06a98 100644 --- a/apps/builder/app/shared/nano-states/nano-states.ts +++ b/apps/builder/app/shared/nano-states/nano-states.ts @@ -343,3 +343,5 @@ export const $dragAndDropState = atom({ }); export const $marketplaceProduct = atom(); + +export const $inspectorLastInputTime = atom(0); diff --git a/apps/builder/app/shared/sync/sync-stores.ts b/apps/builder/app/shared/sync/sync-stores.ts index 2a56749b64..e704d7adf9 100644 --- a/apps/builder/app/shared/sync/sync-stores.ts +++ b/apps/builder/app/shared/sync/sync-stores.ts @@ -38,6 +38,7 @@ import { $resources, $resourceValues, $marketplaceProduct, + $inspectorLastInputTime, } from "~/shared/nano-states"; import { $ephemeralStyles } from "~/canvas/stores"; @@ -118,6 +119,7 @@ export const registerContainers = () => { clientStores.set("dragAndDropState", $dragAndDropState); clientStores.set("ephemeralStyles", $ephemeralStyles); clientStores.set("selectedInstanceStates", $selectedInstanceStates); + clientStores.set("inspectorLastInputTime", $inspectorLastInputTime); for (const [name, store] of $synchronizedBreakpoints) { clientStores.set(name, store);