Skip to content

Commit

Permalink
Add +/- shortcuts for zoom toggling
Browse files Browse the repository at this point in the history
Replaces the mouse wheel action in 2.3. There are problems with preventing
default on it: see facebook/react#14856

Using a keyboard shortcut seems better anyway since not everyone has a mouse
wheel.
  • Loading branch information
Chris Klimas committed May 1, 2022
1 parent b7df794 commit 4050a19
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 18 deletions.
35 changes: 33 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"react-draggable": "^4.4.3",
"react-helmet": "^6.1.0",
"react-hook-thunk-reducer": "^0.2.4",
"react-hotkeys-hook": "^3.4.4",
"react-i18next": "^11.8.9",
"react-popper": "^2.2.4",
"react-router-dom": "^5.2.0",
Expand Down
9 changes: 9 additions & 0 deletions src/routes/story-edit/__tests__/story-edit-route.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import {
StoryInspector
} from '../../../test-util';
import {InnerStoryEditRoute} from '../story-edit-route';
import {useZoomShortcuts} from '../use-zoom-shortcuts';

jest.mock('../toolbar/story-edit-toolbar');
jest.mock('../use-zoom-shortcuts');
jest.mock('../../../components/passage/passage-map/passage-map');

const TestStoryEditRoute: React.FC = () => {
Expand All @@ -35,6 +37,8 @@ const TestStoryEditRoute: React.FC = () => {
};

describe('<StoryEditRoute>', () => {
const useZoomShortcutsMock = useZoomShortcuts as jest.Mock;

async function renderComponent(
story: Story,
contexts?: FakeStateProviderProps
Expand Down Expand Up @@ -114,6 +118,11 @@ describe('<StoryEditRoute>', () => {
expect(screen.getAllByTestId(/^passage-/).length).toBe(1);
});

it('sets up zoom keyboard shortcuts', async () => {
await renderComponent(fakeStory());
expect(useZoomShortcutsMock).toBeCalled();
});

it('is accessible', async () => {
const {container} = await renderComponent(fakeStory());

Expand Down
139 changes: 139 additions & 0 deletions src/routes/story-edit/__tests__/use-zoom-shortcuts.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {cleanup, fireEvent, render, screen} from '@testing-library/react';
import {Story, useStoriesContext} from '../../../store/stories';
import {FakeStateProvider, fakeStory, StoryInspector} from '../../../test-util';
import {useZoomShortcuts} from '../use-zoom-shortcuts';

const TestZoomShortcuts: React.FC = () => {
const {stories} = useStoriesContext();

useZoomShortcuts(stories[0]);
return <div>test zoom shortcut</div>;
};

describe('useZoomShortcuts()', () => {
function renderComponent(story: Story) {
return render(
<FakeStateProvider stories={[story]}>
<TestZoomShortcuts />
<StoryInspector />
</FakeStateProvider>
);
}

describe('when the + key is pressed', () => {
it('increases the story zoom', async () => {
const story = fakeStory();

story.zoom = 0.3;
renderComponent(story);
fireEvent.keyUp(document.body, {
key: '=',
code: '=',
keyCode: 187,
charCode: 187
});
expect(screen.getByTestId('story-inspector-default').dataset.zoom).toBe(
'0.6'
);
cleanup();
story.zoom = 0.6;
renderComponent(story);
fireEvent.keyUp(document.body, {
key: '=',
code: '=',
keyCode: 187,
charCode: 187
});
expect(screen.getByTestId('story-inspector-default').dataset.zoom).toBe(
'1'
);
});

it('does not increase the story zoom if it is 1', () => {
const story = fakeStory();

story.zoom = 1;
renderComponent(story);
fireEvent.keyUp(document.body, {
key: '=',
code: '=',
keyCode: 187,
charCode: 187
});
expect(screen.getByTestId('story-inspector-default').dataset.zoom).toBe(
'1'
);
});
});

describe('when the - key is pressed', () => {
it('decreases the story zoom', () => {
const story = fakeStory();

story.zoom = 1;
renderComponent(story);
fireEvent.keyUp(document.body, {
key: '-',
code: '-',
keyCode: 189,
charCode: 189
});
expect(screen.getByTestId('story-inspector-default').dataset.zoom).toBe(
'0.6'
);
cleanup();
story.zoom = 0.6;
renderComponent(story);
fireEvent.keyUp(document.body, {
key: '-',
code: '-',
keyCode: 189,
charCode: 189
});
expect(screen.getByTestId('story-inspector-default').dataset.zoom).toBe(
'0.3'
);
});

it('does not decrease the story zoom if it is 0.3', () => {
const story = fakeStory();

story.zoom = 0.3;
renderComponent(story);
fireEvent.keyUp(document.body, {
key: '-',
code: '-',
keyCode: 189,
charCode: 189
});
expect(screen.getByTestId('story-inspector-default').dataset.zoom).toBe(
'0.3'
);
});
});

it('does not react to keydown events', () => {
const story = fakeStory();

story.zoom = 0.6;
renderComponent(story);
fireEvent.keyDown(document.body, {
key: '-',
code: '-',
keyCode: 189,
charCode: 189
});
expect(screen.getByTestId('story-inspector-default').dataset.zoom).toBe(
'0.6'
);
fireEvent.keyDown(document.body, {
key: '=',
code: '=',
keyCode: 187,
charCode: 187
});
expect(screen.getByTestId('story-inspector-default').dataset.zoom).toBe(
'0.6'
);
});
});
2 changes: 2 additions & 0 deletions src/routes/story-edit/story-edit-route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import './story-edit-route.css';
import {ZoomButtons} from './zoom-buttons';
import {DocumentTitle} from '../../components/document-title/document-title';
import {useZoomTransition} from './use-zoom-transition';
import {useZoomShortcuts} from './use-zoom-shortcuts';

export const InnerStoryEditRoute: React.FC = () => {
const [inited, setInited] = React.useState(false);
Expand All @@ -36,6 +37,7 @@ export const InnerStoryEditRoute: React.FC = () => {
const {dispatch: undoableStoriesDispatch, stories} =
useUndoableStoriesContext();
const story = storyWithId(stories, storyId);
useZoomShortcuts(story);

const selectedPassages = React.useMemo(
() => story.passages.filter(passage => passage.selected),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ describe('<DeletePassagesButton>', () => {
expect(passages[0].dataset.id).toBe(story.passages[2].id);
});

// To a degree, this is actually testing react-hotkeys-hook. But it makes
// sense to test this behavior since deleting passages is scary.

describe('when a text input is not focused', () => {
let story: Story;

Expand Down
18 changes: 2 additions & 16 deletions src/routes/story-edit/toolbar/passage/delete-passages-button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {IconTrash} from '@tabler/icons';
import 'element-closest';
import * as React from 'react';
import {useHotkeys} from 'react-hotkeys-hook';
import {useTranslation} from 'react-i18next';
import {IconButton} from '../../../../components/control/icon-button';
import {deletePassages, Passage, Story} from '../../../../store/stories';
Expand Down Expand Up @@ -30,22 +31,7 @@ export const DeletePassagesButton: React.FC<
);
}, [dispatch, passages, story]);

// Trigger on the delete or backspace key, but only if the user isn't editing
// text. (This also works if the user has a CodeMirror instance focused.)

React.useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (
['Backspace', 'Delete'].includes(event.key) &&
!(event.target as HTMLElement)?.closest('input, textarea')
) {
handleClick();
}
};

document.addEventListener('keydown', listener);
return () => document.removeEventListener('keydown', listener);
}, [handleClick]);
useHotkeys('Backspace,Delete', handleClick, [handleClick]);

return (
<IconButton
Expand Down
39 changes: 39 additions & 0 deletions src/routes/story-edit/use-zoom-shortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {useHotkeys} from 'react-hotkeys-hook';
import {Story, updateStory, useStoriesContext} from '../../store/stories';

export function useZoomShortcuts(story: Story) {
const {dispatch, stories} = useStoriesContext();

useHotkeys(
'-',
() => {
switch (story.zoom) {
case 1:
dispatch(updateStory(stories, story, {zoom: 0.6}));
break;
case 0.6:
dispatch(updateStory(stories, story, {zoom: 0.3}));
break;
// Do nothing if zoom is 0.3
}
},
{keydown: false, keyup: true},
[dispatch, stories, story]
);
useHotkeys(
'=',
() => {
switch (story.zoom) {
case 0.3:
dispatch(updateStory(stories, story, {zoom: 0.6}));
break;
case 0.6:
dispatch(updateStory(stories, story, {zoom: 1}));
break;
// Do nothing if zoom is 1
}
},
{keydown: false, keyup: true},
[dispatch, stories, story]
);
}

0 comments on commit 4050a19

Please sign in to comment.