From 941f4a1e4cdaa4fdf60beea8901bf2255ddddd14 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sat, 26 Sep 2026 16:33:03 +1000 Subject: [PATCH] fix: auto show and hide editor action bar --- .../canvas/envelope-canvas-types.ts | 6 ----- .../reconcile-envelope-canvas-groups.ts | 10 ++----- .../use-envelope-canvas-contents-layer.ts | 1 - .../use-envelope-canvas-fields-layer.ts | 3 --- .../canvas/use-envelope-canvas-selection.ts | 10 +++---- .../envelope-editor-content-drag-drop.tsx | 12 ++++++++- .../envelope-editor-fields-drag-drop.tsx | 12 ++++++++- .../envelope-editor-fields-page-renderer.tsx | 27 +++++++++++++++++-- .../envelope-canvas-zoom-selection.spec.ts | 5 +--- .../providers/envelope-editor-provider.tsx | 15 +++++++++++ 10 files changed, 68 insertions(+), 33 deletions(-) diff --git a/apps/remix/app/components/general/envelope-editor/canvas/envelope-canvas-types.ts b/apps/remix/app/components/general/envelope-editor/canvas/envelope-canvas-types.ts index 2680ab075..41a61f9de 100644 --- a/apps/remix/app/components/general/envelope-editor/canvas/envelope-canvas-types.ts +++ b/apps/remix/app/components/general/envelope-editor/canvas/envelope-canvas-types.ts @@ -37,12 +37,6 @@ export type EnvelopeCanvasSelectionKind = 'field' | 'content'; export type EnvelopeCanvasSelection = { kind: EnvelopeCanvasSelectionKind; groups: Konva.Group[]; - - /** - * Whether the selection was made programmatically (e.g. on creation) rather - * than by the user. - */ - isAuto: boolean; } | null; /** diff --git a/apps/remix/app/components/general/envelope-editor/canvas/reconcile-envelope-canvas-groups.ts b/apps/remix/app/components/general/envelope-editor/canvas/reconcile-envelope-canvas-groups.ts index e8c51070e..659b36457 100644 --- a/apps/remix/app/components/general/envelope-editor/canvas/reconcile-envelope-canvas-groups.ts +++ b/apps/remix/app/components/general/envelope-editor/canvas/reconcile-envelope-canvas-groups.ts @@ -65,13 +65,8 @@ type SyncEditorSelectionToCanvasOptions = { */ selectedGroups: Konva.Group[]; - select: (kind: EnvelopeCanvasSelectionKind, nodes: Konva.Node[], options?: { isAuto?: boolean }) => void; + select: (kind: EnvelopeCanvasSelectionKind, nodes: Konva.Node[]) => void; clear: () => void; - - /** - * Whether a selection made by this sync counts as automatic. - */ - isAuto: boolean; }; /** @@ -92,7 +87,6 @@ export const syncEditorSelectionToCanvas = ({ selectedGroups, select, clear, - isAuto, }: SyncEditorSelectionToCanvasOptions) => { const isSingleSelection = selectedGroups.length === 1; @@ -106,7 +100,7 @@ export const syncEditorSelectionToCanvas = ({ const groupToSelect = layer.findOne(`#${editorFormId}`); if (groupToSelect instanceof Konva.Group) { - select(kind, [groupToSelect], { isAuto }); + select(kind, [groupToSelect]); } return; diff --git a/apps/remix/app/components/general/envelope-editor/canvas/use-envelope-canvas-contents-layer.ts b/apps/remix/app/components/general/envelope-editor/canvas/use-envelope-canvas-contents-layer.ts index ee7a9b3ba..0fee3affc 100644 --- a/apps/remix/app/components/general/envelope-editor/canvas/use-envelope-canvas-contents-layer.ts +++ b/apps/remix/app/components/general/envelope-editor/canvas/use-envelope-canvas-contents-layer.ts @@ -345,7 +345,6 @@ export const useEnvelopeCanvasContentsLayer = ({ selectedGroups, select: selection.select, clear: selection.clear, - isAuto: false, }); } diff --git a/apps/remix/app/components/general/envelope-editor/canvas/use-envelope-canvas-fields-layer.ts b/apps/remix/app/components/general/envelope-editor/canvas/use-envelope-canvas-fields-layer.ts index 4ff00a028..063e4fd66 100644 --- a/apps/remix/app/components/general/envelope-editor/canvas/use-envelope-canvas-fields-layer.ts +++ b/apps/remix/app/components/general/envelope-editor/canvas/use-envelope-canvas-fields-layer.ts @@ -376,8 +376,6 @@ export const useEnvelopeCanvasFieldsLayer = ({ selection.select('field', liveSelectedGroups); } - // Newly created fields are auto selected, which suppresses the action bar - // so it can't intercept the next placement click. syncEditorSelectionToCanvas({ layer, kind: 'field', @@ -386,7 +384,6 @@ export const useEnvelopeCanvasFieldsLayer = ({ selectedGroups, select: selection.select, clear: selection.clear, - isAuto: true, }); applyPageItemsVisibility(); diff --git a/apps/remix/app/components/general/envelope-editor/canvas/use-envelope-canvas-selection.ts b/apps/remix/app/components/general/envelope-editor/canvas/use-envelope-canvas-selection.ts index 2bb37c351..f61842f98 100644 --- a/apps/remix/app/components/general/envelope-editor/canvas/use-envelope-canvas-selection.ts +++ b/apps/remix/app/components/general/envelope-editor/canvas/use-envelope-canvas-selection.ts @@ -21,10 +21,6 @@ const EMPTY_GROUPS: Konva.Group[] = []; /** How far past a resize handle you can still grab it, in screen pixels. */ const TRANSFORMER_ANCHOR_HIT_STROKE_PX = 24; -type SelectOptions = { - isAuto?: boolean; -}; - type UseEnvelopeCanvasSelectionOptions = { /** * Resolve the transformer configuration for a selection. @@ -106,7 +102,7 @@ export const useEnvelopeCanvasSelection = ({ getTransformerConfig, onChange }: U }, []); const select = useCallback( - (kind: EnvelopeCanvasSelectionKind, nodes: Konva.Node[], options?: SelectOptions) => { + (kind: EnvelopeCanvasSelectionKind, nodes: Konva.Node[]) => { const groupName = ENVELOPE_CANVAS_GROUP_NAMES[kind]; const groups = nodes.filter( @@ -117,7 +113,7 @@ export const useEnvelopeCanvasSelection = ({ getTransformerConfig, onChange }: U Boolean(node.getParent()), ); - applySelection(groups.length > 0 ? { kind, groups, isAuto: Boolean(options?.isAuto) } : null); + applySelection(groups.length > 0 ? { kind, groups } : null); }, [applySelection], ); @@ -169,7 +165,7 @@ export const useEnvelopeCanvasSelection = ({ getTransformerConfig, onChange }: U .find(`.${ENVELOPE_CANVAS_GROUP_NAMES[currentSelection.kind]}`) .filter((group) => selectedIds.has(group.id())); - select(currentSelection.kind, groups, { isAuto: currentSelection.isAuto }); + select(currentSelection.kind, groups); }, [select], ); diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-content-drag-drop.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-content-drag-drop.tsx index cb68b1f35..092d853ee 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-content-drag-drop.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-content-drag-drop.tsx @@ -83,7 +83,7 @@ type EnvelopeEditorContentDragDropProps = { }; export const EnvelopeEditorContentDragDrop = ({ selectedEnvelopeItemId }: EnvelopeEditorContentDragDropProps) => { - const { envelope, editorContents } = useCurrentEnvelopeEditor(); + const { envelope, editorContents, setIsPlacingItem } = useCurrentEnvelopeEditor(); const organisation = useCurrentOrganisation(); @@ -96,6 +96,16 @@ export const EnvelopeEditorContentDragDrop = ({ selectedEnvelopeItemId }: Envelo const [selectedContent, setSelectedContent] = useState(null); + // Let the canvas know a content is being placed, so it can get its selection + // out of the way of the placement click. + useEffect(() => { + setIsPlacingItem(selectedContent !== null); + + return () => { + setIsPlacingItem(false); + }; + }, [selectedContent, setIsPlacingItem]); + const { isWithinPageBounds, getPage } = useDocumentElement(); const [isContentWithinBounds, setIsContentWithinBounds] = useState(false); diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx index b9af073ed..90052c7d5 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx @@ -94,12 +94,22 @@ export const EnvelopeEditorFieldDragDrop = ({ selectedRecipientId, selectedEnvelopeItemId, }: EnvelopeEditorFieldDragDropProps) => { - const { envelope, editorFields, isTemplate, getRecipientColorKey } = useCurrentEnvelopeEditor(); + const { envelope, editorFields, isTemplate, getRecipientColorKey, setIsPlacingItem } = useCurrentEnvelopeEditor(); const { t } = useLingui(); const [selectedField, setSelectedField] = useState(null); + // Let the canvas know a field is being placed, so it can get its selection + // out of the way of the placement click. + useEffect(() => { + setIsPlacingItem(selectedField !== null); + + return () => { + setIsPlacingItem(false); + }; + }, [selectedField, setIsPlacingItem]); + const { isWithinPageBounds, getPage } = useDocumentElement(); const isFieldsDisabled = useMemo(() => { diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx index ca641ee90..e1b96846f 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx @@ -14,6 +14,7 @@ import { useLingui } from '@lingui/react/macro'; import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import { CopyPlusIcon, TrashIcon } from 'lucide-react'; +import { useEffect } from 'react'; import { match } from 'ts-pattern'; import { EnvelopeCanvasActionBar, @@ -54,7 +55,7 @@ const getNodeSelectionKind = (node: Konva.Node): EnvelopeCanvasSelectionKind | n export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageRenderData }) => { const { t } = useLingui(); - const { envelope, editorFields, editorContents, selectedEditorTab } = useCurrentEnvelopeEditor(); + const { envelope, editorFields, editorContents, selectedEditorTab, isPlacingItem } = useCurrentEnvelopeEditor(); const { currentEnvelopeItem } = useCurrentEnvelopeRender(); const organisation = useCurrentOrganisation(); @@ -136,6 +137,28 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR onEmptyClick: selection.clear, }); + /** + * Picking a field or content from the palette drops the selection and any + * pending creation, so nothing floating over the page sits under the + * placement click. + * + * The action bar and pending menus are DOM elements above the canvas, so a + * placement click landing on one would press its button as well as placing + * the item, and by then the button acts on the item which was just placed + * (e.g. removing it). + */ + useEffect(() => { + if (!isPlacingItem) { + return; + } + + fields.clearPending(); + contents.clearPending(); + selection.clear(); + + pageLayer.current?.batchDraw(); + }, [isPlacingItem]); + /** * Initialize the Konva page canvas and all fields, contents and interactions. */ @@ -234,7 +257,7 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR return ( <> {currentSelection?.kind === 'field' && ( -