Skip to content

Commit

Permalink
✨ tab traverse shortcuts
Browse files Browse the repository at this point in the history
Signed-off-by: Marc Nuri <[email protected]>
  • Loading branch information
manusa committed Nov 1, 2022
1 parent c318476 commit f9a4e21
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 33 deletions.
14 changes: 8 additions & 6 deletions docs/Keyboard-shortcuts.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Keyboard Shortcuts

| Key combination | Description |
|------------------------------------------------------------|---------------------|
| `Ctrl+r` `Cmd+r` `F5` | Reload current tab. |
| `Ctrl++` `Cmd++` <br /> `Ctrl+ScrollUp` `Cmd+ScrollUp` | Zoom in. |
| `Ctrl+-` `Cmd+-` <br /> `Ctrl+ScrollDown` `Cmd+ScrollDown` | Zoom out. |
| `Ctrl+0` `Cmd+0` | Reset zoom. |
| Key combination | Description |
|------------------------------------------------------------|--------------------------------|
| `Ctrl+r` `Cmd+r` `F5` | Reload current tab. |
| `Ctrl++` `Cmd++` <br /> `Ctrl+ScrollUp` `Cmd+ScrollUp` | Zoom in. |
| `Ctrl+-` `Cmd+-` <br /> `Ctrl+ScrollDown` `Cmd+ScrollDown` | Zoom out. |
| `Ctrl+0` `Cmd+0` | Reset zoom. |
| `Ctrl+Tab` | Jump to the next open tab. |
| `Ctrl+Shift+Tab` | Jump to the previous open tab. |
2 changes: 2 additions & 0 deletions src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ const APP_EVENTS = {
canNotify: 'canNotify',
tabsReady: 'tabsReady',
tabReorder: 'tabReorder',
tabTraverseNext: 'tabTraverseNext',
tabTraversePrevious: 'tabTraversePrevious',
zoomIn: 'zoomIn',
zoomOut: 'zoomOut',
zoomReset: 'zoomReset'
Expand Down
23 changes: 23 additions & 0 deletions src/main/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,29 @@ describe('Main module test suite', () => {
// Then
expect(event.sender.reloadIgnoringCache).toHaveBeenCalledTimes(1);
});
describe('handleTabTraverse', () => {
beforeEach(() => {
jest.spyOn(tabManagerModule, 'getTab').mockImplementation();
});
test('tabTraverseNext', () => {
jest.spyOn(tabManagerModule, 'getNextTab')
.mockImplementation(() => 'nextTabId');
main.init();
// When
mockIpc.listeners.tabTraverseNext();
// Then
expect(tabManagerModule.getTab).toHaveBeenCalledWith('nextTabId');
});
test('tabTraversePrevious', () => {
jest.spyOn(tabManagerModule, 'getPreviousTab')
.mockImplementation(() => 'previousTabId');
main.init();
// When
mockIpc.listeners.tabTraversePrevious();
// Then
expect(tabManagerModule.getTab).toHaveBeenCalledWith('previousTabId');
});
});
test('handleZoomIn', () => {
const event = {sender: {
getZoomFactor: jest.fn(() => 0),
Expand Down
8 changes: 8 additions & 0 deletions src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ const handleMainWindowResize = event => {

const handleTabReload = event => event.sender.reloadIgnoringCache();

const handleTabTraverse = getTabIdFunction => () => {
const tabId = getTabIdFunction();
tabContainer.webContents.send(APP_EVENTS.activateTabInContainer, {tabId});
activateTab(tabId);
};

const handleZoomIn = event => event.sender.setZoomFactor(event.sender.getZoomFactor() + 0.1);

const handleZoomOut = event => {
Expand Down Expand Up @@ -139,6 +145,8 @@ const initTabListener = () => {
});
ipc.on(APP_EVENTS.reload, handleTabReload);
ipc.on(APP_EVENTS.tabReorder, handleTabReorder);
ipc.on(APP_EVENTS.tabTraverseNext, handleTabTraverse(tabManager.getNextTab));
ipc.on(APP_EVENTS.tabTraversePrevious, handleTabTraverse(tabManager.getPreviousTab));
ipc.on(APP_EVENTS.zoomIn, handleZoomIn);
ipc.on(APP_EVENTS.zoomOut, handleZoomOut);
ipc.on(APP_EVENTS.zoomReset, handleZoomReset);
Expand Down
45 changes: 45 additions & 0 deletions src/tab-manager/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,51 @@ describe('Tab Manager module test suite', () => {
expect(result).toBeNull();
});
});
describe('getTabTraverse', () => {
beforeEach(() => {
tabManager.addTabs({send: jest.fn()})([
{id: 'A'},
{id: 'B'},
{id: 'C'}
]);
});
describe('getNextTab with tabs [A, B, C]', () => {
test('with currentTab = A, should return B', () => {
// Given
tabManager.setActiveTab('A');
// When
const nextTab = tabManager.getNextTab();
// Then
expect(nextTab).toBe('B');
});
test('with currentTab = C, should return A', () => {
// Given
tabManager.setActiveTab('C');
// When
const nextTab = tabManager.getNextTab();
// Then
expect(nextTab).toBe('A');
});
});
describe('getPreviousTab', () => {
test('with currentTab = B, should return A', () => {
// Given
tabManager.setActiveTab('B');
// When
const nextTab = tabManager.getPreviousTab();
// Then
expect(nextTab).toBe('A');
});
test('with currentTab = A, should return C', () => {
// Given
tabManager.setActiveTab('A');
// When
const nextTab = tabManager.getPreviousTab();
// Then
expect(nextTab).toBe('C');
});
});
});
describe('addTabs', () => {
test('webPreferences is sandboxed and has no node integration', () => {
// When
Expand Down
26 changes: 21 additions & 5 deletions src/tab-manager/__tests__/preload.keyboard-shortcuts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@ describe('Browser Keyboard Shortcuts test suite', () => {
let mockIpcRenderer;
let browserKeyboardShortcuts;
beforeEach(() => {
global.APP_EVENTS = {
reload: 'reload',
zoomIn: 'zoomIn',
zoomOut: 'zoomOut'
};
global.APP_EVENTS = require('../../constants').APP_EVENTS;
mockIpcRenderer = {
send: jest.fn()
};
Expand Down Expand Up @@ -70,6 +66,15 @@ describe('Browser Keyboard Shortcuts test suite', () => {
expect(mockIpcRenderer.send).toHaveBeenCalledTimes(1);
expect(mockIpcRenderer.send).toHaveBeenCalledWith('reload');
});
test('ctrl+tab, should send tabTraverseNext event', () => {
// Given
browserKeyboardShortcuts.initKeyboardShortcuts();
// When
window.dispatchEvent(new KeyboardEvent('keyup', {key: 'Tab', ctrlKey: true}));
// Then
expect(mockIpcRenderer.send).toHaveBeenCalledTimes(1);
expect(mockIpcRenderer.send).toHaveBeenCalledWith('tabTraverseNext');
});
});
describe('Command modified events', () => {
test('cmd+R, should send reload app event', () => {
Expand All @@ -82,6 +87,17 @@ describe('Browser Keyboard Shortcuts test suite', () => {
expect(mockIpcRenderer.send).toHaveBeenCalledWith('reload');
});
});
describe('Control+Shift modified events', () => {
test('ctrl+shift+tab, should send tabTraversePrevious event', () => {
// Given
browserKeyboardShortcuts.initKeyboardShortcuts();
// When
window.dispatchEvent(new KeyboardEvent('keyup', {key: 'Tab', ctrlKey: true, shiftKey: true}));
// Then
expect(mockIpcRenderer.send).toHaveBeenCalledTimes(1);
expect(mockIpcRenderer.send).toHaveBeenCalledWith('tabTraversePrevious');
});
});
describe('Mouse wheel events', () => {
test('ctrl+scrollUp, should send zoomIn event', () => {
// Given
Expand Down
16 changes: 15 additions & 1 deletion src/tab-manager/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,20 @@ const setActiveTab = tabId => {
settings.updateSettings({activeTab});
};

const getTabTraverse = operation => () => {
const tabIds = Object.keys(tabs);
const idx = operation(tabIds.indexOf(getActiveTab()));
if (idx < 0) {
return tabIds[tabIds.length - 1];
} else if (idx >= tabIds.length) {
return tabIds[0];
}
return tabIds[idx];
};

const getNextTab = getTabTraverse(idx => idx + 1);
const getPreviousTab = getTabTraverse(idx => idx - 1);

const removeAll = () => {
Object.values(tabs).forEach(browserView => browserView.webContents.destroy());
Object.keys(tabs).forEach(key => delete tabs[key]);
Expand All @@ -158,5 +172,5 @@ const canNotify = tabId => {
};

module.exports = {
addTabs, getTab, getActiveTab, setActiveTab, canNotify, reload, removeAll
addTabs, getTab, getActiveTab, setActiveTab, getNextTab, getPreviousTab, canNotify, reload, removeAll
};
52 changes: 31 additions & 21 deletions src/tab-manager/preload.keyboard-shortcuts.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,50 @@
/* eslint-disable no-undef */
const {ipcRenderer} = require('electron');

const codeActionMap = {
F5: APP_EVENTS.reload
const triggerForActionMap = actionMap => ({key}) => {
if (actionMap[key]) {
ipcRenderer.send(actionMap[key]);
}
};

const controlCodeActionMap = {
const triggerCode = event => triggerForActionMap({
F5: APP_EVENTS.reload
})(event);

const triggerControlCode = event => triggerForActionMap({
r: APP_EVENTS.reload,
R: APP_EVENTS.reload,
'+': APP_EVENTS.zoomIn,
'-': APP_EVENTS.zoomOut,
0: APP_EVENTS.zoomReset
};
0: APP_EVENTS.zoomReset,
Tab: APP_EVENTS.tabTraverseNext
})(event);

const commandCodeActionMap = {
const triggerControlShiftCode = event => triggerForActionMap({
Tab: APP_EVENTS.tabTraversePrevious
})(event);

const triggerCommandCode = event => triggerForActionMap({
r: APP_EVENTS.reload,
R: APP_EVENTS.reload
};
})(event);

const triggerForActionMap = actionMap => key => {
if (actionMap[key]) {
ipcRenderer.send(actionMap[key]);
}
};

const isPlain = event => event.ctrlKey === false && event.metaKey === false && event.shiftKey === false;
const isControl = event => event.ctrlKey === true && event.metaKey === false && event.shiftKey === false;
const isControlShift = event => event.ctrlKey === true && event.metaKey === false && event.shiftKey === true;
const isCommand = event => event.ctrlKey === false && event.metaKey === true && event.shiftKey === false;

const initKeyboardShortcuts = () => {
const triggerCodeActionMap = triggerForActionMap(codeActionMap);
const triggerControlCodeActionMap = triggerForActionMap(controlCodeActionMap);
const triggerCommandCodeActionMap = triggerForActionMap(commandCodeActionMap);
window.addEventListener('keyup', event => {
if (event.ctrlKey === false && event.metaKey === false) {
triggerCodeActionMap(event.key);
} else if (event.ctrlKey === true && event.metaKey === false) {
triggerControlCodeActionMap(event.key);
} else if (event.ctrlKey === false && event.metaKey === true) {
triggerCommandCodeActionMap(event.key);
if (isPlain(event)) {
triggerCode(event);
} else if (isControl(event)) {
triggerControlCode(event);
} else if (isControlShift(event)) {
triggerControlShiftCode(event);
} else if (isCommand(event)) {
triggerCommandCode(event);
}
});
window.addEventListener('load', () => {
Expand Down

0 comments on commit f9a4e21

Please sign in to comment.