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} */