From 1f896dded49b804cffe3343c52f7629e179d14d6 Mon Sep 17 00:00:00 2001
From: Guste Gaubaite <219.guste@gmail.com>
Date: Tue, 21 Oct 2025 13:56:04 +0200
Subject: [PATCH 01/11] refactor(#558): Remove add collection and relation
buttons from toolbar
---
.../add-collection.component.tsx | 55 -------------------
.../components/add-collection/index.ts | 1 -
src/pods/toolbar/components/index.ts | 2 -
.../components/relation-button/index.ts | 1 -
.../relation-button.component.tsx | 45 ---------------
src/pods/toolbar/toolbar.pod.tsx | 4 --
6 files changed, 108 deletions(-)
delete mode 100644 src/pods/toolbar/components/add-collection/add-collection.component.tsx
delete mode 100644 src/pods/toolbar/components/add-collection/index.ts
delete mode 100644 src/pods/toolbar/components/relation-button/index.ts
delete mode 100644 src/pods/toolbar/components/relation-button/relation-button.component.tsx
diff --git a/src/pods/toolbar/components/add-collection/add-collection.component.tsx b/src/pods/toolbar/components/add-collection/add-collection.component.tsx
deleted file mode 100644
index 43e8a002..00000000
--- a/src/pods/toolbar/components/add-collection/add-collection.component.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { useModalDialogContext } from '@/core/providers/modal-dialog-provider';
-import { EditTablePod } from '@/pods/edit-table';
-import { TableIcon } from '@/common/components/icons';
-import { useCanvasViewSettingsContext } from '@/core/providers';
-
-import {
- TableVm,
- useCanvasSchemaContext,
-} from '@/core/providers/canvas-schema';
-import { ADD_COLLECTION_TITLE } from '@/common/components/modal-dialog';
-import { ActionButton } from '@/common/components/action-button';
-import { SHORTCUTS } from '@/common/shortcut';
-
-const BORDER_MARGIN = 40;
-
-export const AddCollection = () => {
- const { openModal, closeModal } = useModalDialogContext();
- const { canvasSchema, addTable } = useCanvasSchemaContext();
- const { canvasViewSettings, setLoadSample } = useCanvasViewSettingsContext();
-
- const handleAddTable = (newTable: TableVm) => {
- const updatedTable = {
- ...newTable,
- x: canvasViewSettings.scrollPosition.x + BORDER_MARGIN,
- y: canvasViewSettings.scrollPosition.y + BORDER_MARGIN,
- };
-
- addTable(updatedTable);
- closeModal();
- };
-
- const handleEditTableClick = () => {
- setLoadSample(false);
- openModal(
- ,
- ADD_COLLECTION_TITLE
- );
- };
- const handleCloseModal = () => {
- closeModal();
- };
- return (
- }
- label="Add Collection"
- onClick={handleEditTableClick}
- className="hide-mobile"
- shortcutOptions={SHORTCUTS.addCollection}
- />
- );
-};
diff --git a/src/pods/toolbar/components/add-collection/index.ts b/src/pods/toolbar/components/add-collection/index.ts
deleted file mode 100644
index 0f5212cc..00000000
--- a/src/pods/toolbar/components/add-collection/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './add-collection.component';
diff --git a/src/pods/toolbar/components/index.ts b/src/pods/toolbar/components/index.ts
index 69373094..be8df17c 100644
--- a/src/pods/toolbar/components/index.ts
+++ b/src/pods/toolbar/components/index.ts
@@ -1,6 +1,5 @@
export * from './theme-toggle-button';
export * from './zoom-button';
-export * from './relation-button';
export * from './canvas-setting-button';
export * from './export-button';
export * from './new-button';
@@ -9,7 +8,6 @@ export * from './save-button';
export * from './undo-button';
export * from './redo-button';
export * from './delete-button';
-export * from './add-collection';
export * from './about-button';
export * from './duplicate-button';
export * from './copy-button';
diff --git a/src/pods/toolbar/components/relation-button/index.ts b/src/pods/toolbar/components/relation-button/index.ts
deleted file mode 100644
index bc45f398..00000000
--- a/src/pods/toolbar/components/relation-button/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './relation-button.component';
diff --git a/src/pods/toolbar/components/relation-button/relation-button.component.tsx b/src/pods/toolbar/components/relation-button/relation-button.component.tsx
deleted file mode 100644
index 6010ed05..00000000
--- a/src/pods/toolbar/components/relation-button/relation-button.component.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { useModalDialogContext } from '@/core/providers/modal-dialog-provider';
-import { EditRelationPod } from '@/pods/edit-relation';
-import { Relation } from '@/common/components/icons';
-import { ADD_RELATION_TITLE } from '@/common/components';
-import {
- RelationVm,
- useCanvasSchemaContext,
-} from '@/core/providers/canvas-schema';
-import { ActionButton } from '@/common/components/action-button';
-import { SHORTCUTS } from '@/common/shortcut';
-
-export const RelationButton = () => {
- const { openModal, closeModal } = useModalDialogContext();
- const { canvasSchema, addRelation } = useCanvasSchemaContext();
-
- const handleChangeCanvasSchema = (relation: RelationVm) => {
- addRelation(relation);
- closeModal();
- };
- const handleCloseEditRelation = () => {
- closeModal();
- };
-
- const handleRelationClick = () => {
- openModal(
- ,
- ADD_RELATION_TITLE
- );
- };
-
- return (
- }
- label="Add Relation"
- onClick={handleRelationClick}
- className="hide-mobile"
- shortcutOptions={SHORTCUTS.addRelation}
- disabled={canvasSchema.tables.length < 1}
- />
- );
-};
diff --git a/src/pods/toolbar/toolbar.pod.tsx b/src/pods/toolbar/toolbar.pod.tsx
index 1db89408..98d21997 100644
--- a/src/pods/toolbar/toolbar.pod.tsx
+++ b/src/pods/toolbar/toolbar.pod.tsx
@@ -3,8 +3,6 @@ import {
// CanvasSettingButton,
ZoomInButton,
ZoomOutButton,
- RelationButton,
- AddCollection,
ThemeToggleButton,
ExportButton,
NewButton,
@@ -29,9 +27,7 @@ export const ToolbarPod: React.FC = () => {
-
-
From 0c838f26f517e40bd23301ea25ef5b883422d4b9 Mon Sep 17 00:00:00 2001
From: Guste Gaubaite <219.guste@gmail.com>
Date: Tue, 21 Oct 2025 13:57:25 +0200
Subject: [PATCH 02/11] feat(#558): create and add floating bar component,
integrate with main scene (desktop only)
---
.../add-collection.component.tsx | 58 +++++++++++++++++++
.../components/add-collection/index.ts | 1 +
.../floating-bar-components.module.css | 13 +++++
src/pods/floating-bar/components/index.ts | 2 +
.../components/relation-button/index.ts | 1 +
.../relation-button.component.tsx | 48 +++++++++++++++
.../floating-bar/floating-bar.component.tsx | 16 +++++
.../floating-bar/floating-bar.pod.module.css | 21 +++++++
src/pods/floating-bar/floating-bar.pod.tsx | 9 +++
src/pods/floating-bar/index.ts | 1 +
src/scenes/main.scene.tsx | 5 +-
11 files changed, 174 insertions(+), 1 deletion(-)
create mode 100644 src/pods/floating-bar/components/add-collection/add-collection.component.tsx
create mode 100644 src/pods/floating-bar/components/add-collection/index.ts
create mode 100644 src/pods/floating-bar/components/floating-bar-components.module.css
create mode 100644 src/pods/floating-bar/components/index.ts
create mode 100644 src/pods/floating-bar/components/relation-button/index.ts
create mode 100644 src/pods/floating-bar/components/relation-button/relation-button.component.tsx
create mode 100644 src/pods/floating-bar/floating-bar.component.tsx
create mode 100644 src/pods/floating-bar/floating-bar.pod.module.css
create mode 100644 src/pods/floating-bar/floating-bar.pod.tsx
create mode 100644 src/pods/floating-bar/index.ts
diff --git a/src/pods/floating-bar/components/add-collection/add-collection.component.tsx b/src/pods/floating-bar/components/add-collection/add-collection.component.tsx
new file mode 100644
index 00000000..be779cb7
--- /dev/null
+++ b/src/pods/floating-bar/components/add-collection/add-collection.component.tsx
@@ -0,0 +1,58 @@
+import { useModalDialogContext } from '@/core/providers/modal-dialog-provider';
+import { EditTablePod } from '@/pods/edit-table';
+import { TableIcon } from '@/common/components/icons';
+import { useCanvasViewSettingsContext } from '@/core/providers';
+
+import {
+ TableVm,
+ useCanvasSchemaContext,
+} from '@/core/providers/canvas-schema';
+import { ADD_COLLECTION_TITLE } from '@/common/components/modal-dialog';
+import { ActionButton } from '@/common/components/action-button';
+import { SHORTCUTS } from '@/common/shortcut';
+import classes from '../floating-bar-components.module.css';
+
+const BORDER_MARGIN = 40;
+
+export const AddCollection = () => {
+ const { openModal, closeModal } = useModalDialogContext();
+ const { canvasSchema, addTable } = useCanvasSchemaContext();
+ const { canvasViewSettings, setLoadSample } = useCanvasViewSettingsContext();
+
+ const handleAddTable = (newTable: TableVm) => {
+ const updatedTable = {
+ ...newTable,
+ x: canvasViewSettings.scrollPosition.x + BORDER_MARGIN,
+ y: canvasViewSettings.scrollPosition.y + BORDER_MARGIN,
+ };
+
+ addTable(updatedTable);
+ closeModal();
+ };
+
+ const handleEditTableClick = () => {
+ setLoadSample(false);
+ openModal(
+ ,
+ ADD_COLLECTION_TITLE
+ );
+ };
+ const handleCloseModal = () => {
+ closeModal();
+ };
+ return (
+ }
+ label="Add Collection"
+ onClick={handleEditTableClick}
+ className={`${classes.button} hide-mobile`}
+ shortcutOptions={SHORTCUTS.addCollection}
+ showLabel={false}
+ tooltipPosition="top"
+ />
+ );
+};
diff --git a/src/pods/floating-bar/components/add-collection/index.ts b/src/pods/floating-bar/components/add-collection/index.ts
new file mode 100644
index 00000000..0f5212cc
--- /dev/null
+++ b/src/pods/floating-bar/components/add-collection/index.ts
@@ -0,0 +1 @@
+export * from './add-collection.component';
diff --git a/src/pods/floating-bar/components/floating-bar-components.module.css b/src/pods/floating-bar/components/floating-bar-components.module.css
new file mode 100644
index 00000000..d7c157b8
--- /dev/null
+++ b/src/pods/floating-bar/components/floating-bar-components.module.css
@@ -0,0 +1,13 @@
+.button {
+ padding: 5px;
+}
+
+.button :global svg {
+ width: 1.8em;
+ height: 1.8em;
+}
+
+.button :global([role='tooltip']) {
+ transform: translate(0, -60px);
+ white-space: nowrap;
+}
diff --git a/src/pods/floating-bar/components/index.ts b/src/pods/floating-bar/components/index.ts
new file mode 100644
index 00000000..8e63fe90
--- /dev/null
+++ b/src/pods/floating-bar/components/index.ts
@@ -0,0 +1,2 @@
+export * from './add-collection';
+export * from './relation-button';
diff --git a/src/pods/floating-bar/components/relation-button/index.ts b/src/pods/floating-bar/components/relation-button/index.ts
new file mode 100644
index 00000000..bc45f398
--- /dev/null
+++ b/src/pods/floating-bar/components/relation-button/index.ts
@@ -0,0 +1 @@
+export * from './relation-button.component';
diff --git a/src/pods/floating-bar/components/relation-button/relation-button.component.tsx b/src/pods/floating-bar/components/relation-button/relation-button.component.tsx
new file mode 100644
index 00000000..79c6a72d
--- /dev/null
+++ b/src/pods/floating-bar/components/relation-button/relation-button.component.tsx
@@ -0,0 +1,48 @@
+import { useModalDialogContext } from '@/core/providers/modal-dialog-provider';
+import { EditRelationPod } from '@/pods/edit-relation';
+import { Relation } from '@/common/components/icons';
+import { ADD_RELATION_TITLE } from '@/common/components';
+import {
+ RelationVm,
+ useCanvasSchemaContext,
+} from '@/core/providers/canvas-schema';
+import { ActionButton } from '@/common/components/action-button';
+import { SHORTCUTS } from '@/common/shortcut';
+import classes from '../floating-bar-components.module.css';
+
+export const RelationButton = () => {
+ const { openModal, closeModal } = useModalDialogContext();
+ const { canvasSchema, addRelation } = useCanvasSchemaContext();
+
+ const handleChangeCanvasSchema = (relation: RelationVm) => {
+ addRelation(relation);
+ closeModal();
+ };
+ const handleCloseEditRelation = () => {
+ closeModal();
+ };
+
+ const handleRelationClick = () => {
+ openModal(
+ ,
+ ADD_RELATION_TITLE
+ );
+ };
+
+ return (
+ }
+ label="Add Relation"
+ onClick={handleRelationClick}
+ className={`${classes.button} hide-mobile`}
+ shortcutOptions={SHORTCUTS.addRelation}
+ disabled={canvasSchema.tables.length < 1}
+ showLabel={false}
+ tooltipPosition="top"
+ />
+ );
+};
diff --git a/src/pods/floating-bar/floating-bar.component.tsx b/src/pods/floating-bar/floating-bar.component.tsx
new file mode 100644
index 00000000..84fc7461
--- /dev/null
+++ b/src/pods/floating-bar/floating-bar.component.tsx
@@ -0,0 +1,16 @@
+import { AddCollection } from './components';
+import { RelationButton } from './components';
+import classes from './floating-bar.pod.module.css';
+
+export const FloatingBarComponent: React.FC = () => {
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/src/pods/floating-bar/floating-bar.pod.module.css b/src/pods/floating-bar/floating-bar.pod.module.css
new file mode 100644
index 00000000..d6caffd4
--- /dev/null
+++ b/src/pods/floating-bar/floating-bar.pod.module.css
@@ -0,0 +1,21 @@
+.floating-bar-container {
+ width: 100%;
+ position: fixed;
+ display: flex;
+ justify-content: center;
+ bottom: 80px;
+ z-index: 2;
+}
+
+.floating-bar {
+ position: relative;
+ height: 50px;
+ display: flex;
+ justify-content: center;
+ align-content: center;
+ padding: 6px 12px;
+ gap: var(--space-sm);
+ background-color: var(--bg-toolbar);
+ border-radius: var(--border-radius-m);
+ border: var(--border-toolbar);
+}
diff --git a/src/pods/floating-bar/floating-bar.pod.tsx b/src/pods/floating-bar/floating-bar.pod.tsx
new file mode 100644
index 00000000..c2d75712
--- /dev/null
+++ b/src/pods/floating-bar/floating-bar.pod.tsx
@@ -0,0 +1,9 @@
+import { FloatingBarComponent } from './floating-bar.component';
+
+export const FloatingBarPod: React.FC = () => {
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/src/pods/floating-bar/index.ts b/src/pods/floating-bar/index.ts
new file mode 100644
index 00000000..cfabbf59
--- /dev/null
+++ b/src/pods/floating-bar/index.ts
@@ -0,0 +1 @@
+export * from './floating-bar.pod';
diff --git a/src/scenes/main.scene.tsx b/src/scenes/main.scene.tsx
index 2c9837b6..34c4c066 100644
--- a/src/scenes/main.scene.tsx
+++ b/src/scenes/main.scene.tsx
@@ -1,17 +1,20 @@
import { CanvasPod } from '@/pods/canvas/canvas.pod';
import { ToolbarPod } from '@/pods/toolbar/toolbar.pod';
-import { useModalDialogContext } from '@/core/providers';
+import { useDeviceContext, useModalDialogContext } from '@/core/providers';
import { ModalDialog } from '@/common/components';
import classes from './main.scene.module.css';
import { FooterPod } from '@/pods/footer';
+import { FloatingBarPod } from '@/pods/floating-bar';
export const MainScene: React.FC = () => {
const { modalDialog } = useModalDialogContext();
+ const { isTabletOrMobileDevice } = useDeviceContext();
return (
<>
+ {!isTabletOrMobileDevice && }
From 64b0c4e6940c8a0f5c9cac0b16831ee0a1ce3c81 Mon Sep 17 00:00:00 2001
From: Guste Gaubaite <219.guste@gmail.com>
Date: Wed, 22 Oct 2025 11:25:11 +0200
Subject: [PATCH 03/11] (#558) add extra class for E2E test selectors
---
.../components/add-collection/add-collection.component.tsx | 2 +-
.../components/relation-button/relation-button.component.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/pods/floating-bar/components/add-collection/add-collection.component.tsx b/src/pods/floating-bar/components/add-collection/add-collection.component.tsx
index be779cb7..78b1e2aa 100644
--- a/src/pods/floating-bar/components/add-collection/add-collection.component.tsx
+++ b/src/pods/floating-bar/components/add-collection/add-collection.component.tsx
@@ -49,7 +49,7 @@ export const AddCollection = () => {
icon={}
label="Add Collection"
onClick={handleEditTableClick}
- className={`${classes.button} hide-mobile`}
+ className={`${classes.button} hide-mobile add-collection-button`}
shortcutOptions={SHORTCUTS.addCollection}
showLabel={false}
tooltipPosition="top"
diff --git a/src/pods/floating-bar/components/relation-button/relation-button.component.tsx b/src/pods/floating-bar/components/relation-button/relation-button.component.tsx
index 79c6a72d..e58aecd9 100644
--- a/src/pods/floating-bar/components/relation-button/relation-button.component.tsx
+++ b/src/pods/floating-bar/components/relation-button/relation-button.component.tsx
@@ -38,7 +38,7 @@ export const RelationButton = () => {
icon={}
label="Add Relation"
onClick={handleRelationClick}
- className={`${classes.button} hide-mobile`}
+ className={`${classes.button} hide-mobile relation-button`}
shortcutOptions={SHORTCUTS.addRelation}
disabled={canvasSchema.tables.length < 1}
showLabel={false}
From 9d8443f86c1e86dc85bb43c38519f79a80cd7c82 Mon Sep 17 00:00:00 2001
From: Guste Gaubaite <219.guste@gmail.com>
Date: Wed, 22 Oct 2025 11:25:15 +0200
Subject: [PATCH 04/11] fix(#558): update E2E test to match UI changes (removed
Add Collection label)
---
e2e/add-new-collection.spec.ts | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/e2e/add-new-collection.spec.ts b/e2e/add-new-collection.spec.ts
index a9bab158..61c82454 100644
--- a/e2e/add-new-collection.spec.ts
+++ b/e2e/add-new-collection.spec.ts
@@ -12,9 +12,7 @@ test('opens MongoDB Designer, adds collection, and checks "New Collection" visib
await expect(newButton).toBeVisible();
await newButton.click();
- const addCollectionButton = page
- .getByRole('button', { name: 'Add Collection' })
- .first();
+ const addCollectionButton = page.locator('.add-collection-button');
await expect(addCollectionButton).toBeVisible();
await addCollectionButton.click();
From e9716cedef793df124d40bd0a65e0b56af01b1f3 Mon Sep 17 00:00:00 2001
From: Guste Gaubaite <219.guste@gmail.com>
Date: Fri, 24 Oct 2025 06:54:58 +0200
Subject: [PATCH 05/11] =?UTF-8?q?fix(#558):=20replace=20alt=20key=20for=20?=
=?UTF-8?q?ctrl=20in=20Windows,=20ctrl=20key=20for=20meta=20in=20MacOS=20a?=
=?UTF-8?q?nd=20action=20button=20to=20show=20in=20tooltip=20ctrl=20for=20?=
=?UTF-8?q?Windows/Linux,=20cmd=20(=E2=8C=98)=20for=20MacOS?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../action-button/action-button.component.tsx | 2 +-
src/common/shortcut/shortcut.const.ts | 194 +++++++++---------
src/common/shortcut/shortcut.hook.spec.tsx | 18 +-
src/common/shortcut/shortcut.hook.tsx | 6 +-
4 files changed, 110 insertions(+), 110 deletions(-)
diff --git a/src/common/components/action-button/action-button.component.tsx b/src/common/components/action-button/action-button.component.tsx
index 385a37e7..11a47689 100644
--- a/src/common/components/action-button/action-button.component.tsx
+++ b/src/common/components/action-button/action-button.component.tsx
@@ -25,7 +25,7 @@ export const ActionButton: React.FC = ({
showLabel = true,
tooltipPosition = 'bottom',
}) => {
- const shortcutCommand = isMacOS() ? 'Ctrl' : 'Alt';
+ const shortcutCommand = isMacOS() ? '⌘' : 'Ctrl';
const showTooltip = shortcutOptions && !disabled;
const tooltipText = `(${shortcutCommand} + ${shortcutOptions?.targetKeyLabel})`;
diff --git a/src/common/shortcut/shortcut.const.ts b/src/common/shortcut/shortcut.const.ts
index 3fee1730..23a36ff6 100644
--- a/src/common/shortcut/shortcut.const.ts
+++ b/src/common/shortcut/shortcut.const.ts
@@ -1,104 +1,104 @@
import { ShortcutOptions } from './shortcut.model';
interface Shortcut {
- [key: string]: ShortcutOptions;
+ [key: string]: ShortcutOptions;
}
export const SHORTCUTS: Shortcut = {
- addCollection: {
- description: 'Add Collection',
- id: 'add-collection-button-shortcut',
- targetKey: ['c'],
- targetKeyLabel: 'C',
- },
- addRelation: {
- description: 'Add Relation',
- id: 'add-relation-button-shortcut',
- targetKey: ['r'],
- targetKeyLabel: 'R',
- },
- delete: {
- description: 'Delete',
- id: 'delete-button-shortcut',
- targetKey: ['backspace'],
- targetKeyLabel: 'Backspace',
- },
- export: {
- description: 'Export',
- id: 'export-button-shortcut',
- targetKey: ['e'],
- targetKeyLabel: 'E',
- },
- new: {
- description: 'New',
- id: 'new-button-shortcut',
- targetKey: ['n'],
- targetKeyLabel: 'N',
- },
- open: {
- description: 'Open',
- id: 'open-button-shortcut',
- targetKey: ['o'],
- targetKeyLabel: 'O',
- },
- redo: {
- description: 'Redo',
- id: 'redo-button-shortcut',
- targetKey: ['y'],
- targetKeyLabel: 'Y',
- },
- save: {
- description: 'Save',
- id: 'save-button-shortcut',
- targetKey: ['s'],
- targetKeyLabel: 'S',
- },
- settings: {
- description: 'Settings',
- id: 'settings-button-shortcut',
- targetKey: ['t'],
- targetKeyLabel: 'T',
- },
- undo: {
- description: 'Undo',
- id: 'undo-button-shortcut',
- targetKey: ['z'],
- targetKeyLabel: 'Z',
- },
- zoomIn: {
- description: 'Zoom In',
- id: 'zoom-in-button-shortcut',
- targetKey: ['=', '+'],
- targetKeyLabel: '"+"',
- },
- zoomOut: {
- description: 'Zoom Out',
- id: 'zoom-out-button-shortcut',
- targetKey: ['-', '-'],
- targetKeyLabel: '"-"',
- },
- duplicate: {
- description: 'Duplicate',
- id: 'duplicate-button-shortcut',
- targetKey: ['d'],
- targetKeyLabel: 'D',
- },
- copy: {
- description: 'Copy',
- id: 'copy-button-shortcut',
- targetKey: ['c'],
- targetKeyLabel: 'C',
- },
- paste: {
- description: 'Paste',
- id: 'paste-button-shortcut',
- targetKey: ['v'],
- targetKeyLabel: 'V',
- },
- import: {
- description: 'Import',
- id: 'import-button-shortcut',
- targetKey: ['i'],
- targetKeyLabel: 'I',
- },
+ addCollection: {
+ description: 'Add Collection',
+ id: 'add-collection-button-shortcut',
+ targetKey: ['c'],
+ targetKeyLabel: 'C',
+ },
+ addRelation: {
+ description: 'Add Relation',
+ id: 'add-relation-button-shortcut',
+ targetKey: ['r'],
+ targetKeyLabel: 'R',
+ },
+ delete: {
+ description: 'Delete',
+ id: 'delete-button-shortcut',
+ targetKey: ['backspace'],
+ targetKeyLabel: 'Backspace',
+ },
+ export: {
+ description: 'Export',
+ id: 'export-button-shortcut',
+ targetKey: ['e'],
+ targetKeyLabel: 'E',
+ },
+ new: {
+ description: 'New',
+ id: 'new-button-shortcut',
+ targetKey: ['n'],
+ targetKeyLabel: 'N',
+ },
+ open: {
+ description: 'Open',
+ id: 'open-button-shortcut',
+ targetKey: ['o'],
+ targetKeyLabel: 'O',
+ },
+ redo: {
+ description: 'Redo',
+ id: 'redo-button-shortcut',
+ targetKey: ['y'],
+ targetKeyLabel: 'Y',
+ },
+ save: {
+ description: 'Save',
+ id: 'save-button-shortcut',
+ targetKey: ['s'],
+ targetKeyLabel: 'S',
+ },
+ settings: {
+ description: 'Settings',
+ id: 'settings-button-shortcut',
+ targetKey: [','],
+ targetKeyLabel: ',',
+ },
+ undo: {
+ description: 'Undo',
+ id: 'undo-button-shortcut',
+ targetKey: ['z'],
+ targetKeyLabel: 'Z',
+ },
+ zoomIn: {
+ description: 'Zoom In',
+ id: 'zoom-in-button-shortcut',
+ targetKey: ['=', '+'],
+ targetKeyLabel: '"+"',
+ },
+ zoomOut: {
+ description: 'Zoom Out',
+ id: 'zoom-out-button-shortcut',
+ targetKey: ['-', '-'],
+ targetKeyLabel: '"-"',
+ },
+ duplicate: {
+ description: 'Duplicate',
+ id: 'duplicate-button-shortcut',
+ targetKey: ['d'],
+ targetKeyLabel: 'D',
+ },
+ copy: {
+ description: 'Copy',
+ id: 'copy-button-shortcut',
+ targetKey: ['c'],
+ targetKeyLabel: 'C',
+ },
+ paste: {
+ description: 'Paste',
+ id: 'paste-button-shortcut',
+ targetKey: ['v'],
+ targetKeyLabel: 'V',
+ },
+ import: {
+ description: 'Import',
+ id: 'import-button-shortcut',
+ targetKey: ['i'],
+ targetKeyLabel: 'I',
+ },
};
diff --git a/src/common/shortcut/shortcut.hook.spec.tsx b/src/common/shortcut/shortcut.hook.spec.tsx
index 9c05d8a3..f3aeb6f7 100644
--- a/src/common/shortcut/shortcut.hook.spec.tsx
+++ b/src/common/shortcut/shortcut.hook.spec.tsx
@@ -22,7 +22,7 @@ describe('useShortcut', () => {
const event = new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
- ctrlKey: true,
+ metaKey: true,
});
window.dispatchEvent(event);
@@ -44,7 +44,7 @@ describe('useShortcut', () => {
expect(callback).not.toHaveBeenCalled();
});
- it('should add "Alt" to the event if the user is on Windows or Linux', async () => {
+ it('should add "Ctrl" to the event if the user is on Windows or Linux', async () => {
Object.defineProperty(window.navigator, 'userAgent', {
value: 'Windows',
configurable: true,
@@ -55,7 +55,7 @@ describe('useShortcut', () => {
const event = new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
- altKey: true,
+ ctrlKey: true,
});
window.dispatchEvent(event);
@@ -63,13 +63,13 @@ describe('useShortcut', () => {
expect(callback).toHaveBeenCalled();
});
- it('should add "Ctrl" to the event if the user is on MacOS', async () => {
+ it('should add "⌘" to the event if the user is on MacOS', async () => {
renderHook(() => useShortcut({ targetKey, callback }));
const event = new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
- ctrlKey: true,
+ metaKey: true,
});
window.dispatchEvent(event);
@@ -77,13 +77,13 @@ describe('useShortcut', () => {
expect(callback).toHaveBeenCalled();
});
- it('should not call the callback when the user is on Mac and "Alt" is pressed', async () => {
+ it('should not call the callback when the user is on Mac and "Ctrl" is pressed', async () => {
renderHook(() => useShortcut({ targetKey, callback }));
const event = new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
- altKey: true,
+ ctrlKey: true,
});
window.dispatchEvent(event);
@@ -91,7 +91,7 @@ describe('useShortcut', () => {
expect(callback).not.toHaveBeenCalled();
});
- it('should not call the callback when the user is on Windows or Linux and "Ctrl" is pressed', async () => {
+ it('should not call the callback when the user is on Windows or Linux and "⌘" is pressed', async () => {
Object.defineProperty(window.navigator, 'userAgent', {
value: 'Windows',
configurable: true,
@@ -102,7 +102,7 @@ describe('useShortcut', () => {
const event = new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
- ctrlKey: true,
+ metaKey: true,
});
window.dispatchEvent(event);
diff --git a/src/common/shortcut/shortcut.hook.tsx b/src/common/shortcut/shortcut.hook.tsx
index 1421f4b0..9904332a 100644
--- a/src/common/shortcut/shortcut.hook.tsx
+++ b/src/common/shortcut/shortcut.hook.tsx
@@ -17,12 +17,12 @@ export interface ShortcutHookProps {
const useShortcut = ({ targetKey, callback }: ShortcutHookProps) => {
const handleKeyPress = (event: KeyboardEvent) => {
- const isAltKeyPressed = event.getModifierState('Alt');
+ const isMetaKeyPressed = event.getModifierState('Meta');
const isCtrlKeyPressed = event.getModifierState('Control');
if (
- (isWindowsOrLinux() && isAltKeyPressed) ||
- (isMacOS() && isCtrlKeyPressed)
+ (isWindowsOrLinux() && isCtrlKeyPressed) ||
+ (isMacOS() && isMetaKeyPressed)
) {
if (targetKey.includes(event.key)) {
event.preventDefault();
From e60a649689b3f606f744a5626190b1730b008ceb Mon Sep 17 00:00:00 2001
From: Guste Gaubaite <219.guste@gmail.com>
Date: Fri, 24 Oct 2025 06:58:16 +0200
Subject: [PATCH 06/11] fix(#558 paste-button): tooltip not showing
(SHORTCUTS.Paste -> SHORTCUTS.paste)
---
.../toolbar/components/paste-button/paste-button.component.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pods/toolbar/components/paste-button/paste-button.component.tsx b/src/pods/toolbar/components/paste-button/paste-button.component.tsx
index 58396731..acba62da 100644
--- a/src/pods/toolbar/components/paste-button/paste-button.component.tsx
+++ b/src/pods/toolbar/components/paste-button/paste-button.component.tsx
@@ -13,7 +13,7 @@ export const PasteButton = () => {
onClick={pasteTable}
className="hide-mobile"
disabled={!hasClipboardContent}
- shortcutOptions={SHORTCUTS.Paste}
+ shortcutOptions={SHORTCUTS.paste}
/>
);
};
From 774ebd9476b72cca03af805b0ae9bb714839cb17 Mon Sep 17 00:00:00 2001
From: Guste Gaubaite <219.guste@gmail.com>
Date: Fri, 24 Oct 2025 07:32:55 +0200
Subject: [PATCH 07/11] feat(#558 shortcuts): implement noModifier optional
property and its handling to allow certain actions to use single-key
shortscuts (add collection, add relation and backspace), update tests with
noModifier functionality and ActionButton to show correct tooltip (without
Ctrl/Cmd)
---
.../action-button/action-button.component.tsx | 4 +-
src/common/shortcut/shortcut.const.ts | 7 ++-
src/common/shortcut/shortcut.hook.spec.tsx | 58 +++++++++++++++++++
src/common/shortcut/shortcut.hook.tsx | 23 +++++---
src/common/shortcut/shortcut.model.ts | 1 +
5 files changed, 81 insertions(+), 12 deletions(-)
diff --git a/src/common/components/action-button/action-button.component.tsx b/src/common/components/action-button/action-button.component.tsx
index 11a47689..801368c9 100644
--- a/src/common/components/action-button/action-button.component.tsx
+++ b/src/common/components/action-button/action-button.component.tsx
@@ -27,7 +27,9 @@ export const ActionButton: React.FC = ({
}) => {
const shortcutCommand = isMacOS() ? '⌘' : 'Ctrl';
const showTooltip = shortcutOptions && !disabled;
- const tooltipText = `(${shortcutCommand} + ${shortcutOptions?.targetKeyLabel})`;
+ const tooltipText = shortcutOptions?.noModifier
+ ? `${shortcutOptions.targetKeyLabel}`
+ : `(${shortcutCommand} + ${shortcutOptions?.targetKeyLabel})`;
const tooltipPositionClass =
tooltipPosition === 'top' ? classes.tooltipTop : classes.tooltipBottom;
diff --git a/src/common/shortcut/shortcut.const.ts b/src/common/shortcut/shortcut.const.ts
index 23a36ff6..36f997c5 100644
--- a/src/common/shortcut/shortcut.const.ts
+++ b/src/common/shortcut/shortcut.const.ts
@@ -9,19 +9,22 @@ export const SHORTCUTS: Shortcut = {
description: 'Add Collection',
id: 'add-collection-button-shortcut',
targetKey: ['c'],
- targetKeyLabel: 'C',
+ targetKeyLabel: 'Collection "C"',
+ noModifier: true,
},
addRelation: {
description: 'Add Relation',
id: 'add-relation-button-shortcut',
targetKey: ['r'],
- targetKeyLabel: 'R',
+ targetKeyLabel: 'Relation "R"',
+ noModifier: true,
},
delete: {
description: 'Delete',
id: 'delete-button-shortcut',
targetKey: ['backspace'],
targetKeyLabel: 'Backspace',
+ noModifier: true,
},
export: {
description: 'Export',
diff --git a/src/common/shortcut/shortcut.hook.spec.tsx b/src/common/shortcut/shortcut.hook.spec.tsx
index f3aeb6f7..d1803c40 100644
--- a/src/common/shortcut/shortcut.hook.spec.tsx
+++ b/src/common/shortcut/shortcut.hook.spec.tsx
@@ -109,4 +109,62 @@ describe('useShortcut', () => {
expect(callback).not.toHaveBeenCalled();
});
+
+ it('should call the callback when noModifier is true and only the key is pressed', () => {
+ renderHook(() =>
+ useShortcut({
+ targetKey,
+ callback,
+ noModifier: true,
+ })
+ );
+
+ const event = new KeyboardEvent('keydown', {
+ key: 'a',
+ code: 'KeyA',
+ });
+
+ window.dispatchEvent(event);
+
+ expect(callback).toHaveBeenCalled();
+ });
+
+ it('should not call the callback when noModifier is true and modifier is pressed', () => {
+ renderHook(() =>
+ useShortcut({
+ targetKey,
+ callback,
+ noModifier: true,
+ })
+ );
+
+ const event = new KeyboardEvent('keydown', {
+ key: 'a',
+ code: 'KeyA',
+ ctrlKey: true,
+ });
+
+ window.dispatchEvent(event);
+
+ expect(callback).not.toHaveBeenCalled();
+ });
+
+ it('should not call the callback when noModifier is false and no modifier is pressed', () => {
+ renderHook(() =>
+ useShortcut({
+ targetKey,
+ callback,
+ noModifier: false,
+ })
+ );
+
+ const event = new KeyboardEvent('keydown', {
+ key: 'a',
+ code: 'KeyA',
+ });
+
+ window.dispatchEvent(event);
+
+ expect(callback).not.toHaveBeenCalled();
+ });
});
diff --git a/src/common/shortcut/shortcut.hook.tsx b/src/common/shortcut/shortcut.hook.tsx
index 9904332a..459753b6 100644
--- a/src/common/shortcut/shortcut.hook.tsx
+++ b/src/common/shortcut/shortcut.hook.tsx
@@ -4,6 +4,7 @@ import { useEffect } from 'react';
export interface ShortcutHookProps {
targetKey: string[];
callback: () => void;
+ noModifier?: boolean;
}
/**
@@ -15,19 +16,23 @@ export interface ShortcutHookProps {
* @return {void}
*/
-const useShortcut = ({ targetKey, callback }: ShortcutHookProps) => {
+const useShortcut = ({
+ targetKey,
+ callback,
+ noModifier,
+}: ShortcutHookProps) => {
const handleKeyPress = (event: KeyboardEvent) => {
const isMetaKeyPressed = event.getModifierState('Meta');
const isCtrlKeyPressed = event.getModifierState('Control');
- if (
- (isWindowsOrLinux() && isCtrlKeyPressed) ||
- (isMacOS() && isMetaKeyPressed)
- ) {
- if (targetKey.includes(event.key)) {
- event.preventDefault();
- callback();
- }
+ const hasCorrectModifier = noModifier
+ ? !isMetaKeyPressed && !isCtrlKeyPressed
+ : (isWindowsOrLinux() && isCtrlKeyPressed) ||
+ (isMacOS() && isMetaKeyPressed);
+
+ if (hasCorrectModifier && targetKey.includes(event.key)) {
+ event.preventDefault();
+ callback();
}
};
diff --git a/src/common/shortcut/shortcut.model.ts b/src/common/shortcut/shortcut.model.ts
index 13bff58b..fa75155c 100644
--- a/src/common/shortcut/shortcut.model.ts
+++ b/src/common/shortcut/shortcut.model.ts
@@ -3,4 +3,5 @@ export interface ShortcutOptions {
targetKey: string[];
targetKeyLabel: string;
description: string;
+ noModifier?: boolean;
}
From cf582014e328645105a48defe56277e3a6cc6d1e Mon Sep 17 00:00:00 2001
From: Guste Gaubaite <219.guste@gmail.com>
Date: Fri, 24 Oct 2025 07:44:57 +0200
Subject: [PATCH 08/11] =?UTF-8?q?test(#558=20action=20button=20test):=20fi?=
=?UTF-8?q?x=20tooltip=20to=20show=20updated=20modifier=20(=E2=8C=98=20for?=
=?UTF-8?q?=20Mac,=20Ctrl=20for=20Windows),=20add=20test=20for=20no-modifi?=
=?UTF-8?q?er=20shortcuts=20tooltip=20display?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../action-button.component.spec.tsx | 21 ++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/src/common/components/action-button/action-button.component.spec.tsx b/src/common/components/action-button/action-button.component.spec.tsx
index 03fd9978..13e3f888 100644
--- a/src/common/components/action-button/action-button.component.spec.tsx
+++ b/src/common/components/action-button/action-button.component.spec.tsx
@@ -64,7 +64,7 @@ describe('ActionButton', () => {
const tooltip = getByRole('tooltip');
- expect(tooltip.textContent).toContain('Ctrl + A');
+ expect(tooltip.textContent).toContain('⌘ + A');
});
it('should disable the button if the disabled prop is true', () => {
@@ -125,4 +125,23 @@ describe('ActionButton', () => {
const tooltip = getByRole('tooltip');
expect(tooltip.className).toContain('tooltipTop');
});
+
+ it('should render the tooltip without modifier when noModifier is true', () => {
+ const shortcutOptionsNoMod = {
+ ...shortcutOptions,
+ noModifier: true,
+ };
+
+ const { getByRole } = render(
+ Icon}
+ label="Label"
+ onClick={onClick}
+ shortcutOptions={shortcutOptionsNoMod}
+ />
+ );
+
+ const tooltip = getByRole('tooltip');
+ expect(tooltip.textContent).toBe('A');
+ });
});
From 56985bd272e64a9ef15936c76fc054e80e01b613 Mon Sep 17 00:00:00 2001
From: Guste Gaubaite <219.guste@gmail.com>
Date: Sun, 26 Oct 2025 08:26:42 +0100
Subject: [PATCH 09/11] fix(#558 shortcuts): if a modal is open, do not process
shortcuts
---
src/common/shortcut/shortcut.hook.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/common/shortcut/shortcut.hook.tsx b/src/common/shortcut/shortcut.hook.tsx
index 459753b6..1b7bac10 100644
--- a/src/common/shortcut/shortcut.hook.tsx
+++ b/src/common/shortcut/shortcut.hook.tsx
@@ -1,4 +1,5 @@
import { isMacOS, isWindowsOrLinux } from '@/common/helpers/platform.helpers';
+import { useModalDialogContext } from '@/core/providers';
import { useEffect } from 'react';
export interface ShortcutHookProps {
@@ -21,7 +22,13 @@ const useShortcut = ({
callback,
noModifier,
}: ShortcutHookProps) => {
+ const { modalDialog } = useModalDialogContext();
+
const handleKeyPress = (event: KeyboardEvent) => {
+ if (modalDialog.isOpen) {
+ return;
+ }
+
const isMetaKeyPressed = event.getModifierState('Meta');
const isCtrlKeyPressed = event.getModifierState('Control');
From cd653d8f6950bb25e1a1ed26cbe536944963958b Mon Sep 17 00:00:00 2001
From: Guste Gaubaite <219.guste@gmail.com>
Date: Sun, 26 Oct 2025 09:45:21 +0100
Subject: [PATCH 10/11] refactor(shortcuts): implement Alt modifier, improve
modifier system to handle ctrl/cmd, alt and no-modifier cases, update tests
---
.../action-button.component.spec.tsx | 121 +++++++++++++-----
.../action-button/action-button.component.tsx | 27 +++-
src/common/shortcut/shortcut.const.ts | 7 +-
src/common/shortcut/shortcut.hook.spec.tsx | 79 ++++++++++--
src/common/shortcut/shortcut.hook.tsx | 19 ++-
src/common/shortcut/shortcut.model.ts | 4 +-
6 files changed, 202 insertions(+), 55 deletions(-)
diff --git a/src/common/components/action-button/action-button.component.spec.tsx b/src/common/components/action-button/action-button.component.spec.tsx
index 13e3f888..e1b8d2e1 100644
--- a/src/common/components/action-button/action-button.component.spec.tsx
+++ b/src/common/components/action-button/action-button.component.spec.tsx
@@ -2,12 +2,27 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { vi } from 'vitest';
import { ActionButton } from './action-button.component';
import { ShortcutOptions } from '@/common/shortcut';
+import { useModalDialogContext } from '@/core/providers';
+
+vi.mock('@/core/providers', () => ({
+ useModalDialogContext: vi.fn(),
+}));
describe('ActionButton', () => {
let onClick: () => void;
let shortcutOptions: ShortcutOptions;
beforeEach(() => {
+ vi.mocked(useModalDialogContext).mockReturnValue({
+ modalDialog: {
+ isOpen: false,
+ selectedComponent: null,
+ title: '',
+ },
+ openModal: vi.fn(),
+ closeModal: vi.fn(),
+ });
+
onClick = vi.fn();
shortcutOptions = {
@@ -52,19 +67,84 @@ describe('ActionButton', () => {
expect(onClick).toHaveBeenCalled();
});
- it('should render the tooltip with the correct shortcut key', () => {
- const { getByRole } = render(
- Icon}
- label="Label"
- onClick={onClick}
- shortcutOptions={shortcutOptions}
- />
- );
+ describe('tooltip display', () => {
+ it('should render system modifier correctly', () => {
+ // Test Mac
+ const { getByRole, rerender } = render(
+ Icon}
+ label="Label"
+ onClick={onClick}
+ shortcutOptions={{
+ ...shortcutOptions,
+ modifierType: 'system',
+ }}
+ />
+ );
+ expect(getByRole('tooltip').textContent).toBe('(⌘ + A)');
+
+ // Test Windows
+ Object.defineProperty(window.navigator, 'userAgent', {
+ value: 'Windows',
+ configurable: true,
+ });
+ rerender(
+ Icon}
+ label="Label"
+ onClick={onClick}
+ shortcutOptions={{
+ ...shortcutOptions,
+ modifierType: 'system',
+ }}
+ />
+ );
+ expect(getByRole('tooltip').textContent).toBe('(Ctrl + A)');
+ });
- const tooltip = getByRole('tooltip');
+ it('should render alt tooltip correctly', () => {
+ const { getByRole } = render(
+ Icon}
+ label="Label"
+ onClick={onClick}
+ shortcutOptions={{
+ ...shortcutOptions,
+ modifierType: 'alt',
+ }}
+ />
+ );
+ expect(getByRole('tooltip').textContent).toBe('(Alt + A)');
+ });
+
+ it('should render mo-modifier tooltip correctly', () => {
+ const { getByRole } = render(
+ Icon}
+ label="Label"
+ onClick={onClick}
+ shortcutOptions={{
+ ...shortcutOptions,
+ modifierType: 'none',
+ }}
+ />
+ );
+ expect(getByRole('tooltip').textContent).toBe('(A)');
+ });
- expect(tooltip.textContent).toContain('⌘ + A');
+ it('should not show tooltip when disabled', () => {
+ const { queryByRole } = render(
+ Icon}
+ label="Label"
+ onClick={onClick}
+ disabled
+ shortcutOptions={shortcutOptions}
+ />
+ );
+
+ expect(queryByRole('tooltip')).toBeNull();
+ });
});
it('should disable the button if the disabled prop is true', () => {
@@ -125,23 +205,4 @@ describe('ActionButton', () => {
const tooltip = getByRole('tooltip');
expect(tooltip.className).toContain('tooltipTop');
});
-
- it('should render the tooltip without modifier when noModifier is true', () => {
- const shortcutOptionsNoMod = {
- ...shortcutOptions,
- noModifier: true,
- };
-
- const { getByRole } = render(
- Icon}
- label="Label"
- onClick={onClick}
- shortcutOptions={shortcutOptionsNoMod}
- />
- );
-
- const tooltip = getByRole('tooltip');
- expect(tooltip.textContent).toBe('A');
- });
});
diff --git a/src/common/components/action-button/action-button.component.tsx b/src/common/components/action-button/action-button.component.tsx
index 801368c9..69044b17 100644
--- a/src/common/components/action-button/action-button.component.tsx
+++ b/src/common/components/action-button/action-button.component.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { isMacOS } from '@/common/helpers/platform.helpers';
import classes from './action-button.component.module.css';
-import { ShortcutOptions } from '@/common/shortcut';
+import { ModifierType, ShortcutOptions } from '@/common/shortcut';
import useShortcut from '@/common/shortcut/shortcut.hook';
interface Props {
@@ -25,11 +25,28 @@ export const ActionButton: React.FC = ({
showLabel = true,
tooltipPosition = 'bottom',
}) => {
- const shortcutCommand = isMacOS() ? '⌘' : 'Ctrl';
+ const getModifierSymbol = (modifierType: ModifierType = 'system') => {
+ switch (modifierType) {
+ case 'none':
+ return '';
+ case 'alt':
+ return 'Alt';
+ case 'system':
+ default:
+ return isMacOS() ? '⌘' : 'Ctrl';
+ }
+ };
+
+ const shortcutCommand = getModifierSymbol(shortcutOptions?.modifierType);
+
const showTooltip = shortcutOptions && !disabled;
- const tooltipText = shortcutOptions?.noModifier
- ? `${shortcutOptions.targetKeyLabel}`
- : `(${shortcutCommand} + ${shortcutOptions?.targetKeyLabel})`;
+ const tooltipText =
+ shortcutOptions &&
+ `(${
+ shortcutOptions.modifierType === 'none'
+ ? shortcutOptions.targetKeyLabel
+ : `${shortcutCommand} + ${shortcutOptions.targetKeyLabel}`
+ })`;
const tooltipPositionClass =
tooltipPosition === 'top' ? classes.tooltipTop : classes.tooltipBottom;
diff --git a/src/common/shortcut/shortcut.const.ts b/src/common/shortcut/shortcut.const.ts
index 36f997c5..25183cd5 100644
--- a/src/common/shortcut/shortcut.const.ts
+++ b/src/common/shortcut/shortcut.const.ts
@@ -10,21 +10,21 @@ export const SHORTCUTS: Shortcut = {
id: 'add-collection-button-shortcut',
targetKey: ['c'],
targetKeyLabel: 'Collection "C"',
- noModifier: true,
+ modifierType: 'none',
},
addRelation: {
description: 'Add Relation',
id: 'add-relation-button-shortcut',
targetKey: ['r'],
targetKeyLabel: 'Relation "R"',
- noModifier: true,
+ modifierType: 'none',
},
delete: {
description: 'Delete',
id: 'delete-button-shortcut',
targetKey: ['backspace'],
targetKeyLabel: 'Backspace',
- noModifier: true,
+ modifierType: 'none',
},
export: {
description: 'Export',
@@ -37,6 +37,7 @@ export const SHORTCUTS: Shortcut = {
id: 'new-button-shortcut',
targetKey: ['n'],
targetKeyLabel: 'N',
+ modifierType: 'alt',
},
open: {
description: 'Open',
diff --git a/src/common/shortcut/shortcut.hook.spec.tsx b/src/common/shortcut/shortcut.hook.spec.tsx
index d1803c40..4f2bb940 100644
--- a/src/common/shortcut/shortcut.hook.spec.tsx
+++ b/src/common/shortcut/shortcut.hook.spec.tsx
@@ -1,12 +1,27 @@
import { renderHook } from '@testing-library/react';
import useShortcut from './shortcut.hook';
import { vi } from 'vitest';
+import { useModalDialogContext } from '@/core/providers';
+
+vi.mock('@/core/providers', () => ({
+ useModalDialogContext: vi.fn(),
+}));
describe('useShortcut', () => {
let targetKey: string[];
let callback: () => void;
beforeEach(() => {
+ vi.mocked(useModalDialogContext).mockReturnValue({
+ modalDialog: {
+ isOpen: false,
+ selectedComponent: null,
+ title: '',
+ },
+ openModal: vi.fn(),
+ closeModal: vi.fn(),
+ });
+
targetKey = ['a'];
callback = vi.fn();
@@ -110,51 +125,51 @@ describe('useShortcut', () => {
expect(callback).not.toHaveBeenCalled();
});
- it('should call the callback when noModifier is true and only the key is pressed', () => {
+ it('should call callback with alt modifier', () => {
renderHook(() =>
useShortcut({
targetKey,
callback,
- noModifier: true,
+ modifierType: 'alt',
})
);
const event = new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
+ altKey: true,
});
window.dispatchEvent(event);
-
expect(callback).toHaveBeenCalled();
});
- it('should not call the callback when noModifier is true and modifier is pressed', () => {
+ it('should not call callback if other modifiers are pressed with alt', () => {
renderHook(() =>
useShortcut({
targetKey,
callback,
- noModifier: true,
+ modifierType: 'alt',
})
);
const event = new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
- ctrlKey: true,
+ altKey: true,
+ ctrlKey: true, // No debería funcionar con otros modificadores
});
window.dispatchEvent(event);
-
expect(callback).not.toHaveBeenCalled();
});
- it('should not call the callback when noModifier is false and no modifier is pressed', () => {
+ it('should call callback with no modifiers', () => {
renderHook(() =>
useShortcut({
targetKey,
callback,
- noModifier: false,
+ modifierType: 'none',
})
);
@@ -164,7 +179,53 @@ describe('useShortcut', () => {
});
window.dispatchEvent(event);
+ expect(callback).toHaveBeenCalled();
+ });
+ it('should not call callback if any modifier is pressed', () => {
+ renderHook(() =>
+ useShortcut({
+ targetKey,
+ callback,
+ modifierType: 'none',
+ })
+ );
+
+ const event = new KeyboardEvent('keydown', {
+ key: 'a',
+ code: 'KeyA',
+ altKey: true,
+ });
+
+ window.dispatchEvent(event);
+ expect(callback).not.toHaveBeenCalled();
+ });
+
+ it('should not call callback when modal is open', () => {
+ vi.mocked(useModalDialogContext).mockReturnValueOnce({
+ modalDialog: {
+ isOpen: true,
+ selectedComponent: null,
+ title: '',
+ },
+ openModal: vi.fn(),
+ closeModal: vi.fn(),
+ });
+
+ renderHook(() =>
+ useShortcut({
+ targetKey,
+ callback,
+ modifierType: 'none',
+ })
+ );
+
+ const event = new KeyboardEvent('keydown', {
+ key: 'r',
+ code: 'KeyR',
+ });
+
+ window.dispatchEvent(event);
expect(callback).not.toHaveBeenCalled();
});
});
diff --git a/src/common/shortcut/shortcut.hook.tsx b/src/common/shortcut/shortcut.hook.tsx
index 1b7bac10..aa4722fd 100644
--- a/src/common/shortcut/shortcut.hook.tsx
+++ b/src/common/shortcut/shortcut.hook.tsx
@@ -1,11 +1,12 @@
import { isMacOS, isWindowsOrLinux } from '@/common/helpers/platform.helpers';
import { useModalDialogContext } from '@/core/providers';
import { useEffect } from 'react';
+import { ModifierType } from './shortcut.model';
export interface ShortcutHookProps {
targetKey: string[];
callback: () => void;
- noModifier?: boolean;
+ modifierType?: ModifierType;
}
/**
@@ -20,7 +21,7 @@ export interface ShortcutHookProps {
const useShortcut = ({
targetKey,
callback,
- noModifier,
+ modifierType = 'system',
}: ShortcutHookProps) => {
const { modalDialog } = useModalDialogContext();
@@ -31,13 +32,17 @@ const useShortcut = ({
const isMetaKeyPressed = event.getModifierState('Meta');
const isCtrlKeyPressed = event.getModifierState('Control');
+ const isAltKeyPressed = event.getModifierState('Alt');
- const hasCorrectModifier = noModifier
- ? !isMetaKeyPressed && !isCtrlKeyPressed
- : (isWindowsOrLinux() && isCtrlKeyPressed) ||
- (isMacOS() && isMetaKeyPressed);
+ const isValidModifier = {
+ none: !isMetaKeyPressed && !isCtrlKeyPressed && !isAltKeyPressed,
+ system:
+ (isWindowsOrLinux() && isCtrlKeyPressed) ||
+ (isMacOS() && isMetaKeyPressed),
+ alt: isAltKeyPressed && !isCtrlKeyPressed && !isMetaKeyPressed,
+ }[modifierType];
- if (hasCorrectModifier && targetKey.includes(event.key)) {
+ if (isValidModifier && targetKey.includes(event.key)) {
event.preventDefault();
callback();
}
diff --git a/src/common/shortcut/shortcut.model.ts b/src/common/shortcut/shortcut.model.ts
index fa75155c..4fc1bc49 100644
--- a/src/common/shortcut/shortcut.model.ts
+++ b/src/common/shortcut/shortcut.model.ts
@@ -1,7 +1,9 @@
+export type ModifierType = 'system' | 'alt' | 'none';
+
export interface ShortcutOptions {
id: string;
targetKey: string[];
targetKeyLabel: string;
description: string;
- noModifier?: boolean;
+ modifierType?: ModifierType;
}
From 4d63182e1ce058b5142205661fc9f73bff19df1f Mon Sep 17 00:00:00 2001
From: Guste Gaubaite <219.guste@gmail.com>
Date: Mon, 27 Oct 2025 08:01:06 +0100
Subject: [PATCH 11/11] docs(shortcuts): update hook documentation with correct
modifier descriptions
---
src/common/shortcut/shortcut.hook.spec.tsx | 2 +-
src/common/shortcut/shortcut.hook.tsx | 8 +++++---
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/common/shortcut/shortcut.hook.spec.tsx b/src/common/shortcut/shortcut.hook.spec.tsx
index 4f2bb940..0eeaa278 100644
--- a/src/common/shortcut/shortcut.hook.spec.tsx
+++ b/src/common/shortcut/shortcut.hook.spec.tsx
@@ -157,7 +157,7 @@ describe('useShortcut', () => {
key: 'a',
code: 'KeyA',
altKey: true,
- ctrlKey: true, // No debería funcionar con otros modificadores
+ ctrlKey: true,
});
window.dispatchEvent(event);
diff --git a/src/common/shortcut/shortcut.hook.tsx b/src/common/shortcut/shortcut.hook.tsx
index aa4722fd..d2c08182 100644
--- a/src/common/shortcut/shortcut.hook.tsx
+++ b/src/common/shortcut/shortcut.hook.tsx
@@ -10,11 +10,13 @@ export interface ShortcutHookProps {
}
/**
- * This hook is used to create a keyboard shortcut
- * it uses Ctrl + key for Windows and Cmd + key for Mac
- * to avoid conflicts with the browser shortcuts
+ * * This hook is used to create keyboard shortcuts with different modifier types:
+ * - system: Uses Ctrl (Windows/Linux) or Cmd (Mac) as modifier
+ * - alt: Uses Alt key as modifier (same behavior in all platforms)
+ * - none: No modifier required, direct key press
* @param {String[]} targetKey The key that will trigger the shortcut
* @param {Function} callback The function to be called when the shortcut is triggered
+ * @param {ModifierType} modifierType The type of modifier to use ('system' | 'alt' | 'none')
* @return {void}
*/