diff --git a/src/actions/actionGroup.ts b/src/actions/actionGroup.tsx
similarity index 68%
rename from src/actions/actionGroup.ts
rename to src/actions/actionGroup.tsx
index 7755a41d..4d606ad4 100644
--- a/src/actions/actionGroup.ts
+++ b/src/actions/actionGroup.tsx
@@ -1,7 +1,11 @@
+import React from "react";
import { KEYS } from "../keys";
+import { t } from "../i18n";
+import { getShortcutKey } from "../utils";
import { register } from "./register";
+import { group, ungroup } from "../components/icons";
import { newElementWith } from "../element/mutateElement";
-import { getSelectedElements } from "../scene";
+import { getSelectedElements, isSomeElementSelected } from "../scene";
import {
getSelectedGroupIds,
selectGroup,
@@ -13,6 +17,39 @@ import {
} from "../groups";
import { getNonDeletedElements } from "../element";
import { randomId } from "../random";
+import { ToolButton } from "../components/ToolButton";
+import { ExcalidrawElement } from "../element/types";
+import { AppState } from "../types";
+
+const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
+ if (elements.length >= 2) {
+ const groupIds = elements[0].groupIds;
+ for (const groupId of groupIds) {
+ if (
+ elements.reduce(
+ (acc, element) => acc && isElementInGroup(element, groupId),
+ true,
+ )
+ ) {
+ return true;
+ }
+ }
+ }
+ return false;
+};
+
+const enableActionGroup = (
+ elements: readonly ExcalidrawElement[],
+ appState: AppState,
+) => {
+ const selectedElements = getSelectedElements(
+ getNonDeletedElements(elements),
+ appState,
+ );
+ return (
+ selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
+ );
+};
export const actionGroup = register({
name: "group",
@@ -91,7 +128,7 @@ export const actionGroup = register({
contextMenuOrder: 4,
contextItemLabel: "labels.group",
contextItemPredicate: (elements, appState) =>
- getSelectedElements(getNonDeletedElements(elements), appState).length > 1,
+ enableActionGroup(elements, appState),
keyTest: (event) => {
return (
!event.shiftKey &&
@@ -99,6 +136,17 @@ export const actionGroup = register({
event.keyCode === KEYS.G_KEY_CODE
);
},
+ PanelComponent: ({ elements, appState, updateData }) => (
+ updateData(null)}
+ title={`${t("labels.group")} — ${getShortcutKey("CtrlOrCmd+G")}`}
+ aria-label={t("labels.group")}
+ visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
+ >
+ ),
});
export const actionUngroup = register({
@@ -140,4 +188,16 @@ export const actionUngroup = register({
contextItemLabel: "labels.ungroup",
contextItemPredicate: (elements, appState) =>
getSelectedGroupIds(appState).length > 0,
+
+ PanelComponent: ({ elements, appState, updateData }) => (
+ updateData(null)}
+ title={`${t("labels.ungroup")} — ${getShortcutKey("CtrlOrCmd+Shift+G")}`}
+ aria-label={t("labels.ungroup")}
+ visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
+ >
+ ),
});
diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx
index 8c06af08..e430e206 100644
--- a/src/components/Actions.tsx
+++ b/src/components/Actions.tsx
@@ -78,6 +78,8 @@ export const SelectedShapeActions = ({
{renderAction("duplicateSelection")}
{renderAction("deleteSelectedElements")}
+ {renderAction("group")}
+ {renderAction("ungroup")}
)}
diff --git a/src/components/icons.tsx b/src/components/icons.tsx
index 901cdb91..c489baf0 100644
--- a/src/components/icons.tsx
+++ b/src/components/icons.tsx
@@ -212,3 +212,128 @@ export const shield = createIcon(
"M11.553 22.894a.998.998 0 00.894 0s3.037-1.516 5.465-4.097C19.616 16.987 21 14.663 21 12V5a1 1 0 00-.649-.936l-8-3a.998.998 0 00-.702 0l-8 3A1 1 0 003 5v7c0 2.663 1.384 4.987 3.088 6.797 2.428 2.581 5.465 4.097 5.465 4.097zm-1.303-8.481l6.644-6.644a.856.856 0 111.212 1.212l-7.25 7.25a.856.856 0 01-1.212 0l-3.75-3.75a.856.856 0 111.212-1.212l3.144 3.144z",
{ width: 24 },
);
+
+export const group = createIcon(
+ <>
+
+
+
+
+
+
+
+
+ >,
+ { width: 182, height: 182 },
+);
+export const ungroup = createIcon(
+ <>
+
+
+
+
+
+
+
+
+
+
+ >,
+ { width: 182, height: 182 },
+);
diff --git a/src/tests/regressionTests.test.tsx b/src/tests/regressionTests.test.tsx
index de2d03fe..297cf1f9 100644
--- a/src/tests/regressionTests.test.tsx
+++ b/src/tests/regressionTests.test.tsx
@@ -973,7 +973,6 @@ describe("regression tests", () => {
"Copy styles",
"Paste styles",
"Delete",
- "Group selection",
"Ungroup selection",
"Add to library",
"Send backward",
@@ -984,7 +983,7 @@ describe("regression tests", () => {
];
expect(contextMenu).not.toBeNull();
- expect(contextMenu?.children.length).toBe(11);
+ expect(contextMenu?.children.length).toBe(10);
options?.forEach((opt, i) => {
expect(opt.textContent).toBe(expectedOptions[i]);
});