Skip to content

Commit cd653d8

Browse files
refactor(shortcuts): implement Alt modifier, improve modifier system to handle ctrl/cmd, alt and no-modifier cases, update tests
1 parent 56985bd commit cd653d8

6 files changed

Lines changed: 202 additions & 55 deletions

File tree

src/common/components/action-button/action-button.component.spec.tsx

Lines changed: 91 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,27 @@ import { fireEvent, render, screen } from '@testing-library/react';
22
import { vi } from 'vitest';
33
import { ActionButton } from './action-button.component';
44
import { ShortcutOptions } from '@/common/shortcut';
5+
import { useModalDialogContext } from '@/core/providers';
6+
7+
vi.mock('@/core/providers', () => ({
8+
useModalDialogContext: vi.fn(),
9+
}));
510

611
describe('ActionButton', () => {
712
let onClick: () => void;
813
let shortcutOptions: ShortcutOptions;
914

1015
beforeEach(() => {
16+
vi.mocked(useModalDialogContext).mockReturnValue({
17+
modalDialog: {
18+
isOpen: false,
19+
selectedComponent: null,
20+
title: '',
21+
},
22+
openModal: vi.fn(),
23+
closeModal: vi.fn(),
24+
});
25+
1126
onClick = vi.fn();
1227

1328
shortcutOptions = {
@@ -52,19 +67,84 @@ describe('ActionButton', () => {
5267
expect(onClick).toHaveBeenCalled();
5368
});
5469

55-
it('should render the tooltip with the correct shortcut key', () => {
56-
const { getByRole } = render(
57-
<ActionButton
58-
icon={<span>Icon</span>}
59-
label="Label"
60-
onClick={onClick}
61-
shortcutOptions={shortcutOptions}
62-
/>
63-
);
70+
describe('tooltip display', () => {
71+
it('should render system modifier correctly', () => {
72+
// Test Mac
73+
const { getByRole, rerender } = render(
74+
<ActionButton
75+
icon={<span>Icon</span>}
76+
label="Label"
77+
onClick={onClick}
78+
shortcutOptions={{
79+
...shortcutOptions,
80+
modifierType: 'system',
81+
}}
82+
/>
83+
);
84+
expect(getByRole('tooltip').textContent).toBe('(⌘ + A)');
85+
86+
// Test Windows
87+
Object.defineProperty(window.navigator, 'userAgent', {
88+
value: 'Windows',
89+
configurable: true,
90+
});
91+
rerender(
92+
<ActionButton
93+
icon={<span>Icon</span>}
94+
label="Label"
95+
onClick={onClick}
96+
shortcutOptions={{
97+
...shortcutOptions,
98+
modifierType: 'system',
99+
}}
100+
/>
101+
);
102+
expect(getByRole('tooltip').textContent).toBe('(Ctrl + A)');
103+
});
64104

65-
const tooltip = getByRole('tooltip');
105+
it('should render alt tooltip correctly', () => {
106+
const { getByRole } = render(
107+
<ActionButton
108+
icon={<span>Icon</span>}
109+
label="Label"
110+
onClick={onClick}
111+
shortcutOptions={{
112+
...shortcutOptions,
113+
modifierType: 'alt',
114+
}}
115+
/>
116+
);
117+
expect(getByRole('tooltip').textContent).toBe('(Alt + A)');
118+
});
119+
120+
it('should render mo-modifier tooltip correctly', () => {
121+
const { getByRole } = render(
122+
<ActionButton
123+
icon={<span>Icon</span>}
124+
label="Label"
125+
onClick={onClick}
126+
shortcutOptions={{
127+
...shortcutOptions,
128+
modifierType: 'none',
129+
}}
130+
/>
131+
);
132+
expect(getByRole('tooltip').textContent).toBe('(A)');
133+
});
66134

67-
expect(tooltip.textContent).toContain('⌘ + A');
135+
it('should not show tooltip when disabled', () => {
136+
const { queryByRole } = render(
137+
<ActionButton
138+
icon={<span>Icon</span>}
139+
label="Label"
140+
onClick={onClick}
141+
disabled
142+
shortcutOptions={shortcutOptions}
143+
/>
144+
);
145+
146+
expect(queryByRole('tooltip')).toBeNull();
147+
});
68148
});
69149

70150
it('should disable the button if the disabled prop is true', () => {
@@ -125,23 +205,4 @@ describe('ActionButton', () => {
125205
const tooltip = getByRole('tooltip');
126206
expect(tooltip.className).toContain('tooltipTop');
127207
});
128-
129-
it('should render the tooltip without modifier when noModifier is true', () => {
130-
const shortcutOptionsNoMod = {
131-
...shortcutOptions,
132-
noModifier: true,
133-
};
134-
135-
const { getByRole } = render(
136-
<ActionButton
137-
icon={<span>Icon</span>}
138-
label="Label"
139-
onClick={onClick}
140-
shortcutOptions={shortcutOptionsNoMod}
141-
/>
142-
);
143-
144-
const tooltip = getByRole('tooltip');
145-
expect(tooltip.textContent).toBe('A');
146-
});
147208
});

src/common/components/action-button/action-button.component.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { isMacOS } from '@/common/helpers/platform.helpers';
33
import classes from './action-button.component.module.css';
4-
import { ShortcutOptions } from '@/common/shortcut';
4+
import { ModifierType, ShortcutOptions } from '@/common/shortcut';
55
import useShortcut from '@/common/shortcut/shortcut.hook';
66

77
interface Props {
@@ -25,11 +25,28 @@ export const ActionButton: React.FC<Props> = ({
2525
showLabel = true,
2626
tooltipPosition = 'bottom',
2727
}) => {
28-
const shortcutCommand = isMacOS() ? '⌘' : 'Ctrl';
28+
const getModifierSymbol = (modifierType: ModifierType = 'system') => {
29+
switch (modifierType) {
30+
case 'none':
31+
return '';
32+
case 'alt':
33+
return 'Alt';
34+
case 'system':
35+
default:
36+
return isMacOS() ? '⌘' : 'Ctrl';
37+
}
38+
};
39+
40+
const shortcutCommand = getModifierSymbol(shortcutOptions?.modifierType);
41+
2942
const showTooltip = shortcutOptions && !disabled;
30-
const tooltipText = shortcutOptions?.noModifier
31-
? `${shortcutOptions.targetKeyLabel}`
32-
: `(${shortcutCommand} + ${shortcutOptions?.targetKeyLabel})`;
43+
const tooltipText =
44+
shortcutOptions &&
45+
`(${
46+
shortcutOptions.modifierType === 'none'
47+
? shortcutOptions.targetKeyLabel
48+
: `${shortcutCommand} + ${shortcutOptions.targetKeyLabel}`
49+
})`;
3350

3451
const tooltipPositionClass =
3552
tooltipPosition === 'top' ? classes.tooltipTop : classes.tooltipBottom;

src/common/shortcut/shortcut.const.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,21 @@ export const SHORTCUTS: Shortcut = {
1010
id: 'add-collection-button-shortcut',
1111
targetKey: ['c'],
1212
targetKeyLabel: 'Collection "C"',
13-
noModifier: true,
13+
modifierType: 'none',
1414
},
1515
addRelation: {
1616
description: 'Add Relation',
1717
id: 'add-relation-button-shortcut',
1818
targetKey: ['r'],
1919
targetKeyLabel: 'Relation "R"',
20-
noModifier: true,
20+
modifierType: 'none',
2121
},
2222
delete: {
2323
description: 'Delete',
2424
id: 'delete-button-shortcut',
2525
targetKey: ['backspace'],
2626
targetKeyLabel: 'Backspace',
27-
noModifier: true,
27+
modifierType: 'none',
2828
},
2929
export: {
3030
description: 'Export',
@@ -37,6 +37,7 @@ export const SHORTCUTS: Shortcut = {
3737
id: 'new-button-shortcut',
3838
targetKey: ['n'],
3939
targetKeyLabel: 'N',
40+
modifierType: 'alt',
4041
},
4142
open: {
4243
description: 'Open',

src/common/shortcut/shortcut.hook.spec.tsx

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
import { renderHook } from '@testing-library/react';
22
import useShortcut from './shortcut.hook';
33
import { vi } from 'vitest';
4+
import { useModalDialogContext } from '@/core/providers';
5+
6+
vi.mock('@/core/providers', () => ({
7+
useModalDialogContext: vi.fn(),
8+
}));
49

510
describe('useShortcut', () => {
611
let targetKey: string[];
712
let callback: () => void;
813

914
beforeEach(() => {
15+
vi.mocked(useModalDialogContext).mockReturnValue({
16+
modalDialog: {
17+
isOpen: false,
18+
selectedComponent: null,
19+
title: '',
20+
},
21+
openModal: vi.fn(),
22+
closeModal: vi.fn(),
23+
});
24+
1025
targetKey = ['a'];
1126
callback = vi.fn();
1227

@@ -110,51 +125,51 @@ describe('useShortcut', () => {
110125
expect(callback).not.toHaveBeenCalled();
111126
});
112127

113-
it('should call the callback when noModifier is true and only the key is pressed', () => {
128+
it('should call callback with alt modifier', () => {
114129
renderHook(() =>
115130
useShortcut({
116131
targetKey,
117132
callback,
118-
noModifier: true,
133+
modifierType: 'alt',
119134
})
120135
);
121136

122137
const event = new KeyboardEvent('keydown', {
123138
key: 'a',
124139
code: 'KeyA',
140+
altKey: true,
125141
});
126142

127143
window.dispatchEvent(event);
128-
129144
expect(callback).toHaveBeenCalled();
130145
});
131146

132-
it('should not call the callback when noModifier is true and modifier is pressed', () => {
147+
it('should not call callback if other modifiers are pressed with alt', () => {
133148
renderHook(() =>
134149
useShortcut({
135150
targetKey,
136151
callback,
137-
noModifier: true,
152+
modifierType: 'alt',
138153
})
139154
);
140155

141156
const event = new KeyboardEvent('keydown', {
142157
key: 'a',
143158
code: 'KeyA',
144-
ctrlKey: true,
159+
altKey: true,
160+
ctrlKey: true, // No debería funcionar con otros modificadores
145161
});
146162

147163
window.dispatchEvent(event);
148-
149164
expect(callback).not.toHaveBeenCalled();
150165
});
151166

152-
it('should not call the callback when noModifier is false and no modifier is pressed', () => {
167+
it('should call callback with no modifiers', () => {
153168
renderHook(() =>
154169
useShortcut({
155170
targetKey,
156171
callback,
157-
noModifier: false,
172+
modifierType: 'none',
158173
})
159174
);
160175

@@ -164,7 +179,53 @@ describe('useShortcut', () => {
164179
});
165180

166181
window.dispatchEvent(event);
182+
expect(callback).toHaveBeenCalled();
183+
});
167184

185+
it('should not call callback if any modifier is pressed', () => {
186+
renderHook(() =>
187+
useShortcut({
188+
targetKey,
189+
callback,
190+
modifierType: 'none',
191+
})
192+
);
193+
194+
const event = new KeyboardEvent('keydown', {
195+
key: 'a',
196+
code: 'KeyA',
197+
altKey: true,
198+
});
199+
200+
window.dispatchEvent(event);
201+
expect(callback).not.toHaveBeenCalled();
202+
});
203+
204+
it('should not call callback when modal is open', () => {
205+
vi.mocked(useModalDialogContext).mockReturnValueOnce({
206+
modalDialog: {
207+
isOpen: true,
208+
selectedComponent: null,
209+
title: '',
210+
},
211+
openModal: vi.fn(),
212+
closeModal: vi.fn(),
213+
});
214+
215+
renderHook(() =>
216+
useShortcut({
217+
targetKey,
218+
callback,
219+
modifierType: 'none',
220+
})
221+
);
222+
223+
const event = new KeyboardEvent('keydown', {
224+
key: 'r',
225+
code: 'KeyR',
226+
});
227+
228+
window.dispatchEvent(event);
168229
expect(callback).not.toHaveBeenCalled();
169230
});
170231
});

0 commit comments

Comments
 (0)