fix: Actions panel ux improvement (#6850)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
afea0df141
commit
71ad3c5356
167
src/actions/actionProperties.test.tsx
Normal file
167
src/actions/actionProperties.test.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import { Excalidraw } from "../packages/excalidraw/index";
|
||||||
|
import { queryByTestId } from "@testing-library/react";
|
||||||
|
import { render } from "../tests/test-utils";
|
||||||
|
import { UI } from "../tests/helpers/ui";
|
||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS } from "../colors";
|
||||||
|
import { FONT_FAMILY, STROKE_WIDTH } from "../constants";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
describe("element locking", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("properties when tool selected", () => {
|
||||||
|
it("should show active background top picks", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
|
||||||
|
const color = DEFAULT_ELEMENT_BACKGROUND_PICKS[1];
|
||||||
|
|
||||||
|
// just in case we change it in the future
|
||||||
|
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
||||||
|
|
||||||
|
h.setState({
|
||||||
|
currentItemBackgroundColor: color,
|
||||||
|
});
|
||||||
|
const activeColor = queryByTestId(
|
||||||
|
document.body,
|
||||||
|
`color-top-pick-${color}`,
|
||||||
|
);
|
||||||
|
expect(activeColor).toHaveClass("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show fill style when background non-transparent", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
|
||||||
|
const color = DEFAULT_ELEMENT_BACKGROUND_PICKS[1];
|
||||||
|
|
||||||
|
// just in case we change it in the future
|
||||||
|
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
||||||
|
|
||||||
|
h.setState({
|
||||||
|
currentItemBackgroundColor: color,
|
||||||
|
currentItemFillStyle: "hachure",
|
||||||
|
});
|
||||||
|
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
|
||||||
|
|
||||||
|
expect(hachureFillButton).toHaveClass("active");
|
||||||
|
h.setState({
|
||||||
|
currentItemFillStyle: "solid",
|
||||||
|
});
|
||||||
|
const solidFillStyle = queryByTestId(document.body, `fill-solid`);
|
||||||
|
expect(solidFillStyle).toHaveClass("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show fill style when background transparent", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
|
||||||
|
h.setState({
|
||||||
|
currentItemBackgroundColor: COLOR_PALETTE.transparent,
|
||||||
|
currentItemFillStyle: "hachure",
|
||||||
|
});
|
||||||
|
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
|
||||||
|
|
||||||
|
expect(hachureFillButton).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show horizontal text align for text tool", () => {
|
||||||
|
UI.clickTool("text");
|
||||||
|
|
||||||
|
h.setState({
|
||||||
|
currentItemTextAlign: "right",
|
||||||
|
});
|
||||||
|
|
||||||
|
const centerTextAlign = queryByTestId(document.body, `align-right`);
|
||||||
|
expect(centerTextAlign).toBeChecked();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("properties when elements selected", () => {
|
||||||
|
it("should show active styles when single element selected", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
backgroundColor: "red",
|
||||||
|
fillStyle: "cross-hatch",
|
||||||
|
});
|
||||||
|
h.elements = [rect];
|
||||||
|
API.setSelectedElements([rect]);
|
||||||
|
|
||||||
|
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
||||||
|
expect(crossHatchButton).toHaveClass("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show fill style selected element's background is transparent", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
backgroundColor: COLOR_PALETTE.transparent,
|
||||||
|
fillStyle: "cross-hatch",
|
||||||
|
});
|
||||||
|
h.elements = [rect];
|
||||||
|
API.setSelectedElements([rect]);
|
||||||
|
|
||||||
|
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
||||||
|
expect(crossHatchButton).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should highlight common stroke width of selected elements", () => {
|
||||||
|
const rect1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.thin,
|
||||||
|
});
|
||||||
|
const rect2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.thin,
|
||||||
|
});
|
||||||
|
h.elements = [rect1, rect2];
|
||||||
|
API.setSelectedElements([rect1, rect2]);
|
||||||
|
|
||||||
|
const thinStrokeWidthButton = queryByTestId(
|
||||||
|
document.body,
|
||||||
|
`strokeWidth-thin`,
|
||||||
|
);
|
||||||
|
expect(thinStrokeWidthButton).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not highlight any stroke width button if no common style", () => {
|
||||||
|
const rect1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.thin,
|
||||||
|
});
|
||||||
|
const rect2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.bold,
|
||||||
|
});
|
||||||
|
h.elements = [rect1, rect2];
|
||||||
|
API.setSelectedElements([rect1, rect2]);
|
||||||
|
|
||||||
|
expect(queryByTestId(document.body, `strokeWidth-thin`)).not.toBe(null);
|
||||||
|
expect(
|
||||||
|
queryByTestId(document.body, `strokeWidth-thin`),
|
||||||
|
).not.toBeChecked();
|
||||||
|
expect(
|
||||||
|
queryByTestId(document.body, `strokeWidth-bold`),
|
||||||
|
).not.toBeChecked();
|
||||||
|
expect(
|
||||||
|
queryByTestId(document.body, `strokeWidth-extraBold`),
|
||||||
|
).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show properties of different element types when selected", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.bold,
|
||||||
|
});
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
fontFamily: FONT_FAMILY.Cascadia,
|
||||||
|
});
|
||||||
|
h.elements = [rect, text];
|
||||||
|
API.setSelectedElements([rect, text]);
|
||||||
|
|
||||||
|
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
|
||||||
|
expect(queryByTestId(document.body, `font-family-code`)).toBeChecked();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,4 +1,4 @@
|
|||||||
import { AppState } from "../../src/types";
|
import { AppState, Primitive } from "../../src/types";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||||
@ -51,6 +51,7 @@ import {
|
|||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
|
STROKE_WIDTH,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import {
|
import {
|
||||||
@ -82,7 +83,6 @@ import { getLanguage, t } from "../i18n";
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../random";
|
||||||
import {
|
import {
|
||||||
canChangeRoundness,
|
|
||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
getCommonAttributeOfSelectedElements,
|
getCommonAttributeOfSelectedElements,
|
||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
@ -118,25 +118,44 @@ export const changeProperty = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFormValue = function <T>(
|
export const getFormValue = function <T extends Primitive>(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
getAttribute: (element: ExcalidrawElement) => T,
|
getAttribute: (element: ExcalidrawElement) => T,
|
||||||
defaultValue: T,
|
isRelevantElement: true | ((element: ExcalidrawElement) => boolean),
|
||||||
|
defaultValue: T | ((isSomeElementSelected: boolean) => T),
|
||||||
): T {
|
): T {
|
||||||
const editingElement = appState.editingElement;
|
const editingElement = appState.editingElement;
|
||||||
const nonDeletedElements = getNonDeletedElements(elements);
|
const nonDeletedElements = getNonDeletedElements(elements);
|
||||||
return (
|
|
||||||
(editingElement && getAttribute(editingElement)) ??
|
let ret: T | null = null;
|
||||||
(isSomeElementSelected(nonDeletedElements, appState)
|
|
||||||
? getCommonAttributeOfSelectedElements(
|
if (editingElement) {
|
||||||
nonDeletedElements,
|
ret = getAttribute(editingElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ret) {
|
||||||
|
const hasSelection = isSomeElementSelected(nonDeletedElements, appState);
|
||||||
|
|
||||||
|
if (hasSelection) {
|
||||||
|
ret =
|
||||||
|
getCommonAttributeOfSelectedElements(
|
||||||
|
isRelevantElement === true
|
||||||
|
? nonDeletedElements
|
||||||
|
: nonDeletedElements.filter((el) => isRelevantElement(el)),
|
||||||
appState,
|
appState,
|
||||||
getAttribute,
|
getAttribute,
|
||||||
)
|
) ??
|
||||||
: defaultValue) ??
|
(typeof defaultValue === "function"
|
||||||
defaultValue
|
? defaultValue(true)
|
||||||
);
|
: defaultValue);
|
||||||
|
} else {
|
||||||
|
ret =
|
||||||
|
typeof defaultValue === "function" ? defaultValue(false) : defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
const offsetElementAfterFontResize = (
|
const offsetElementAfterFontResize = (
|
||||||
@ -247,6 +266,7 @@ export const actionChangeStrokeColor = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeColor,
|
(element) => element.strokeColor,
|
||||||
|
true,
|
||||||
appState.currentItemStrokeColor,
|
appState.currentItemStrokeColor,
|
||||||
)}
|
)}
|
||||||
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
||||||
@ -289,6 +309,7 @@ export const actionChangeBackgroundColor = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.backgroundColor,
|
(element) => element.backgroundColor,
|
||||||
|
true,
|
||||||
appState.currentItemBackgroundColor,
|
appState.currentItemBackgroundColor,
|
||||||
)}
|
)}
|
||||||
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
||||||
@ -338,23 +359,28 @@ export const actionChangeFillStyle = register({
|
|||||||
} (${getShortcutKey("Alt-Click")})`,
|
} (${getShortcutKey("Alt-Click")})`,
|
||||||
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
||||||
active: allElementsZigZag ? true : undefined,
|
active: allElementsZigZag ? true : undefined,
|
||||||
|
testId: `fill-hachure`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "cross-hatch",
|
value: "cross-hatch",
|
||||||
text: t("labels.crossHatch"),
|
text: t("labels.crossHatch"),
|
||||||
icon: FillCrossHatchIcon,
|
icon: FillCrossHatchIcon,
|
||||||
|
testId: `fill-cross-hatch`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "solid",
|
value: "solid",
|
||||||
text: t("labels.solid"),
|
text: t("labels.solid"),
|
||||||
icon: FillSolidIcon,
|
icon: FillSolidIcon,
|
||||||
|
testId: `fill-solid`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.fillStyle,
|
(element) => element.fillStyle,
|
||||||
appState.currentItemFillStyle,
|
(element) => element.hasOwnProperty("fillStyle"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemFillStyle,
|
||||||
)}
|
)}
|
||||||
onClick={(value, event) => {
|
onClick={(value, event) => {
|
||||||
const nextValue =
|
const nextValue =
|
||||||
@ -393,26 +419,31 @@ export const actionChangeStrokeWidth = register({
|
|||||||
group="stroke-width"
|
group="stroke-width"
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: 1,
|
value: STROKE_WIDTH.thin,
|
||||||
text: t("labels.thin"),
|
text: t("labels.thin"),
|
||||||
icon: StrokeWidthBaseIcon,
|
icon: StrokeWidthBaseIcon,
|
||||||
|
testId: "strokeWidth-thin",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 2,
|
value: STROKE_WIDTH.bold,
|
||||||
text: t("labels.bold"),
|
text: t("labels.bold"),
|
||||||
icon: StrokeWidthBoldIcon,
|
icon: StrokeWidthBoldIcon,
|
||||||
|
testId: "strokeWidth-bold",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 4,
|
value: STROKE_WIDTH.extraBold,
|
||||||
text: t("labels.extraBold"),
|
text: t("labels.extraBold"),
|
||||||
icon: StrokeWidthExtraBoldIcon,
|
icon: StrokeWidthExtraBoldIcon,
|
||||||
|
testId: "strokeWidth-extraBold",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeWidth,
|
(element) => element.strokeWidth,
|
||||||
appState.currentItemStrokeWidth,
|
(element) => element.hasOwnProperty("strokeWidth"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemStrokeWidth,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@ -461,7 +492,9 @@ export const actionChangeSloppiness = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.roughness,
|
(element) => element.roughness,
|
||||||
appState.currentItemRoughness,
|
(element) => element.hasOwnProperty("roughness"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemRoughness,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@ -509,7 +542,9 @@ export const actionChangeStrokeStyle = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeStyle,
|
(element) => element.strokeStyle,
|
||||||
appState.currentItemStrokeStyle,
|
(element) => element.hasOwnProperty("strokeStyle"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemStrokeStyle,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@ -549,6 +584,7 @@ export const actionChangeOpacity = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.opacity,
|
(element) => element.opacity,
|
||||||
|
true,
|
||||||
appState.currentItemOpacity,
|
appState.currentItemOpacity,
|
||||||
) ?? undefined
|
) ?? undefined
|
||||||
}
|
}
|
||||||
@ -607,7 +643,12 @@ export const actionChangeFontSize = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection
|
||||||
|
? null
|
||||||
|
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@ -692,21 +733,25 @@ export const actionChangeFontFamily = register({
|
|||||||
value: FontFamilyValues;
|
value: FontFamilyValues;
|
||||||
text: string;
|
text: string;
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
|
testId: string;
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Virgil,
|
value: FONT_FAMILY.Virgil,
|
||||||
text: t("labels.handDrawn"),
|
text: t("labels.handDrawn"),
|
||||||
icon: FreedrawIcon,
|
icon: FreedrawIcon,
|
||||||
|
testId: "font-family-virgil",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Helvetica,
|
value: FONT_FAMILY.Helvetica,
|
||||||
text: t("labels.normal"),
|
text: t("labels.normal"),
|
||||||
icon: FontFamilyNormalIcon,
|
icon: FontFamilyNormalIcon,
|
||||||
|
testId: "font-family-normal",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Cascadia,
|
value: FONT_FAMILY.Cascadia,
|
||||||
text: t("labels.code"),
|
text: t("labels.code"),
|
||||||
icon: FontFamilyCodeIcon,
|
icon: FontFamilyCodeIcon,
|
||||||
|
testId: "font-family-code",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -729,7 +774,12 @@ export const actionChangeFontFamily = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection
|
||||||
|
? null
|
||||||
|
: appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@ -806,7 +856,10 @@ export const actionChangeTextAlign = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
appState.currentItemTextAlign,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemTextAlign,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@ -882,7 +935,9 @@ export const actionChangeVerticalAlign = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
VERTICAL_ALIGN.MIDDLE,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@ -947,9 +1002,9 @@ export const actionChangeRoundness = register({
|
|||||||
appState,
|
appState,
|
||||||
(element) =>
|
(element) =>
|
||||||
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
||||||
(canChangeRoundness(appState.activeTool.type) &&
|
(element) => element.hasOwnProperty("roundness"),
|
||||||
appState.currentItemRoundness) ||
|
(hasSelection) =>
|
||||||
null,
|
hasSelection ? null : appState.currentItemRoundness,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@ -1043,6 +1098,7 @@ export const actionChangeArrowhead = register({
|
|||||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||||
? element.startArrowhead
|
? element.startArrowhead
|
||||||
: appState.currentItemStartArrowhead,
|
: appState.currentItemStartArrowhead,
|
||||||
|
true,
|
||||||
appState.currentItemStartArrowhead,
|
appState.currentItemStartArrowhead,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData({ position: "start", type: value })}
|
onChange={(value) => updateData({ position: "start", type: value })}
|
||||||
@ -1089,6 +1145,7 @@ export const actionChangeArrowhead = register({
|
|||||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||||
? element.endArrowhead
|
? element.endArrowhead
|
||||||
: appState.currentItemEndArrowhead,
|
: appState.currentItemEndArrowhead,
|
||||||
|
true,
|
||||||
appState.currentItemEndArrowhead,
|
appState.currentItemEndArrowhead,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData({ position: "end", type: value })}
|
onChange={(value) => updateData({ position: "end", type: value })}
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
hasBackground,
|
hasBackground,
|
||||||
hasStrokeStyle,
|
hasStrokeStyle,
|
||||||
hasStrokeWidth,
|
hasStrokeWidth,
|
||||||
hasText,
|
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { SHAPES } from "../shapes";
|
import { SHAPES } from "../shapes";
|
||||||
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
||||||
@ -20,7 +19,7 @@ import Stack from "./Stack";
|
|||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { hasBoundTextElement } from "../element/typeChecks";
|
import { hasBoundTextElement, isTextElement } from "../element/typeChecks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { actionToggleZenMode } from "../actions";
|
import { actionToggleZenMode } from "../actions";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
@ -66,7 +65,8 @@ export const SelectedShapeActions = ({
|
|||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
|
|
||||||
const showFillIcons =
|
const showFillIcons =
|
||||||
hasBackground(appState.activeTool.type) ||
|
(hasBackground(appState.activeTool.type) &&
|
||||||
|
!isTransparent(appState.currentItemBackgroundColor)) ||
|
||||||
targetElements.some(
|
targetElements.some(
|
||||||
(element) =>
|
(element) =>
|
||||||
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
||||||
@ -123,14 +123,15 @@ export const SelectedShapeActions = ({
|
|||||||
<>{renderAction("changeRoundness")}</>
|
<>{renderAction("changeRoundness")}</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(hasText(appState.activeTool.type) ||
|
{(appState.activeTool.type === "text" ||
|
||||||
targetElements.some((element) => hasText(element.type))) && (
|
targetElements.some(isTextElement)) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeFontSize")}
|
{renderAction("changeFontSize")}
|
||||||
|
|
||||||
{renderAction("changeFontFamily")}
|
{renderAction("changeFontFamily")}
|
||||||
|
|
||||||
{suppportsHorizontalAlign(targetElements) &&
|
{(appState.activeTool.type === "text" ||
|
||||||
|
suppportsHorizontalAlign(targetElements)) &&
|
||||||
renderAction("changeTextAlign")}
|
renderAction("changeTextAlign")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -55,6 +55,7 @@ export const TopPicks = ({
|
|||||||
type="button"
|
type="button"
|
||||||
title={color}
|
title={color}
|
||||||
onClick={() => onChange(color)}
|
onClick={() => onChange(color)}
|
||||||
|
data-testid={`color-top-pick-${color}`}
|
||||||
>
|
>
|
||||||
<div className="color-picker__button-outline" />
|
<div className="color-picker__button-outline" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -302,6 +302,12 @@ export const ROUGHNESS = {
|
|||||||
cartoonist: 2,
|
cartoonist: 2,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const STROKE_WIDTH = {
|
||||||
|
thin: 1,
|
||||||
|
bold: 2,
|
||||||
|
extraBold: 4,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const DEFAULT_ELEMENT_PROPS: {
|
export const DEFAULT_ELEMENT_PROPS: {
|
||||||
strokeColor: ExcalidrawElement["strokeColor"];
|
strokeColor: ExcalidrawElement["strokeColor"];
|
||||||
backgroundColor: ExcalidrawElement["backgroundColor"];
|
backgroundColor: ExcalidrawElement["backgroundColor"];
|
||||||
|
@ -39,8 +39,6 @@ export const canChangeRoundness = (type: string) =>
|
|||||||
type === "line" ||
|
type === "line" ||
|
||||||
type === "diamond";
|
type === "diamond";
|
||||||
|
|
||||||
export const hasText = (type: string) => type === "text";
|
|
||||||
|
|
||||||
export const canHaveArrowheads = (type: string) => type === "arrow";
|
export const canHaveArrowheads = (type: string) => type === "arrow";
|
||||||
|
|
||||||
export const getElementAtPosition = (
|
export const getElementAtPosition = (
|
||||||
|
@ -14,7 +14,6 @@ export {
|
|||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
canChangeRoundness,
|
canChangeRoundness,
|
||||||
getElementAtPosition,
|
getElementAtPosition,
|
||||||
hasText,
|
|
||||||
getElementsAtPosition,
|
getElementsAtPosition,
|
||||||
} from "./comparisons";
|
} from "./comparisons";
|
||||||
export { getNormalizedZoom } from "./zoom";
|
export { getNormalizedZoom } from "./zoom";
|
||||||
|
@ -14255,217 +14255,6 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||||||
|
|
||||||
exports[`regression tests > should group elements and ungroup them > [end of test] number of renders 1`] = `21`;
|
exports[`regression tests > should group elements and ungroup them > [end of test] number of renders 1`] = `21`;
|
||||||
|
|
||||||
exports[`regression tests > should show fill icons when element has non transparent background > [end of test] appState 1`] = `
|
|
||||||
{
|
|
||||||
"activeEmbeddable": null,
|
|
||||||
"activeTool": {
|
|
||||||
"customType": null,
|
|
||||||
"lastActiveTool": null,
|
|
||||||
"locked": false,
|
|
||||||
"type": "selection",
|
|
||||||
},
|
|
||||||
"collaborators": Map {},
|
|
||||||
"contextMenu": null,
|
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentItemBackgroundColor": "#ffc9c9",
|
|
||||||
"currentItemEndArrowhead": "arrow",
|
|
||||||
"currentItemFillStyle": "solid",
|
|
||||||
"currentItemFontFamily": 1,
|
|
||||||
"currentItemFontSize": 20,
|
|
||||||
"currentItemOpacity": 100,
|
|
||||||
"currentItemRoughness": 1,
|
|
||||||
"currentItemRoundness": "round",
|
|
||||||
"currentItemStartArrowhead": null,
|
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
|
||||||
"currentItemStrokeStyle": "solid",
|
|
||||||
"currentItemStrokeWidth": 2,
|
|
||||||
"currentItemTextAlign": "left",
|
|
||||||
"cursorButton": "up",
|
|
||||||
"defaultSidebarDockedPreference": false,
|
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
|
||||||
"editingFrame": null,
|
|
||||||
"editingGroupId": null,
|
|
||||||
"editingLinearElement": null,
|
|
||||||
"elementsToHighlight": null,
|
|
||||||
"errorMessage": null,
|
|
||||||
"exportBackground": true,
|
|
||||||
"exportEmbedScene": false,
|
|
||||||
"exportScale": 1,
|
|
||||||
"exportWithDarkMode": false,
|
|
||||||
"fileHandle": null,
|
|
||||||
"frameRendering": {
|
|
||||||
"clip": true,
|
|
||||||
"enabled": true,
|
|
||||||
"name": true,
|
|
||||||
"outline": true,
|
|
||||||
},
|
|
||||||
"frameToHighlight": null,
|
|
||||||
"gridSize": null,
|
|
||||||
"height": 768,
|
|
||||||
"isBindingEnabled": true,
|
|
||||||
"isLoading": false,
|
|
||||||
"isResizing": false,
|
|
||||||
"isRotating": false,
|
|
||||||
"lastPointerDownWith": "mouse",
|
|
||||||
"multiElement": null,
|
|
||||||
"name": "Untitled-201933152653",
|
|
||||||
"objectsSnapModeEnabled": false,
|
|
||||||
"offsetLeft": 0,
|
|
||||||
"offsetTop": 0,
|
|
||||||
"openDialog": null,
|
|
||||||
"openMenu": null,
|
|
||||||
"openPopup": "elementBackground",
|
|
||||||
"openSidebar": null,
|
|
||||||
"originSnapOffset": null,
|
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
|
||||||
"penMode": false,
|
|
||||||
"pendingImageElementId": null,
|
|
||||||
"previousSelectedElementIds": {},
|
|
||||||
"resizingElement": null,
|
|
||||||
"scrollX": 0,
|
|
||||||
"scrollY": 0,
|
|
||||||
"scrolledOutside": false,
|
|
||||||
"selectedElementIds": {
|
|
||||||
"id0": true,
|
|
||||||
},
|
|
||||||
"selectedElementsAreBeingDragged": false,
|
|
||||||
"selectedGroupIds": {},
|
|
||||||
"selectedLinearElement": null,
|
|
||||||
"selectionElement": null,
|
|
||||||
"shouldCacheIgnoreZoom": false,
|
|
||||||
"showHyperlinkPopup": false,
|
|
||||||
"showStats": false,
|
|
||||||
"showWelcomeScreen": true,
|
|
||||||
"snapLines": [],
|
|
||||||
"startBoundElement": null,
|
|
||||||
"suggestedBindings": [],
|
|
||||||
"theme": "light",
|
|
||||||
"toast": null,
|
|
||||||
"viewBackgroundColor": "#ffffff",
|
|
||||||
"viewModeEnabled": false,
|
|
||||||
"width": 1024,
|
|
||||||
"zenModeEnabled": false,
|
|
||||||
"zoom": {
|
|
||||||
"value": 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`regression tests > should show fill icons when element has non transparent background > [end of test] history 1`] = `
|
|
||||||
{
|
|
||||||
"recording": false,
|
|
||||||
"redoStack": [],
|
|
||||||
"stateHistory": [
|
|
||||||
{
|
|
||||||
"appState": {
|
|
||||||
"editingGroupId": null,
|
|
||||||
"editingLinearElement": null,
|
|
||||||
"name": "Untitled-201933152653",
|
|
||||||
"selectedElementIds": {},
|
|
||||||
"selectedGroupIds": {},
|
|
||||||
"viewBackgroundColor": "#ffffff",
|
|
||||||
},
|
|
||||||
"elements": [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appState": {
|
|
||||||
"editingGroupId": null,
|
|
||||||
"editingLinearElement": null,
|
|
||||||
"name": "Untitled-201933152653",
|
|
||||||
"selectedElementIds": {
|
|
||||||
"id0": true,
|
|
||||||
},
|
|
||||||
"selectedGroupIds": {},
|
|
||||||
"viewBackgroundColor": "#ffffff",
|
|
||||||
},
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"angle": 0,
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"boundElements": null,
|
|
||||||
"fillStyle": "solid",
|
|
||||||
"frameId": null,
|
|
||||||
"groupIds": [],
|
|
||||||
"height": 10,
|
|
||||||
"id": "id0",
|
|
||||||
"isDeleted": false,
|
|
||||||
"link": null,
|
|
||||||
"locked": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"roughness": 1,
|
|
||||||
"roundness": {
|
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
|
||||||
"strokeColor": "#1e1e1e",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 2,
|
|
||||||
"type": "rectangle",
|
|
||||||
"updated": 1,
|
|
||||||
"version": 2,
|
|
||||||
"versionNonce": 453191,
|
|
||||||
"width": 10,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appState": {
|
|
||||||
"editingGroupId": null,
|
|
||||||
"editingLinearElement": null,
|
|
||||||
"name": "Untitled-201933152653",
|
|
||||||
"selectedElementIds": {
|
|
||||||
"id0": true,
|
|
||||||
},
|
|
||||||
"selectedGroupIds": {},
|
|
||||||
"viewBackgroundColor": "#ffffff",
|
|
||||||
},
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"angle": 0,
|
|
||||||
"backgroundColor": "#ffc9c9",
|
|
||||||
"boundElements": null,
|
|
||||||
"fillStyle": "solid",
|
|
||||||
"frameId": null,
|
|
||||||
"groupIds": [],
|
|
||||||
"height": 10,
|
|
||||||
"id": "id0",
|
|
||||||
"isDeleted": false,
|
|
||||||
"link": null,
|
|
||||||
"locked": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"roughness": 1,
|
|
||||||
"roundness": {
|
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
|
||||||
"strokeColor": "#1e1e1e",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 2,
|
|
||||||
"type": "rectangle",
|
|
||||||
"updated": 1,
|
|
||||||
"version": 3,
|
|
||||||
"versionNonce": 2019559783,
|
|
||||||
"width": 10,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`regression tests > should show fill icons when element has non transparent background > [end of test] number of elements 1`] = `0`;
|
|
||||||
|
|
||||||
exports[`regression tests > should show fill icons when element has non transparent background > [end of test] number of renders 1`] = `9`;
|
|
||||||
|
|
||||||
exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] appState 1`] = `
|
exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] appState 1`] = `
|
||||||
{
|
{
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
|
@ -535,6 +535,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button active"
|
class="color-picker__button active"
|
||||||
|
data-testid="color-top-pick-#ffffff"
|
||||||
style="--swatch-color: #ffffff;"
|
style="--swatch-color: #ffffff;"
|
||||||
title="#ffffff"
|
title="#ffffff"
|
||||||
type="button"
|
type="button"
|
||||||
@ -545,6 +546,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button"
|
class="color-picker__button"
|
||||||
|
data-testid="color-top-pick-#f8f9fa"
|
||||||
style="--swatch-color: #f8f9fa;"
|
style="--swatch-color: #f8f9fa;"
|
||||||
title="#f8f9fa"
|
title="#f8f9fa"
|
||||||
type="button"
|
type="button"
|
||||||
@ -555,6 +557,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button"
|
class="color-picker__button"
|
||||||
|
data-testid="color-top-pick-#f5faff"
|
||||||
style="--swatch-color: #f5faff;"
|
style="--swatch-color: #f5faff;"
|
||||||
title="#f5faff"
|
title="#f5faff"
|
||||||
type="button"
|
type="button"
|
||||||
@ -565,6 +568,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button"
|
class="color-picker__button"
|
||||||
|
data-testid="color-top-pick-#fffce8"
|
||||||
style="--swatch-color: #fffce8;"
|
style="--swatch-color: #fffce8;"
|
||||||
title="#fffce8"
|
title="#fffce8"
|
||||||
type="button"
|
type="button"
|
||||||
@ -575,6 +579,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button"
|
class="color-picker__button"
|
||||||
|
data-testid="color-top-pick-#fdf8f6"
|
||||||
style="--swatch-color: #fdf8f6;"
|
style="--swatch-color: #fdf8f6;"
|
||||||
title="#fdf8f6"
|
title="#fdf8f6"
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -1089,20 +1089,6 @@ describe("regression tests", () => {
|
|||||||
});
|
});
|
||||||
assertSelectedElements(rect3);
|
assertSelectedElements(rect3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show fill icons when element has non transparent background", async () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
expect(screen.queryByText(/fill/i)).not.toBeNull();
|
|
||||||
mouse.down();
|
|
||||||
mouse.up(10, 10);
|
|
||||||
expect(screen.queryByText(/fill/i)).toBeNull();
|
|
||||||
togglePopover("Background");
|
|
||||||
UI.clickOnTestId("color-red");
|
|
||||||
// select rectangle
|
|
||||||
mouse.reset();
|
|
||||||
mouse.click();
|
|
||||||
expect(screen.queryByText(/fill/i)).not.toBeNull();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it(
|
it(
|
||||||
|
@ -695,3 +695,12 @@ export type KeyboardModifiersObject = {
|
|||||||
altKey: boolean;
|
altKey: boolean;
|
||||||
metaKey: boolean;
|
metaKey: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Primitive =
|
||||||
|
| number
|
||||||
|
| string
|
||||||
|
| boolean
|
||||||
|
| bigint
|
||||||
|
| symbol
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user