Skip to content

Commit 24cb036

Browse files
committed
frontend/projects/aria: keyboard nav + aria tweaks
1 parent 8a47776 commit 24cb036

File tree

4 files changed

+516
-219
lines changed

4 files changed

+516
-219
lines changed

src/packages/frontend/_projects.sass

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ div.cc-project-flyout-dragbar:hover
141141
cursor: pointer
142142
background-color: colors.$COL_GRAY_LLL
143143

144+
.cc-projects-row-focused
145+
> .ant-table-cell
146+
background-color: colors.$COL_GRAY_LL
147+
144148
.cc-project-flyout-files-panel,
145149
.cc-project-flyout-settings-panel
146150
.ant-collapse-content-box

src/packages/frontend/projects/projects-page.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
* License: MS-RSL – see LICENSE.md for details
44
*/
55

6+
import type { InputRef } from "antd";
67
import { Col, Grid, Row, Space } from "antd";
78
import { Map, Set } from "immutable";
8-
import { useLayoutEffect, useRef } from "react";
9+
import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
910
import { useIntl } from "react-intl";
1011

1112
// ensure redux stuff (actions and store) are initialized:
@@ -30,6 +31,7 @@ import { LoadAllProjects } from "./projects-load-all";
3031
import { ProjectsOperations } from "./projects-operations";
3132
import { StarredProjectsBar } from "./projects-starred";
3233
import { ProjectsTable } from "./projects-table";
34+
import type { ProjectsTableHandle } from "./projects-table";
3335
import { ProjectsTableControls } from "./projects-table-controls";
3436
import ProjectsPageTour from "./tour";
3537
import { useBookmarkedProjects } from "./use-bookmarked-projects";
@@ -63,6 +65,8 @@ export const ProjectsPage: React.FC = () => {
6365
const createNewRef = useRef<any>(null);
6466
const projectListRef = useRef<any>(null);
6567
const filenameSearchRef = useRef<any>(null);
68+
const searchInputRef = useRef<InputRef>(null);
69+
const projectsTableRef = useRef<ProjectsTableHandle>(null);
6670

6771
// Calculating table height
6872
const containerRef = useRef<HTMLDivElement>(null);
@@ -91,6 +95,49 @@ export const ProjectsPage: React.FC = () => {
9195
string[] | null
9296
>(null);
9397

98+
const focusSearchInput = useCallback(() => {
99+
const input = searchInputRef.current;
100+
if (!input) return false;
101+
input.focus({ cursor: "end" });
102+
return true;
103+
}, []);
104+
105+
const handleSearchNavigate = useCallback((direction: "down" | "up") => {
106+
if (direction === "down") {
107+
projectsTableRef.current?.focusFirstRow();
108+
} else {
109+
projectsTableRef.current?.focusLastRow();
110+
}
111+
}, []);
112+
113+
useEffect(() => {
114+
const handleGlobalShortcut = (event: KeyboardEvent) => {
115+
if (event.defaultPrevented) return;
116+
const target = event.target as HTMLElement | null;
117+
const isEditable =
118+
!!target &&
119+
(target.tagName === "INPUT" ||
120+
target.tagName === "TEXTAREA" ||
121+
target.isContentEditable);
122+
const hasModifier = event.metaKey || event.ctrlKey || event.altKey;
123+
124+
if (event.key === "/" && !hasModifier && !isEditable) {
125+
if (focusSearchInput()) {
126+
event.preventDefault();
127+
}
128+
return;
129+
}
130+
131+
if (event.key === "ArrowDown" && !hasModifier && !isEditable) {
132+
projectsTableRef.current?.focusFirstRow();
133+
event.preventDefault();
134+
}
135+
};
136+
137+
window.addEventListener("keydown", handleGlobalShortcut);
138+
return () => window.removeEventListener("keydown", handleGlobalShortcut);
139+
}, [focusSearchInput]);
140+
94141
// if not shown, trigger a re-calculation
95142
const allLoaded = !!useTypedRedux(
96143
"projects",
@@ -313,6 +360,8 @@ export const ProjectsPage: React.FC = () => {
313360
createNewRef={createNewRef}
314361
searchRef={searchRef}
315362
filtersRef={filtersRef}
363+
searchInputRef={searchInputRef}
364+
onSearchNavigate={handleSearchNavigate}
316365
tour={
317366
<ProjectsPageTour
318367
searchRef={searchRef}
@@ -341,11 +390,13 @@ export const ProjectsPage: React.FC = () => {
341390
aria-label={`Projects list (${visible_projects.length} total)`}
342391
>
343392
<ProjectsTable
393+
ref={projectsTableRef}
344394
visible_projects={visible_projects}
345395
height={tableHeight}
346396
narrow={narrow}
347397
filteredCollaborators={filteredCollaborators}
348398
onFilteredCollaboratorsChange={setFilteredCollaborators}
399+
onRequestSearchFocus={focusSearchInput}
349400
/>
350401
</div>
351402

src/packages/frontend/projects/projects-table-controls.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212

1313
import type { SelectProps } from "antd";
1414

15-
import { Button, Input, Select, Space, Switch } from "antd";
15+
import { Button, Input, InputRef, Select, Space, Switch } from "antd";
1616
import { Set } from "immutable";
1717
import { ReactNode, useMemo } from "react";
18+
import type { KeyboardEvent } from "react";
1819
import { defineMessage, useIntl } from "react-intl";
1920

2021
import { useAutoFocusPreference } from "@cocalc/frontend/account";
@@ -47,13 +48,17 @@ const CONTROLS_STYLE: CSS = {
4748
justifyContent: "space-between",
4849
} as const;
4950

51+
type SearchNavigateDirection = "up" | "down";
52+
5053
interface Props {
5154
visible_projects: string[];
5255
onCreateProject: () => void;
5356
tour: ReactNode;
5457
createNewRef: React.RefObject<any>;
5558
searchRef: React.RefObject<any>;
5659
filtersRef: React.RefObject<any>;
60+
searchInputRef?: React.RefObject<InputRef | null>;
61+
onSearchNavigate?: (direction: SearchNavigateDirection) => void;
5762
}
5863

5964
export function ProjectsTableControls({
@@ -63,6 +68,8 @@ export function ProjectsTableControls({
6368
createNewRef,
6469
searchRef,
6570
filtersRef,
71+
searchInputRef,
72+
onSearchNavigate,
6673
}: Props) {
6774
const intl = useIntl();
6875
const shouldAutoFocus = useAutoFocusPreference();
@@ -116,11 +123,22 @@ export function ProjectsTableControls({
116123
}
117124
}
118125

126+
function handleSearchKeyDown(e: KeyboardEvent<HTMLInputElement>) {
127+
if (e.key === "ArrowDown") {
128+
e.preventDefault();
129+
onSearchNavigate?.("down");
130+
} else if (e.key === "ArrowUp") {
131+
e.preventDefault();
132+
onSearchNavigate?.("up");
133+
}
134+
}
135+
119136
return (
120137
<Space style={CONTROLS_STYLE} direction="horizontal">
121138
{/* Left section: Search and Hashtags */}
122139
<Space wrap ref={searchRef}>
123140
<Input.Search
141+
ref={searchInputRef}
124142
aria-label="Filter projects by name"
125143
placeholder={intl.formatMessage({
126144
id: "projects.table-controls.search.placeholder",
@@ -130,6 +148,7 @@ export function ProjectsTableControls({
130148
value={search}
131149
onChange={handleSearchChange}
132150
onPressEnter={handlePressEnter}
151+
onKeyDown={handleSearchKeyDown}
133152
style={{ width: IS_MOBILE ? 125 : 250 }}
134153
allowClear
135154
/>

0 commit comments

Comments
 (0)