mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-30 16:47:54 +09:00
fix: HTML Embed Code editor inside Dialog Content looses focus (#3020)
## Description closes #2998 Also fixes name edits etc. The same issue with radix as we had in the past, focus lock on any mutation inside dialog aquires focus. And we have no good way to prevent programmatic focus inside iframe. ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 5de6) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env.example` and the `builder/env-check.js` if mandatory
This commit is contained in:
1 parent
ddb7e068ab
commit
3c189aff97
7 files changed
+109
-74
No files matched your search
@@ -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<HTMLDivElement>(null);
|
||||
@@ -110,57 +116,63 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => {
|
||||
>
|
||||
<FloatingPanelProvider container={tabsRef}>
|
||||
<BindingPopoverProvider value={{ containerRef: tabsRef }}>
|
||||
<PanelTabs
|
||||
ref={tabsRef}
|
||||
value={availableTabs.includes(tab) ? tab : availableTabs[0]}
|
||||
onValueChange={setTab}
|
||||
asChild
|
||||
>
|
||||
<Flex direction="column">
|
||||
<PanelTabsList>
|
||||
{isStyleTabVisible && (
|
||||
<div style={{ display: "contents" }} onInput={handleInspectorInput}>
|
||||
<PanelTabs
|
||||
ref={tabsRef}
|
||||
value={availableTabs.includes(tab) ? tab : availableTabs[0]}
|
||||
onValueChange={setTab}
|
||||
asChild
|
||||
>
|
||||
<Flex direction="column">
|
||||
<PanelTabsList>
|
||||
{isStyleTabVisible && (
|
||||
<Tooltip
|
||||
variant="wrapped"
|
||||
content="The Style panel allows manipulation of CSS visually."
|
||||
>
|
||||
<div>
|
||||
<PanelTabsTrigger value="style">Style</PanelTabsTrigger>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
variant="wrapped"
|
||||
content="The Style panel allows manipulation of CSS visually."
|
||||
content="The Settings panel allows for customizing component properties and HTML attributes."
|
||||
>
|
||||
<div>
|
||||
<PanelTabsTrigger value="style">Style</PanelTabsTrigger>
|
||||
<PanelTabsTrigger value="settings">
|
||||
Settings
|
||||
</PanelTabsTrigger>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
variant="wrapped"
|
||||
content="The Settings panel allows for customizing component properties and HTML attributes."
|
||||
</PanelTabsList>
|
||||
<Separator />
|
||||
<PanelTabsContent
|
||||
value="style"
|
||||
css={contentStyle}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div>
|
||||
<PanelTabsTrigger value="settings">
|
||||
Settings
|
||||
</PanelTabsTrigger>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</PanelTabsList>
|
||||
<Separator />
|
||||
<PanelTabsContent value="style" css={contentStyle} tabIndex={-1}>
|
||||
<InstanceInfo instance={selectedInstance} />
|
||||
<StylePanel selectedInstance={selectedInstance} />
|
||||
</PanelTabsContent>
|
||||
<PanelTabsContent
|
||||
value="settings"
|
||||
css={contentStyle}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ScrollArea>
|
||||
<InstanceInfo instance={selectedInstance} />
|
||||
<SettingsPanelContainer
|
||||
key={
|
||||
selectedInstance.id /* Re-render when instance changes */
|
||||
}
|
||||
selectedInstance={selectedInstance}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</PanelTabsContent>
|
||||
</Flex>
|
||||
</PanelTabs>
|
||||
<StylePanel selectedInstance={selectedInstance} />
|
||||
</PanelTabsContent>
|
||||
<PanelTabsContent
|
||||
value="settings"
|
||||
css={contentStyle}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ScrollArea>
|
||||
<InstanceInfo instance={selectedInstance} />
|
||||
<SettingsPanelContainer
|
||||
key={
|
||||
selectedInstance.id /* Re-render when instance changes */
|
||||
}
|
||||
selectedInstance={selectedInstance}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</PanelTabsContent>
|
||||
</Flex>
|
||||
</PanelTabs>
|
||||
</div>
|
||||
</BindingPopoverProvider>
|
||||
</FloatingPanelProvider>
|
||||
</EnhancedTooltipProvider>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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<string>();
|
||||
|
||||
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
|
||||
|
||||
@@ -343,3 +343,5 @@ export const $dragAndDropState = atom<DragAndDropState>({
|
||||
});
|
||||
|
||||
export const $marketplaceProduct = atom<undefined | MarketplaceProduct>();
|
||||
|
||||
export const $inspectorLastInputTime = atom<number>(0);
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user