mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-17 06:21:16 +02:00
fix switching between different popovers for texts
This commit is contained in:
@@ -1172,19 +1172,19 @@ export const actionChangeFontFamily = register({
|
|||||||
openPopup: "fontFamily",
|
openPopup: "fontFamily",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// close, use the cache and clear it afterwards
|
// close immediately to avoid racing with other popovers opening
|
||||||
const data = {
|
const data = {
|
||||||
openPopup: null,
|
|
||||||
currentHoveredFontFamily: null,
|
currentHoveredFontFamily: null,
|
||||||
cachedElements: new Map(cachedElementsRef.current),
|
cachedElements: new Map(cachedElementsRef.current),
|
||||||
resetAll: true,
|
resetAll: true,
|
||||||
} as ChangeFontFamilyData;
|
} as ChangeFontFamilyData;
|
||||||
|
|
||||||
if (isUnmounted.current) {
|
if (isUnmounted.current) {
|
||||||
// in case the component was unmounted by the parent, trigger the update directly
|
|
||||||
updateData({ ...batchedData, ...data });
|
updateData({ ...batchedData, ...data });
|
||||||
} else {
|
} else {
|
||||||
setBatchedData(data);
|
// apply immediately instead of batching
|
||||||
|
updateData({ ...batchedData, ...data });
|
||||||
|
setBatchedData({});
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedElementsRef.current.clear();
|
cachedElementsRef.current.clear();
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -353,6 +353,14 @@ export const CompactShapeActions = ({
|
|||||||
|
|
||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
|
|
||||||
|
// Close local popovers any time a global popup (appState.openPopup) opens/switches
|
||||||
|
useEffect(() => {
|
||||||
|
if (appState.openPopup) {
|
||||||
|
setStrokePopoverOpen(false);
|
||||||
|
setOtherActionsPopoverOpen(false);
|
||||||
|
}
|
||||||
|
}, [appState.openPopup]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="compact-shape-actions">
|
<div className="compact-shape-actions">
|
||||||
{/* Stroke Color */}
|
{/* Stroke Color */}
|
||||||
@@ -394,6 +402,17 @@ export const CompactShapeActions = ({
|
|||||||
type="button"
|
type="button"
|
||||||
className="compact-action-button"
|
className="compact-action-button"
|
||||||
title={t("labels.stroke")}
|
title={t("labels.stroke")}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setStrokePopoverOpen((open) => {
|
||||||
|
const next = !open;
|
||||||
|
if (next) {
|
||||||
|
setOtherActionsPopoverOpen(false);
|
||||||
|
setAppState({ openPopup: null });
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{resizeIcon}
|
{resizeIcon}
|
||||||
</button>
|
</button>
|
||||||
@@ -453,6 +472,16 @@ export const CompactShapeActions = ({
|
|||||||
type="button"
|
type="button"
|
||||||
className="compact-action-button"
|
className="compact-action-button"
|
||||||
title={t("labels.arrowtypes")}
|
title={t("labels.arrowtypes")}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (appState.openPopup === "arrowProperties") {
|
||||||
|
setAppState({ openPopup: null });
|
||||||
|
} else {
|
||||||
|
setStrokePopoverOpen(false);
|
||||||
|
setOtherActionsPopoverOpen(false);
|
||||||
|
setAppState({ openPopup: "arrowProperties" });
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
// Show an icon based on the current arrow type
|
// Show an icon based on the current arrow type
|
||||||
@@ -488,7 +517,11 @@ export const CompactShapeActions = ({
|
|||||||
<PropertiesPopover
|
<PropertiesPopover
|
||||||
container={container}
|
container={container}
|
||||||
style={{ maxWidth: "13rem" }}
|
style={{ maxWidth: "13rem" }}
|
||||||
onClose={() => setAppState({ openPopup: null })}
|
onClose={() => {
|
||||||
|
if (appState.openPopup === "arrowProperties") {
|
||||||
|
setAppState({ openPopup: null });
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{renderAction("changeArrowProperties")}
|
{renderAction("changeArrowProperties")}
|
||||||
</PropertiesPopover>
|
</PropertiesPopover>
|
||||||
@@ -530,6 +563,16 @@ export const CompactShapeActions = ({
|
|||||||
type="button"
|
type="button"
|
||||||
className="compact-action-button"
|
className="compact-action-button"
|
||||||
title={t("labels.textAlign")}
|
title={t("labels.textAlign")}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (appState.openPopup === "textAlign") {
|
||||||
|
setAppState({ openPopup: null });
|
||||||
|
} else {
|
||||||
|
setStrokePopoverOpen(false);
|
||||||
|
setOtherActionsPopoverOpen(false);
|
||||||
|
setAppState({ openPopup: "textAlign" });
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{TextSizeIcon}
|
{TextSizeIcon}
|
||||||
</button>
|
</button>
|
||||||
@@ -539,7 +582,11 @@ export const CompactShapeActions = ({
|
|||||||
className={CLASSES.SHAPE_ACTIONS_THEME_SCOPE}
|
className={CLASSES.SHAPE_ACTIONS_THEME_SCOPE}
|
||||||
container={container}
|
container={container}
|
||||||
style={{ maxWidth: "13rem" }}
|
style={{ maxWidth: "13rem" }}
|
||||||
onClose={() => setAppState({ openPopup: null })}
|
onClose={() => {
|
||||||
|
if (appState.openPopup === "textAlign") {
|
||||||
|
setAppState({ openPopup: null });
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="selected-shape-actions">
|
<div className="selected-shape-actions">
|
||||||
{(appState.activeTool.type === "text" ||
|
{(appState.activeTool.type === "text" ||
|
||||||
@@ -593,6 +640,17 @@ export const CompactShapeActions = ({
|
|||||||
type="button"
|
type="button"
|
||||||
className="compact-action-button"
|
className="compact-action-button"
|
||||||
title={t("labels.actions")}
|
title={t("labels.actions")}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setOtherActionsPopoverOpen((open) => {
|
||||||
|
const next = !open;
|
||||||
|
if (next) {
|
||||||
|
setStrokePopoverOpen(false);
|
||||||
|
setAppState({ openPopup: null });
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{settingsPlusIcon}
|
{settingsPlusIcon}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useRef } from "react";
|
import { useRef, useEffect } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
COLOR_OUTLINE_CONTRAST_THRESHOLD,
|
COLOR_OUTLINE_CONTRAST_THRESHOLD,
|
||||||
@@ -78,6 +78,7 @@ const ColorPickerPopupContent = ({
|
|||||||
elements,
|
elements,
|
||||||
palette = COLOR_PALETTE,
|
palette = COLOR_PALETTE,
|
||||||
updateData,
|
updateData,
|
||||||
|
getOpenPopup,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
ColorPickerProps,
|
ColorPickerProps,
|
||||||
| "type"
|
| "type"
|
||||||
@@ -87,7 +88,9 @@ const ColorPickerPopupContent = ({
|
|||||||
| "elements"
|
| "elements"
|
||||||
| "palette"
|
| "palette"
|
||||||
| "updateData"
|
| "updateData"
|
||||||
>) => {
|
> & {
|
||||||
|
getOpenPopup: () => AppState["openPopup"];
|
||||||
|
}) => {
|
||||||
const { container } = useExcalidrawContainer();
|
const { container } = useExcalidrawContainer();
|
||||||
const [, setActiveColorPickerSection] = useAtom(activeColorPickerSectionAtom);
|
const [, setActiveColorPickerSection] = useAtom(activeColorPickerSectionAtom);
|
||||||
|
|
||||||
@@ -132,7 +135,10 @@ const ColorPickerPopupContent = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
updateData({ openPopup: null });
|
// only clear if we're still the active popup (avoid racing with switch)
|
||||||
|
if (getOpenPopup() === type) {
|
||||||
|
updateData({ openPopup: null });
|
||||||
|
}
|
||||||
setActiveColorPickerSection(null);
|
setActiveColorPickerSection(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -169,6 +175,7 @@ const ColorPickerPopupContent = ({
|
|||||||
if (eyeDropperState) {
|
if (eyeDropperState) {
|
||||||
setEyeDropperState(null);
|
setEyeDropperState(null);
|
||||||
} else {
|
} else {
|
||||||
|
// close explicitly on Escape
|
||||||
updateData({ openPopup: null });
|
updateData({ openPopup: null });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -215,7 +222,12 @@ const ColorPickerTrigger = ({
|
|||||||
? t("labels.showStroke")
|
? t("labels.showStroke")
|
||||||
: t("labels.showBackground")
|
: t("labels.showBackground")
|
||||||
}
|
}
|
||||||
onClick={onToggle}
|
data-openpopup={type}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
// use pointerdown so we run before outside-close logic
|
||||||
|
e.preventDefault();
|
||||||
|
onToggle();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="color-picker__button-outline">{!color && slashIcon}</div>
|
<div className="color-picker__button-outline">{!color && slashIcon}</div>
|
||||||
{compactMode && color && (
|
{compactMode && color && (
|
||||||
@@ -261,6 +273,10 @@ export const ColorPicker = ({
|
|||||||
appState,
|
appState,
|
||||||
compactMode = false,
|
compactMode = false,
|
||||||
}: ColorPickerProps) => {
|
}: ColorPickerProps) => {
|
||||||
|
const openRef = useRef(appState.openPopup);
|
||||||
|
useEffect(() => {
|
||||||
|
openRef.current = appState.openPopup;
|
||||||
|
}, [appState.openPopup]);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -284,8 +300,6 @@ export const ColorPicker = ({
|
|||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
updateData({ openPopup: type });
|
updateData({ openPopup: type });
|
||||||
} else if (appState.openPopup === type) {
|
|
||||||
updateData({ openPopup: null });
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -297,10 +311,18 @@ export const ColorPicker = ({
|
|||||||
compactMode={compactMode}
|
compactMode={compactMode}
|
||||||
mode={type === "elementStroke" ? "stroke" : "background"}
|
mode={type === "elementStroke" ? "stroke" : "background"}
|
||||||
onToggle={() => {
|
onToggle={() => {
|
||||||
// toggle to this type (if already open, close; if another is open, switch)
|
// atomic switch: if another popup is open, close it first, then open this one next tick
|
||||||
updateData({
|
if (appState.openPopup === type) {
|
||||||
openPopup: appState.openPopup === type ? null : type,
|
// toggle off
|
||||||
});
|
updateData({ openPopup: null });
|
||||||
|
} else if (appState.openPopup) {
|
||||||
|
// switching
|
||||||
|
updateData({ openPopup: null });
|
||||||
|
setTimeout(() => updateData({ openPopup: type }), 0);
|
||||||
|
} else {
|
||||||
|
// open this one
|
||||||
|
updateData({ openPopup: type });
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* popup content */}
|
{/* popup content */}
|
||||||
@@ -313,6 +335,7 @@ export const ColorPicker = ({
|
|||||||
elements={elements}
|
elements={elements}
|
||||||
palette={palette}
|
palette={palette}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
|
getOpenPopup={() => openRef.current}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
|
@@ -103,7 +103,18 @@ export const FontPicker = React.memo(
|
|||||||
)}
|
)}
|
||||||
{!compactMode && <ButtonSeparator />}
|
{!compactMode && <ButtonSeparator />}
|
||||||
<Popover.Root open={isOpened} onOpenChange={onPopupChange}>
|
<Popover.Root open={isOpened} onOpenChange={onPopupChange}>
|
||||||
<FontPickerTrigger selectedFontFamily={selectedFontFamily} />
|
<FontPickerTrigger
|
||||||
|
selectedFontFamily={selectedFontFamily}
|
||||||
|
onTrigger={() => {
|
||||||
|
if (isOpened) {
|
||||||
|
onPopupChange(false);
|
||||||
|
} else {
|
||||||
|
// switch from any open popup: close then open next tick
|
||||||
|
onPopupChange(false);
|
||||||
|
setTimeout(() => onPopupChange(true), 0);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{isOpened && (
|
{isOpened && (
|
||||||
<FontPickerList
|
<FontPickerList
|
||||||
selectedFontFamily={selectedFontFamily}
|
selectedFontFamily={selectedFontFamily}
|
||||||
|
@@ -11,10 +11,12 @@ import { isDefaultFont } from "./FontPicker";
|
|||||||
|
|
||||||
interface FontPickerTriggerProps {
|
interface FontPickerTriggerProps {
|
||||||
selectedFontFamily: FontFamilyValues | null;
|
selectedFontFamily: FontFamilyValues | null;
|
||||||
|
onTrigger?: (event: React.SyntheticEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FontPickerTrigger = ({
|
export const FontPickerTrigger = ({
|
||||||
selectedFontFamily,
|
selectedFontFamily,
|
||||||
|
onTrigger,
|
||||||
}: FontPickerTriggerProps) => {
|
}: FontPickerTriggerProps) => {
|
||||||
const isTriggerActive = useMemo(
|
const isTriggerActive = useMemo(
|
||||||
() => Boolean(selectedFontFamily && !isDefaultFont(selectedFontFamily)),
|
() => Boolean(selectedFontFamily && !isDefaultFont(selectedFontFamily)),
|
||||||
@@ -24,7 +26,7 @@ export const FontPickerTrigger = ({
|
|||||||
return (
|
return (
|
||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild>
|
||||||
{/* Empty div as trigger so it's stretched 100% due to different button sizes */}
|
{/* Empty div as trigger so it's stretched 100% due to different button sizes */}
|
||||||
<div>
|
<div data-openpopup="fontFamily">
|
||||||
<ButtonIcon
|
<ButtonIcon
|
||||||
standalone
|
standalone
|
||||||
icon={TextIcon}
|
icon={TextIcon}
|
||||||
@@ -32,8 +34,12 @@ export const FontPickerTrigger = ({
|
|||||||
className="properties-trigger"
|
className="properties-trigger"
|
||||||
testId={"font-family-show-fonts"}
|
testId={"font-family-show-fonts"}
|
||||||
active={isTriggerActive}
|
active={isTriggerActive}
|
||||||
// no-op
|
onClick={(e) => {
|
||||||
onClick={() => {}}
|
if (onTrigger) {
|
||||||
|
e.preventDefault();
|
||||||
|
onTrigger(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
border: "none",
|
border: "none",
|
||||||
}}
|
}}
|
||||||
|
Reference in New Issue
Block a user