Skip to content

Commit 8ed259f

Browse files
Scott DoverScott Dover
authored andcommitted
fix: respond to code review
- move useDataViewer - improve keyboard accessibility - improve localize Signed-off-by: Scott Dover <[email protected]>
1 parent 7e3aa84 commit 8ed259f

File tree

5 files changed

+119
-78
lines changed

5 files changed

+119
-78
lines changed

client/src/webview/ColumnHeader.tsx

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import { useRef } from "react";
44

55
import { AgColumn, GridApi } from "ag-grid-community";
66

7-
import { ColumnMenuProps, useColumnMenu } from "./ColumnMenu";
8-
97
const getIconForColumnType = (type: string) => {
108
switch (type.toLocaleLowerCase()) {
119
case "float":
@@ -31,15 +29,15 @@ const ColumnHeader = ({
3129
column,
3230
currentColumn: getCurrentColumn,
3331
columnType,
34-
setColumnMenu,
3532
theme,
33+
displayMenuForColumn,
3634
}: {
3735
api: GridApi;
3836
column: AgColumn;
3937
currentColumn: () => AgColumn | undefined;
4038
columnType: string;
41-
setColumnMenu: (props: ColumnMenuProps) => void;
4239
theme: string;
40+
displayMenuForColumn: (api: GridApi, column: AgColumn, rect: DOMRect) => void;
4341
}) => {
4442
const ref = useRef<HTMLButtonElement>(undefined!);
4543
const currentColumn = getCurrentColumn();
@@ -51,17 +49,8 @@ const ColumnHeader = ({
5149
const dropdownClassname =
5250
currentColumn?.colId === column.colId ? "active dropdown" : "dropdown";
5351

54-
const getColumnMenu = useColumnMenu({ api, theme });
55-
const displayColumnMenu = () => {
56-
if (currentColumn) {
57-
return setColumnMenu(undefined);
58-
}
59-
setColumnMenu(
60-
getColumnMenu(column, ref.current.getBoundingClientRect(), () =>
61-
setColumnMenu(undefined),
62-
),
63-
);
64-
};
52+
const displayColumnMenu = () =>
53+
displayMenuForColumn(api, column, ref.current.getBoundingClientRect());
6554

6655
return (
6756
<div className={`ag-cell-label-container ${theme}`} role="presentation">
@@ -77,7 +66,12 @@ const ColumnHeader = ({
7766
)}
7867
</span>
7968
<div className={dropdownClassname}>
80-
<button ref={ref} type="button" onClick={displayColumnMenu}>
69+
<button
70+
ref={ref}
71+
type="button"
72+
onClick={displayColumnMenu}
73+
tabIndex={-1}
74+
>
8175
<span />
8276
</button>
8377
</div>

client/src/webview/ColumnMenu.tsx

Lines changed: 50 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -17,64 +17,58 @@ export interface ColumnMenuProps {
1717
top: number;
1818
}
1919

20-
export const useColumnMenu = ({
21-
api,
22-
theme,
23-
}: {
24-
api: GridApi;
25-
theme: ColumnMenuProps["theme"];
26-
}) => {
27-
const applyColumnState = (state: ColumnState[]) => {
28-
api.applyColumnState({ state, defaultState: { sort: null } });
29-
api.ensureIndexVisible(0);
30-
};
31-
32-
return (
33-
column: AgColumn,
34-
{ height, top, left }: DOMRect,
35-
dismissMenu: () => void,
36-
) => ({
37-
column,
38-
dismissMenu,
39-
hasSort: api.getColumnState().some((c) => c.sort),
40-
left,
41-
theme,
42-
top: top + height,
43-
sortColumn: (direction: "asc" | "desc" | null) => {
44-
const newColumnState = api.getColumnState().filter((c) => c.sort);
45-
const colIndex = newColumnState.findIndex(
46-
(c) => c.colId === column.colId,
47-
);
48-
if (colIndex === -1) {
49-
newColumnState.push({
50-
colId: column.colId,
51-
sort: direction,
52-
sortIndex: newColumnState.length,
53-
});
54-
} else {
55-
newColumnState[colIndex].sort = direction;
56-
}
57-
applyColumnState(newColumnState);
58-
},
59-
removeAllSorting: () =>
60-
applyColumnState(
61-
api
62-
.getColumnState()
63-
.filter((c) => c.sort)
64-
.map((c) => ({ colId: c.colId, sort: null })),
65-
),
66-
removeFromSort: () =>
67-
applyColumnState(
68-
api
69-
.getColumnState()
70-
.sort((a, b) => a.sortIndex - b.sortIndex)
71-
.filter((c) => c.sort && c.colId !== column.colId)
72-
// After we remove the column, lets reindex what's left
73-
.map((c, sortIndex) => ({ ...c, sortIndex })),
74-
),
75-
});
20+
const applyColumnState = (api: GridApi, state: ColumnState[]) => {
21+
api.applyColumnState({ state, defaultState: { sort: null } });
22+
api.ensureIndexVisible(0);
7623
};
7724

25+
export const getColumnMenu = (
26+
api: GridApi,
27+
theme: ColumnMenuProps["theme"],
28+
column: AgColumn,
29+
{ height, top, left }: DOMRect,
30+
dismissMenu: () => void,
31+
) => ({
32+
column,
33+
dismissMenu,
34+
hasSort: api.getColumnState().some((c) => c.sort),
35+
left,
36+
theme,
37+
top: top + height,
38+
sortColumn: (direction: "asc" | "desc" | null) => {
39+
const newColumnState = api.getColumnState().filter((c) => c.sort);
40+
const colIndex = newColumnState.findIndex((c) => c.colId === column.colId);
41+
if (colIndex === -1) {
42+
newColumnState.push({
43+
colId: column.colId,
44+
sort: direction,
45+
sortIndex: newColumnState.length,
46+
});
47+
} else {
48+
newColumnState[colIndex].sort = direction;
49+
}
50+
applyColumnState(api, newColumnState);
51+
},
52+
removeAllSorting: () =>
53+
applyColumnState(
54+
api,
55+
api
56+
.getColumnState()
57+
.filter((c) => c.sort)
58+
.map((c) => ({ colId: c.colId, sort: null })),
59+
),
60+
removeFromSort: () =>
61+
applyColumnState(
62+
api,
63+
api
64+
.getColumnState()
65+
.sort((a, b) => a.sortIndex - b.sortIndex)
66+
.filter((c) => c.sort && c.colId !== column.colId)
67+
// After we remove the column, lets reindex what's left
68+
.map((c, sortIndex) => ({ ...c, sortIndex })),
69+
),
70+
});
71+
7872
const ColumnMenu = ({
7973
column,
8074
dismissMenu,

client/src/webview/GridMenu.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ const GridMenu = ({
105105
className={`ag-menu-option ${index === activeIndex ? "ag-menu-option-active" : ""} ${menuItem.disabled ? "ag-menu-option-disabled" : ""}`}
106106
role="menuitem"
107107
aria-haspopup="menu"
108-
tabIndex={-1}
109108
key={menuItem.name}
110109
onMouseEnter={() => {
111110
if (menuItem.disabled) {

client/src/webview/localize.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// This expects for translations to be defined in a script block with an id of
66
// `l10-messages`.
77
let localizedTerms = null;
8-
const localize = (term: string) => {
8+
const localize = (term: string, replacments?: Record<string, string>) => {
99
if (!localizedTerms) {
1010
try {
1111
localizedTerms = JSON.parse(
@@ -17,7 +17,15 @@ const localize = (term: string) => {
1717
}
1818
}
1919

20-
return localizedTerms[term] ?? term;
20+
let translatedString = localizedTerms[term] ?? term;
21+
if (replacments) {
22+
Object.entries(replacments).forEach(
23+
([key, value]) =>
24+
(translatedString = translatedString.replace(`{${key}}`, value)),
25+
);
26+
}
27+
28+
return translatedString;
2129
};
2230

2331
export default localize;

client/src/webview/useDataViewer.tsx renamed to client/src/webview/useDataViewer.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,22 @@
33
import { useCallback, useEffect, useRef, useState } from "react";
44

55
import {
6+
AgColumn,
67
AllCommunityModule,
78
ColDef,
9+
GridApi,
810
GridReadyEvent,
911
IGetRowsParams,
1012
ModuleRegistry,
1113
SortModelItem,
14+
SuppressHeaderKeyboardEventParams,
1215
} from "ag-grid-community";
1316
import { v4 } from "uuid";
1417

1518
import { TableData } from "../components/LibraryNavigator/types";
1619
import { Column } from "../connection/rest/api/compute";
1720
import ColumnHeader from "./ColumnHeader";
18-
import { ColumnMenuProps } from "./ColumnMenu";
21+
import { ColumnMenuProps, getColumnMenu } from "./ColumnMenu";
1922

2023
declare const acquireVsCodeApi;
2124
const vscode = acquireVsCodeApi();
@@ -142,6 +145,18 @@ const useDataViewer = (theme: string) => {
142145
[columns],
143146
);
144147

148+
const displayMenuForColumn = useCallback(
149+
(api: GridApi, column: AgColumn, rect: DOMRect) => {
150+
if (columnMenuRef.current?.column) {
151+
return setColumnMenu(undefined);
152+
}
153+
setColumnMenu(
154+
getColumnMenu(api, theme, column, rect, () => setColumnMenu(undefined)),
155+
);
156+
},
157+
[theme],
158+
);
159+
145160
useEffect(() => {
146161
if (columns.length > 0) {
147162
return;
@@ -153,11 +168,42 @@ const useDataViewer = (theme: string) => {
153168
headerComponent: ColumnHeader,
154169
headerComponentParams: {
155170
columnType: column.type,
156-
setColumnMenu,
157171
currentColumn: () => columnMenuRef.current?.column,
172+
displayMenuForColumn,
158173
theme,
159174
},
175+
suppressHeaderKeyboardEvent: (
176+
params: SuppressHeaderKeyboardEventParams,
177+
) => {
178+
// If a user tabs to a different column, dismiss the column menu
179+
if (params.event.key === "Tab") {
180+
setColumnMenu(undefined);
181+
return false;
182+
}
183+
if (
184+
params.event.key === "Enter" ||
185+
(params.event.key === "F10" && params.event.shiftKey)
186+
) {
187+
const dropdownButton =
188+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
189+
(params.event.target as HTMLElement).querySelector(
190+
".dropdown > button",
191+
);
192+
if (!dropdownButton) {
193+
return true;
194+
}
195+
displayMenuForColumn(
196+
params.api,
197+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
198+
params.column as AgColumn,
199+
dropdownButton.getBoundingClientRect(),
200+
);
201+
return true;
202+
}
203+
return false;
204+
},
160205
}));
206+
161207
columns.unshift({
162208
field: "#",
163209
suppressMovable: true,
@@ -166,7 +212,7 @@ const useDataViewer = (theme: string) => {
166212

167213
setColumns(columns);
168214
});
169-
}, [columns.length, theme]);
215+
}, [columns.length, theme, displayMenuForColumn]);
170216

171217
useEffect(() => {
172218
window.addEventListener("contextmenu", contextMenuHandler, true);

0 commit comments

Comments
 (0)