Skip to content

Commit bdec6e4

Browse files
authored
Keyboard shortcuts UI (#201)
* mostly styled * escape * improve reference stability * add dot shortcut * diff options shortcuts
1 parent 077a9cb commit bdec6e4

File tree

9 files changed

+249
-57
lines changed

9 files changed

+249
-57
lines changed

ts/CodeDiffContainer.tsx

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -157,35 +157,39 @@ function lengthOrZero(data: unknown[] | string | null | undefined) {
157157
function FileDiff(props: FileDiffProps) {
158158
const {pathBefore, pathAfter, contentsBefore, contentsAfter, diffOps} = props;
159159
// build the diff view and add it to the current DOM
160-
const opts: Partial<PatchOptions> = {
161-
// set the display titles for each resource
162-
beforeName: pathBefore || '(none)',
163-
afterName: pathAfter || '(none)',
164-
// TODO: thread through minJumpSize
165-
};
166-
167-
// First guess a language based on the file name.
168-
// Fall back to guessing based on the contents of the longer version.
169-
const path = pathBefore || pathAfter;
170-
let language = guessLanguageUsingFileName(path);
171160

172161
const lastOp = diffOps[diffOps.length - 1];
173162
const numLines = Math.max(lastOp.before[1], lastOp.after[1]);
174163

175-
if (
176-
!language &&
177-
!HIGHLIGHT_BLACKLIST.includes(extractFilename(path)) &&
178-
numLines < GIT_CONFIG.webdiff.maxLinesForSyntax
179-
) {
180-
let byLength = [contentsBefore, contentsAfter];
181-
if (contentsAfter && lengthOrZero(contentsAfter) > lengthOrZero(contentsBefore)) {
182-
byLength = [byLength[1], byLength[0]];
164+
// First guess a language based on the file name.
165+
// Fall back to guessing based on the contents of the longer version.
166+
const path = pathBefore || pathAfter;
167+
const language = React.useMemo(() => {
168+
let language = guessLanguageUsingFileName(path);
169+
if (
170+
!language &&
171+
!HIGHLIGHT_BLACKLIST.includes(extractFilename(path)) &&
172+
numLines < GIT_CONFIG.webdiff.maxLinesForSyntax
173+
) {
174+
let byLength = [contentsBefore, contentsAfter];
175+
if (contentsAfter && lengthOrZero(contentsAfter) > lengthOrZero(contentsBefore)) {
176+
byLength = [byLength[1], byLength[0]];
177+
}
178+
language = byLength[0] ? guessLanguageUsingContents(byLength[0]) ?? null : null;
183179
}
184-
language = byLength[0] ? guessLanguageUsingContents(byLength[0]) ?? null : null;
185-
}
186-
if (language) {
187-
opts.language = language;
188-
}
180+
return language;
181+
}, [contentsAfter, contentsBefore, numLines, path]);
182+
183+
const opts = React.useMemo(
184+
(): Partial<PatchOptions> => ({
185+
// set the display titles for each resource
186+
beforeName: pathBefore || '(none)',
187+
afterName: pathAfter || '(none)',
188+
language,
189+
// TODO: thread through minJumpSize
190+
}),
191+
[language, pathAfter, pathBefore],
192+
);
189193

190194
return (
191195
<div className="diff">

ts/DiffOptions.tsx

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import React from 'react';
22

33
import {DiffAlgorithm, DiffOptions, encodeDiffOptions} from './diff-options';
4+
import {PageCover} from './codediff/PageCover';
5+
import {isLegitKeypress} from './file_diff';
46

57
export interface Props {
68
options: Partial<DiffOptions>;
79
setOptions: (newOptions: Partial<DiffOptions>) => void;
10+
isVisible: boolean;
11+
setIsVisible: (isVisible: boolean) => void;
812
}
913

1014
const gearStyle: React.CSSProperties = {
@@ -28,18 +32,9 @@ const closeButtonStyle: React.CSSProperties = {
2832
cursor: 'pointer',
2933
};
3034

31-
const pageCoverStyle: React.CSSProperties = {
32-
position: 'absolute',
33-
zIndex: 1,
34-
left: 0,
35-
top: 0,
36-
right: 0,
37-
bottom: 0,
38-
};
39-
4035
const popupStyle: React.CSSProperties = {
4136
position: 'fixed',
42-
zIndex: 2,
37+
zIndex: 3,
4338
right: 8,
4439
border: '1px solid #ddd',
4540
borderRadius: 4,
@@ -54,11 +49,10 @@ const popupStyle: React.CSSProperties = {
5449
};
5550

5651
export function DiffOptionsControl(props: Props) {
57-
const {options, setOptions} = props;
58-
const [isPopupVisible, setIsPopupVisible] = React.useState(false);
52+
const {options, setOptions, isVisible, setIsVisible} = props;
5953

6054
const togglePopup = () => {
61-
setIsPopupVisible(oldVal => !oldVal);
55+
setIsVisible(!isVisible);
6256
};
6357
const toggleIgnoreAllSpace = () => {
6458
setOptions({...options, ignoreAllSpace: !options.ignoreAllSpace});
@@ -76,16 +70,31 @@ export function DiffOptionsControl(props: Props) {
7670
setOptions({...options, diffAlgorithm: e.currentTarget.value as DiffAlgorithm});
7771
};
7872

73+
React.useEffect(() => {
74+
const handleKeydown = (e: KeyboardEvent) => {
75+
if (!isLegitKeypress(e)) return;
76+
if (e.code == 'KeyW') {
77+
setOptions({...options, ignoreAllSpace: !options.ignoreAllSpace});
78+
} else if (e.code == 'KeyB') {
79+
setOptions({...options, ignoreSpaceChange: !options.ignoreSpaceChange});
80+
}
81+
};
82+
document.addEventListener('keydown', handleKeydown);
83+
return () => {
84+
document.removeEventListener('keydown', handleKeydown);
85+
};
86+
}, [options, setOptions]);
87+
7988
const diffOptsStr = encodeDiffOptions(options).join(' ');
8089

8190
return (
8291
<>
8392
<button style={gearStyle} onClick={togglePopup}>
8493
8594
</button>
86-
{isPopupVisible ? (
95+
{isVisible ? (
8796
<>
88-
<div style={pageCoverStyle} onClick={togglePopup}></div>
97+
<PageCover onClick={togglePopup} />
8998
<div style={popupStyle}>
9099
<button style={closeButtonStyle} onClick={togglePopup}>
91100

ts/DiffView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ export interface Props {
1212
imageDiffMode: ImageDiffMode;
1313
pdiffMode: PerceptualDiffMode;
1414
diffOptions: Partial<DiffOptions>;
15-
changeImageDiffModeHandler: (mode: ImageDiffMode) => void;
16-
changePDiffMode: (pdiffMode: PerceptualDiffMode) => void;
15+
changeImageDiffMode: (mode: ImageDiffMode) => void;
16+
changePDiffMode: React.Dispatch<React.SetStateAction<PerceptualDiffMode>>;
1717
changeDiffOptions: (options: Partial<DiffOptions>) => void;
1818
}
1919

ts/ImageDiff.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ import {isOneSided, isSameSizeImagePair} from './utils';
88
import {ImageSideBySide} from './ImageSideBySide';
99
import {ImageBlinker} from './ImageBlinker';
1010
import {ImageOnionSkin, ImageSwipe} from './ImageSwipe';
11+
import {isLegitKeypress} from './file_diff';
1112

1213
declare const HAS_IMAGE_MAGICK: boolean;
1314

1415
export interface Props {
1516
filePair: ImageFilePair;
1617
imageDiffMode: ImageDiffMode;
1718
pdiffMode: PerceptualDiffMode;
18-
changeImageDiffModeHandler: (mode: ImageDiffMode) => void;
19-
changePDiffMode: (pdiffMode: PerceptualDiffMode) => void;
19+
changeImageDiffMode: (mode: ImageDiffMode) => void;
20+
changePDiffMode: React.Dispatch<React.SetStateAction<PerceptualDiffMode>>;
2021
}
2122

2223
export interface ImageDiffProps {
@@ -25,6 +26,8 @@ export interface ImageDiffProps {
2526
shrinkToFit: boolean;
2627
}
2728

29+
const PDIFF_MODES: PerceptualDiffMode[] = ['off', 'bbox', 'pixels'];
30+
2831
/** A diff between two images. */
2932
export function ImageDiff(props: Props) {
3033
const [shrinkToFit, setShrinkToFit] = React.useState(true);
@@ -33,7 +36,7 @@ export function ImageDiff(props: Props) {
3336
setShrinkToFit(e.target.checked);
3437
};
3538

36-
const {changePDiffMode, pdiffMode} = props;
39+
const {changePDiffMode, pdiffMode, changeImageDiffMode} = props;
3740
let {imageDiffMode} = props;
3841
const pair = props.filePair;
3942
if (isOneSided(pair)) {
@@ -75,6 +78,24 @@ export function ImageDiff(props: Props) {
7578
};
7679
}, [shrinkToFit, forceUpdate]);
7780

81+
// TODO: switch to useKey() or some such
82+
React.useEffect(() => {
83+
const handleKeydown = (e: KeyboardEvent) => {
84+
if (!isLegitKeypress(e)) return;
85+
if (e.code == 'KeyS') {
86+
changeImageDiffMode('side-by-side');
87+
} else if (e.code == 'KeyB') {
88+
changeImageDiffMode('blink');
89+
} else if (e.code == 'KeyP') {
90+
changePDiffMode(mode => PDIFF_MODES[(PDIFF_MODES.indexOf(mode) + 1) % 3]);
91+
}
92+
};
93+
document.addEventListener('keydown', handleKeydown);
94+
return () => {
95+
document.removeEventListener('keydown', handleKeydown);
96+
};
97+
}, [changeImageDiffMode, changePDiffMode]);
98+
7899
const component = {
79100
'side-by-side': ImageSideBySide,
80101
blink: ImageBlinker,

ts/ImageDiffModeSelector.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export type ImageDiffMode = (typeof IMAGE_DIFF_MODES)[number];
1010
export interface Props {
1111
filePair: FilePair;
1212
imageDiffMode: ImageDiffMode;
13-
changeImageDiffModeHandler: (imageDiffMode: ImageDiffMode) => void;
13+
changeImageDiffMode: (imageDiffMode: ImageDiffMode) => void;
1414
}
1515

1616
/** A widget to toggle between image diff modes (blink or side-by-side). */
@@ -27,7 +27,7 @@ export function ImageDiffModeSelector(props: Props) {
2727
<a
2828
href="#"
2929
onClick={() => {
30-
props.changeImageDiffModeHandler(val);
30+
props.changeImageDiffMode(val);
3131
}}>
3232
{inner}
3333
</a>

ts/Root.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,20 @@ import {isLegitKeypress} from './file_diff';
88
import {ImageDiffMode} from './ImageDiffModeSelector';
99
import {filePairDisplayName} from './utils';
1010
import {DiffOptionsControl} from './DiffOptions';
11+
import {KeyboardShortcuts} from './codediff/KeyboardShortcuts';
1112

1213
declare const pairs: FilePair[];
1314
declare const initialIdx: number;
1415

1516
type Props = RouteComponentProps<{index?: string}>;
1617

17-
const PDIFF_MODES: PerceptualDiffMode[] = ['off', 'bbox', 'pixels'];
18-
1918
// Webdiff application root.
2019
export function Root(props: Props) {
2120
const [pdiffMode, setPDiffMode] = React.useState<PerceptualDiffMode>('off');
2221
const [imageDiffMode, setImageDiffMode] = React.useState<ImageDiffMode>('side-by-side');
2322
const [diffOptions, setDiffOptions] = React.useState<Partial<DiffOptions>>({});
23+
const [showKeyboardHelp, setShowKeyboardHelp] = React.useState(false);
24+
const [showOptions, setShowOptions] = React.useState(false);
2425

2526
const history = useHistory();
2627
const selectIndex = React.useCallback(
@@ -48,31 +49,43 @@ export function Root(props: Props) {
4849
if (idx < pairs.length - 1) {
4950
selectIndex(idx + 1);
5051
}
51-
} else if (e.code == 'KeyS') {
52-
setImageDiffMode('side-by-side');
53-
} else if (e.code == 'KeyB') {
54-
setImageDiffMode('blink');
55-
} else if (e.code == 'KeyP') {
56-
setPDiffMode(PDIFF_MODES[(PDIFF_MODES.indexOf(pdiffMode) + 1) % 3]);
52+
} else if (e.code === 'Slash' && e.shiftKey) {
53+
setShowKeyboardHelp(val => !val);
54+
} else if (e.code === 'Escape') {
55+
setShowKeyboardHelp(false);
56+
} else if (e.code === 'Period') {
57+
setShowOptions(val => !val);
5758
}
5859
};
5960
document.addEventListener('keydown', handleKeydown);
6061
return () => {
6162
document.removeEventListener('keydown', handleKeydown);
6263
};
63-
}, [idx, pdiffMode, selectIndex, setImageDiffMode, setPDiffMode]);
64+
}, [idx, selectIndex]);
6465

6566
return (
6667
<div>
67-
<DiffOptionsControl options={diffOptions} setOptions={setDiffOptions} />
68+
<DiffOptionsControl
69+
options={diffOptions}
70+
setOptions={setDiffOptions}
71+
isVisible={showOptions}
72+
setIsVisible={setShowOptions}
73+
/>
6874
<FileSelector selectedFileIndex={idx} filePairs={pairs} fileChangeHandler={selectIndex} />
75+
{showKeyboardHelp ? (
76+
<KeyboardShortcuts
77+
onClose={() => {
78+
setShowKeyboardHelp(false);
79+
}}
80+
/>
81+
) : null}
6982
<DiffView
7083
key={`diff-${idx}`}
7184
thinFilePair={filePair}
7285
imageDiffMode={imageDiffMode}
7386
pdiffMode={pdiffMode}
7487
diffOptions={diffOptions}
75-
changeImageDiffModeHandler={setImageDiffMode}
88+
changeImageDiffMode={setImageDiffMode}
7689
changePDiffMode={setPDiffMode}
7790
changeDiffOptions={setDiffOptions}
7891
/>

0 commit comments

Comments
 (0)