diff --git a/packages/editor/src/editor/Canvas.tsx b/packages/editor/src/editor/Canvas.tsx index ea3de9f..2cb55bc 100644 --- a/packages/editor/src/editor/Canvas.tsx +++ b/packages/editor/src/editor/Canvas.tsx @@ -40,6 +40,12 @@ function isPrimaryButton(e: React.PointerEvent | PointerEvent): boolean { return (e as PointerEvent).buttons === 1 || (e as PointerEvent).button === 0; } +function selectionKeyOf(ids: string[]): string { + // Keep stable regardless of selection order. + // This avoids false mismatches between context-menu state and reducer state. + return [...ids].sort().join('|'); +} + export function Canvas(props: CanvasProps) { const ref = useRef(null); const [box, setBox] = useState<{ x1: number; y1: number; x2: number; y2: number } | null>(null); @@ -148,7 +154,7 @@ export function Canvas(props: CanvasProps) { ? [...props.selectionIds, targetId] : [targetId] : props.selectionIds; - const selectionKey = nextSelectionIds.join('|'); + const selectionKey = selectionKeyOf(nextSelectionIds); if (targetId) { if (!props.selectionIds.includes(targetId)) { diff --git a/packages/editor/src/editor/EditorApp.tsx b/packages/editor/src/editor/EditorApp.tsx index dfe4226..45ca9c9 100644 --- a/packages/editor/src/editor/EditorApp.tsx +++ b/packages/editor/src/editor/EditorApp.tsx @@ -86,6 +86,11 @@ export function EditorApp() { const closeContextMenu = useCallback(() => setCtxMenu(null), []); + const selectionKeyOf = useCallback((ids: string[]) => { + // Keep stable regardless of ordering. + return [...ids].sort().join('|'); + }, []); + const ctxMenuSyncedRef = useRef(false); useEffect(() => { @@ -99,7 +104,7 @@ export function EditorApp() { useEffect(() => { if (!ctxMenu) return; - const currentKey = state.selection.ids.join('|'); + const currentKey = selectionKeyOf(state.selection.ids); // Wait until reducer state has caught up with the context menu's intended selection. if (!ctxMenuSyncedRef.current) {