diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index d8b72b1f2..cffb8e7f4 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -1168,25 +1168,23 @@ export const actionChangeFontFamily = register({ } } - setBatchedData({ - openPopup: "fontFamily", - }); + updateData({ ...batchedData, openPopup: "fontFamily" }); + setBatchedData({}); } else { - // close immediately to avoid racing with other popovers opening + // close: clear openPopup if we're still the active popup const data = { + openPopup: + appState.openPopup === "fontFamily" + ? null + : appState.openPopup, currentHoveredFontFamily: null, cachedElements: new Map(cachedElementsRef.current), resetAll: true, } as ChangeFontFamilyData; - if (isUnmounted.current) { - updateData({ ...batchedData, ...data }); - } else { - // apply immediately instead of batching - updateData({ ...batchedData, ...data }); - setBatchedData({}); - } - + // apply immediately to avoid racing with other popovers opening + updateData({ ...batchedData, ...data }); + setBatchedData({}); cachedElementsRef.current.clear(); } }} diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index 4c78a88dc..aa3382d4d 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -413,6 +413,10 @@ export const CompactShapeActions = ({ return next; }); }} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} > {resizeIcon} @@ -460,8 +464,8 @@ export const CompactShapeActions = ({ { - setAppState({ openPopup: open ? "arrowProperties" : null }); if (open) { + setAppState({ openPopup: "arrowProperties" }); setStrokePopoverOpen(false); setOtherActionsPopoverOpen(false); } @@ -482,6 +486,10 @@ export const CompactShapeActions = ({ setAppState({ openPopup: "arrowProperties" }); } }} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} > {(() => { // Show an icon based on the current arrow type @@ -518,9 +526,11 @@ export const CompactShapeActions = ({ container={container} style={{ maxWidth: "13rem" }} onClose={() => { - if (appState.openPopup === "arrowProperties") { - setAppState({ openPopup: null }); - } + setAppState((prev: AppState) => + prev.openPopup === "arrowProperties" + ? { openPopup: null } + : null, + ); }} > {renderAction("changeArrowProperties")} @@ -551,8 +561,8 @@ export const CompactShapeActions = ({ { - setAppState({ openPopup: open ? "textAlign" : null }); if (open) { + setAppState({ openPopup: "textAlign" }); setStrokePopoverOpen(false); setOtherActionsPopoverOpen(false); } @@ -573,6 +583,10 @@ export const CompactShapeActions = ({ setAppState({ openPopup: "textAlign" }); } }} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} > {TextSizeIcon} @@ -583,9 +597,11 @@ export const CompactShapeActions = ({ container={container} style={{ maxWidth: "13rem" }} onClose={() => { - if (appState.openPopup === "textAlign") { - setAppState({ openPopup: null }); - } + setAppState((prev: AppState) => + prev.openPopup === "textAlign" + ? { openPopup: null } + : null, + ); }} >
@@ -651,6 +667,10 @@ export const CompactShapeActions = ({ return next; }); }} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} > {settingsPlusIcon} diff --git a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx b/packages/excalidraw/components/ColorPicker/ColorPicker.tsx index 3a0ec9c5e..25b497bd9 100644 --- a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx +++ b/packages/excalidraw/components/ColorPicker/ColorPicker.tsx @@ -226,8 +226,14 @@ const ColorPickerTrigger = ({ onPointerDown={(e) => { // use pointerdown so we run before outside-close logic e.preventDefault(); + e.stopPropagation(); onToggle(); }} + onClick={(e) => { + // suppress Radix default toggle to avoid double-toggle flicker + e.preventDefault(); + e.stopPropagation(); + }} >
{!color && slashIcon}
{compactMode && color && ( @@ -313,7 +319,7 @@ export const ColorPicker = ({ onToggle={() => { // atomic switch: if another popup is open, close it first, then open this one next tick if (appState.openPopup === type) { - // toggle off + // toggle off on same trigger updateData({ openPopup: null }); } else if (appState.openPopup) { // switching diff --git a/packages/excalidraw/components/FontPicker/FontPicker.tsx b/packages/excalidraw/components/FontPicker/FontPicker.tsx index d9a13a446..2043a9f29 100644 --- a/packages/excalidraw/components/FontPicker/FontPicker.tsx +++ b/packages/excalidraw/components/FontPicker/FontPicker.tsx @@ -102,16 +102,19 @@ export const FontPicker = React.memo(
)} {!compactMode && } - + {}}> { + onTrigger={(e) => { + // suppress default to avoid double toggle + if (e && e.preventDefault) { + e.preventDefault(); + } + if (isOpened) { onPopupChange(false); } else { - // switch from any open popup: close then open next tick - onPopupChange(false); - setTimeout(() => onPopupChange(true), 0); + onPopupChange(true); } }} /> diff --git a/packages/excalidraw/components/FontPicker/FontPickerList.tsx b/packages/excalidraw/components/FontPicker/FontPickerList.tsx index 6faab2917..33fc3860d 100644 --- a/packages/excalidraw/components/FontPicker/FontPickerList.tsx +++ b/packages/excalidraw/components/FontPicker/FontPickerList.tsx @@ -285,6 +285,7 @@ export const FontPickerList = React.memo( onClose={onClose} onPointerLeave={onLeave} onKeyDown={onKeyDown} + preventAutoFocusOnTouch={true} > {/* Empty div as trigger so it's stretched 100% due to different button sizes */} -
+
{ + e.preventDefault(); + onTrigger?.(e); + }} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} + > { - if (onTrigger) { - e.preventDefault(); - onTrigger(e); - } + e.preventDefault(); + e.stopPropagation(); }} style={{ border: "none", diff --git a/packages/excalidraw/components/PropertiesPopover.tsx b/packages/excalidraw/components/PropertiesPopover.tsx index d8372ea27..d4437b385 100644 --- a/packages/excalidraw/components/PropertiesPopover.tsx +++ b/packages/excalidraw/components/PropertiesPopover.tsx @@ -17,6 +17,7 @@ interface PropertiesPopoverProps { onPointerLeave?: React.PointerEventHandler; onFocusOutside?: Popover.PopoverContentProps["onFocusOutside"]; onPointerDownOutside?: Popover.PopoverContentProps["onPointerDownOutside"]; + preventAutoFocusOnTouch?: boolean; } export const PropertiesPopover = React.forwardRef< @@ -34,6 +35,7 @@ export const PropertiesPopover = React.forwardRef< onFocusOutside, onPointerLeave, onPointerDownOutside, + preventAutoFocusOnTouch = false, }, ref, ) => { @@ -64,6 +66,12 @@ export const PropertiesPopover = React.forwardRef< onKeyDown={onKeyDown} onFocusOutside={onFocusOutside} onPointerDownOutside={onPointerDownOutside} + onOpenAutoFocus={(e) => { + // prevent auto-focus on touch devices to avoid keyboard popup + if (preventAutoFocusOnTouch && device.isTouchScreen) { + e.preventDefault(); + } + }} onCloseAutoFocus={(e) => { e.stopPropagation(); // prevents focusing the trigger