Skip to content

Commit aa7cc40

Browse files
committed
Make arrow keys work in tables, escape cancel renaming
Issue #46
1 parent abac0c5 commit aa7cc40

2 files changed

Lines changed: 67 additions & 20 deletions

File tree

src/components/tables/common.tsx

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,23 @@ function useTable<TData>(
125125
return [table, columnVisibility, setColumnVisibility, columnOrder, setColumnOrder, columnSizing, sorting];
126126
}
127127

128+
interface HIDEvent {
129+
modKey: boolean,
130+
shiftKey: boolean,
131+
isRmb: boolean,
132+
}
133+
128134
function useSelectHandler<TData>(
129135
table: Table<TData>,
130136
selectedReducer: TableSelectReducer,
131137
getRowId: (row: TData) => string,
132138
setCurrent?: (id: string) => void,
133-
): [number, (event: React.MouseEvent<Element>, index: number, lastIndex: number) => void] {
139+
): [number, (event: HIDEvent, index: number, lastIndex: number) => void] {
134140
const [lastIndex, setLastIndex] = useState(-1);
135141

136-
const onRowClick = useCallback((event: React.MouseEvent<Element>, index: number, lastIndex: number) => {
142+
const onRowClick = useCallback((event: HIDEvent, index: number, lastIndex: number) => {
137143
const rows = table.getRowModel().rows;
138-
event.preventDefault();
139-
const modKey = eventHasModKey(event);
144+
if (index < 0 || index >= rows.length) return;
140145

141146
function genIds() {
142147
const minIndex = Math.min(index, lastIndex);
@@ -148,15 +153,15 @@ function useSelectHandler<TData>(
148153
return ids;
149154
}
150155

151-
if (event.shiftKey && modKey && lastIndex !== -1) {
156+
if (event.shiftKey && event.modKey && lastIndex !== -1) {
152157
const ids = genIds();
153158
selectedReducer({ verb: "add", ids });
154159
} else if (event.shiftKey && lastIndex !== -1) {
155160
const ids = genIds();
156161
selectedReducer({ verb: "set", ids });
157-
} else if (modKey) {
162+
} else if (event.modKey) {
158163
selectedReducer({ verb: "toggle", ids: [getRowId(rows[index].original)] });
159-
} else if (event.button !== 2 || !rows[index].getIsSelected()) {
164+
} else if (!event.isRmb || !rows[index].getIsSelected()) {
160165
selectedReducer({ verb: "set", ids: [getRowId(rows[index].original)] });
161166
}
162167

@@ -258,7 +263,7 @@ function TableRow<TData>(props: {
258263
index: number,
259264
start: number,
260265
lastIndex: number,
261-
onRowClick: (e: React.MouseEvent<Element>, i: number, li: number) => void,
266+
onRowClick: (e: HIDEvent, i: number, li: number) => void,
262267
onRowDoubleClick?: (row: TData) => void,
263268
height: number,
264269
columnSizing: ColumnSizingState,
@@ -272,17 +277,55 @@ function TableRow<TData>(props: {
272277
}
273278
}, [propsDblClick, row.original]);
274279

280+
const { onRowClick, index, lastIndex } = props;
281+
282+
const onMouseEvent = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
283+
onRowClick({
284+
modKey: eventHasModKey(e),
285+
shiftKey: e.shiftKey,
286+
isRmb: e.button === 2,
287+
}, index, lastIndex);
288+
}, [index, lastIndex, onRowClick]);
289+
290+
const ref = useRef<HTMLDivElement>(null);
291+
292+
const onKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
293+
let newIndex = index;
294+
if (e.key === "ArrowDown") {
295+
newIndex += 1;
296+
} else if (e.key === "ArrowUp") {
297+
newIndex -= 1;
298+
} else if (e.key !== "ContextMenu") {
299+
return;
300+
}
301+
e.preventDefault();
302+
if (ref.current != null) {
303+
if (e.key === "ArrowDown" && ref.current.nextElementSibling != null) {
304+
const element = ref.current.nextElementSibling as HTMLElement;
305+
element.scrollIntoView({ behavior: "instant", block: "nearest" });
306+
element.focus();
307+
}
308+
if (e.key === "ArrowUp" && ref.current.previousElementSibling != null) {
309+
const element = ref.current.previousElementSibling as HTMLElement;
310+
element.scrollIntoView({ behavior: "instant", block: "nearest" });
311+
element.focus();
312+
}
313+
}
314+
onRowClick({
315+
modKey: eventHasModKey(e),
316+
shiftKey: e.shiftKey,
317+
isRmb: e.key === "ContextMenu",
318+
}, newIndex, lastIndex);
319+
}, [index, lastIndex, onRowClick]);
320+
275321
return (
276-
<div
322+
<div ref={ref}
277323
className={`tr${props.selected ? " selected" : ""}`}
278324
style={{ height: `${props.height}px`, transform: `translateY(${props.start}px)` }}
279-
onClick={(e) => {
280-
props.onRowClick(e, props.index, props.lastIndex);
281-
}}
282-
onContextMenu={(e) => {
283-
props.onRowClick(e, props.index, props.lastIndex);
284-
}}
325+
onClick={onMouseEvent}
326+
onContextMenu={onMouseEvent}
285327
onDoubleClick={onRowDoubleClick}
328+
onKeyDown={onKeyDown}
286329
tabIndex={-1}
287330
>
288331
<MemoizedInnerRow {...props} />
@@ -530,7 +573,9 @@ export function EditableNameField(props: EditableNameFieldProps) {
530573
setNewName(props.currentName);
531574
}, [props.currentName]);
532575

533-
const onEnter = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
576+
const ref = useRef<HTMLDivElement>(null);
577+
578+
const onTextKeyDown = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
534579
if (event.key === "Enter") {
535580
props.onUpdate?.(
536581
newName,
@@ -539,6 +584,10 @@ export function EditableNameField(props: EditableNameFieldProps) {
539584
},
540585
() => { setRenaming(false); });
541586
}
587+
if (event.key === "Escape") {
588+
setRenaming(false);
589+
ref.current?.focus();
590+
}
542591
}, [newName, props]);
543592

544593
useEffect(() => {
@@ -548,8 +597,6 @@ export function EditableNameField(props: EditableNameFieldProps) {
548597
}
549598
}, [isRenaming]);
550599

551-
const ref = useRef<HTMLDivElement>(null);
552-
553600
useEffect(() => {
554601
if (ref.current != null) {
555602
const row = ref.current.parentNode?.parentNode as HTMLDivElement;
@@ -580,7 +627,7 @@ export function EditableNameField(props: EditableNameFieldProps) {
580627
}}
581628
onChange={(e) => { setNewName(e.target.value); }}
582629
onBlur={() => { setRenaming(false); }}
583-
onKeyDown={onEnter}
630+
onKeyDown={onTextKeyDown}
584631
onClick={(e) => { e.stopPropagation(); }} />
585632
: <Box pl="xs" sx={{ flexGrow: 1, textOverflow: "ellipsis", overflow: "hidden" }}>
586633
{props.currentName}

src/trutil.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export function pathMapToServer(path: string, config: ServerConfig) {
125125
return mappedPath;
126126
}
127127

128-
export function eventHasModKey(event: React.MouseEvent<Element>) {
128+
export function eventHasModKey(event: React.MouseEvent<Element> | React.KeyboardEvent<Element>) {
129129
return (navigator.platform.startsWith("Mac") && event.metaKey) ||
130130
(!navigator.platform.startsWith("Mac") && event.ctrlKey);
131131
}

0 commit comments

Comments
 (0)