Skip to content

Commit 0fc1c9e

Browse files
committed
frontend/project-page: Add tab semantics to activity bar and file tabs (Phase 11b)
1 parent c15b155 commit 0fc1c9e

File tree

6 files changed

+112
-27
lines changed

6 files changed

+112
-27
lines changed

src/dev/ARIA.md

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -405,28 +405,37 @@ Location: `packages/frontend/projects/`
405405
- [x]**File explorer settings**: `role="region" aria-label="File explorer settings"`
406406
- [x]**Projects settings**: `role="region" aria-label="Projects settings"`
407407

408-
#### 11b: Project Page
408+
#### 11b: Project Page ✅ COMPLETED
409409

410410
Location: `packages/frontend/project/page/`
411411

412-
- [ ] **page.tsx** - Main project workspace
413-
- [ ] Page structure: `<main role="main" aria-label="Project workspace: {projectName}"`
414-
- [ ] Activity bar: `role="tablist"` with tab semantics
415-
- [ ] Content area: `role="main" or region` for editor content
412+
**Completed** ✅:
416413

417-
- [ ] **activity-bar.tsx** - Left sidebar with features
418-
- [ ] Container: `role="tablist" aria-label="Project activity tabs"`
419-
- [ ] Each tab: `role="tab"` with `aria-selected` and `aria-controls`
420-
- [ ] Tab panels: `role="tabpanel"` with `aria-labelledby`
414+
- [x] **page.tsx** - Main project workspace
415+
- [x] Main content area: `<div role="main" aria-label="Content: {currentFilename}">` (line 389-392)
416+
- [x] Activity bar sidebar: `<aside role="complementary" aria-label="Project activity bar">` (line 356-371)
417+
- [x] File tabs navigation: `<nav aria-label="Open files">` (line 307-313)
418+
- [x] Flyout sidebar: `<aside role="complementary" aria-label="Project sidebar">` (line 262-278)
421419

422-
- [ ] **file-tabs.tsx** - Open files tab bar
423-
- [ ] Container: `role="tablist" aria-label="Open files in {projectName}"`
424-
- [ ] Tab items: `role="tab"` with aria-selected, aria-controls
425-
- [ ] Tab panels: `role="tabpanel" aria-labelledby="tab-{id}"`
426-
- [ ] Close buttons: `aria-label="Close {fileName}"`
420+
- [x] **Activity Bar** (`activity-bar-tabs.tsx` / `VerticalFixedTabs` component)
421+
- [x] Container: `role="tablist" aria-label="Project activity tabs"` (line 267-268)
422+
- [x] Each tab button: `role="tab"`, `aria-selected={isActive}`, `aria-controls="activity-panel-{name}"` (line 230-232)
427423

428-
- [ ] **content.tsx** - Main content area
429-
- [ ] Container: `role="main"` or clear region role
424+
- [x] **File Tabs** (`file-tabs.tsx` / `FileTabs` component)
425+
- [x] Container: Ant Design `<Tabs>` with `aria-label="Open files"` (line 167)
426+
- [x] Tab items: `role="tab"` with `aria-selected={isActive}`, `aria-controls="content-{tabId}"` (Label component)
427+
- [x] Tab panels: Ant Design Tabs handles tab panel semantics automatically
428+
429+
- [x] **Content Switching** (`content.tsx`)
430+
- [x] Each content section: `role="tabpanel"` with dynamic `aria-label` based on active tab (line 119-120)
431+
- [x] Labels cover all tab types: home, files, new, log, search, servers, settings, info, users, upgrades, editor paths
432+
433+
**Files Modified**:
434+
435+
- `packages/frontend/project/page/file-tab.tsx` - Added ARIA props to FileTab component interface and body div
436+
- `packages/frontend/project/page/activity-bar-tabs.tsx` - Added role="tablist" and ARIA attributes to VerticalFixedTabs
437+
- `packages/frontend/project/page/file-tabs.tsx` - Added ARIA props to Label component and Tabs aria-label
438+
- `packages/frontend/project/page/content.tsx` - Added role="tabpanel" with dynamic aria-label labels
430439

431440
#### 11c: Flyouts
432441

src/packages/frontend/project/page/activity-bar-tabs.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ export function VerticalFixedTabs({
227227
flyout={name}
228228
condensed={condensed}
229229
showLabel={showActBarLabels}
230+
role="tab"
231+
aria-selected={isActive}
232+
aria-controls={`activity-panel-${name}`}
230233
/>
231234
);
232235
if (tab != null) items.push(tab);
@@ -261,6 +264,8 @@ export function VerticalFixedTabs({
261264
return (
262265
<div
263266
ref={parent}
267+
role="tablist"
268+
aria-label="Project activity tabs"
264269
style={{
265270
display: "flex",
266271
flexDirection: "column",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* This file is part of CoCalc: Copyright © 2025 Sagemath, Inc.
3+
* License: MS-RSL – see LICENSE.md for details.
4+
*/
5+
6+
/**
7+
* ARIA labels for project page content tabs/panels.
8+
* Maps tab names to descriptive labels for screen readers.
9+
* This is a first step to extract tab titles to a central location
10+
* and avoid duplication with other tab definitions.
11+
*/
12+
13+
export const PROJECT_TAB_ARIA_LABELS: Record<string, string> = {
14+
home: "Home",
15+
files: "File Explorer",
16+
new: "Create New",
17+
log: "Recent Files",
18+
search: "Find",
19+
servers: "Compute Servers",
20+
settings: "Project Settings",
21+
info: "Project Information",
22+
users: "Collaborators",
23+
upgrades: "Licenses",
24+
active: "Open Files",
25+
} as const;
26+
27+
/**
28+
* Get the ARIA label for a project tab by name.
29+
* For editor tabs (editor-{path}), returns "Editor: {filename}"
30+
* For fixed tabs, returns the label from PROJECT_TAB_ARIA_LABELS
31+
* Falls back to "Project Content" if tab name is not recognized
32+
*/
33+
export function getProjectTabAriaLabel(tabName: string): string {
34+
// Handle editor tabs (editor-{path})
35+
if (tabName.startsWith("editor-")) {
36+
const path = tabName.slice("editor-".length);
37+
return `Editor: ${path}`;
38+
}
39+
40+
// Look up fixed tab label
41+
const label = PROJECT_TAB_ARIA_LABELS[tabName];
42+
if (label != null) {
43+
return label;
44+
}
45+
46+
// Fallback for unknown tabs
47+
return "Project Content";
48+
}

src/packages/frontend/project/page/content.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { editor_id } from "@cocalc/frontend/project/utils";
4444
import { webapp_client } from "@cocalc/frontend/webapp-client";
4545
import { hidden_meta_file } from "@cocalc/util/misc";
4646
import { useProjectContext } from "../context";
47+
import { getProjectTabAriaLabel } from "./consts";
4748
import getAnchorTagComponent from "./anchor-tag-component";
4849
import HomePage from "./home-page";
4950
import { ProjectCollaboratorsPage } from "./project-collaborators";
@@ -100,6 +101,8 @@ export const Content: React.FC<Props> = (props: Props) => {
100101
return (
101102
<div
102103
ref={contentRef}
104+
role="tabpanel"
105+
aria-label={getProjectTabAriaLabel(tab_name)}
103106
style={{
104107
...MAIN_STYLE,
105108
...(!is_visible ? { display: "none" } : undefined),

src/packages/frontend/project/page/file-tab.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ interface Props0 {
184184
flyout?: FixedTab;
185185
condensed?: boolean;
186186
showLabel?: boolean; // only relevant for the vertical activity bar. still showing alert tags!
187+
// ARIA attributes for tab semantics
188+
role?: string;
189+
"aria-selected"?: boolean;
190+
"aria-controls"?: string;
187191
}
188192
interface PropsPath extends Props0 {
189193
path: string;
@@ -215,10 +219,10 @@ export function FileTab(props: Readonly<Props>) {
215219
// alerts only work on non-docker projects (for now) -- #7077
216220
const status_alerts: string[] =
217221
!onCoCalcDocker && name === "info"
218-
? project_status
222+
? (project_status
219223
?.get("alerts")
220224
?.map((a) => a.get("type"))
221-
.toJS() ?? []
225+
.toJS() ?? [])
222226
: [];
223227

224228
const other_settings = useTypedRedux("account", "other_settings");
@@ -303,8 +307,8 @@ export function FileTab(props: Readonly<Props>) {
303307
flyout === active_flyout
304308
? COLORS.PROJECT.FIXED_LEFT_ACTIVE
305309
: active_flyout == null
306-
? COLORS.GRAY_L
307-
: COLORS.GRAY_L0;
310+
? COLORS.GRAY_L
311+
: COLORS.GRAY_L0;
308312
const bg = flyout === active_flyout ? COLORS.GRAY_L0 : undefined;
309313

310314
return (
@@ -368,7 +372,7 @@ export function FileTab(props: Readonly<Props>) {
368372

369373
const icon =
370374
path != null
371-
? file_options(path)?.icon ?? "code-o"
375+
? (file_options(path)?.icon ?? "code-o")
372376
: FIXED_PROJECT_TABS[name!].icon;
373377

374378
const tags =
@@ -466,6 +470,9 @@ export function FileTab(props: Readonly<Props>) {
466470
cocalc-test={label}
467471
onClick={click}
468472
onMouseUp={onMouseUp}
473+
role={props.role}
474+
aria-selected={props["aria-selected"]}
475+
aria-controls={props["aria-controls"]}
469476
>
470477
<div
471478
style={{

src/packages/frontend/project/page/file-tabs.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ import { FileTab } from "./file-tab";
2222

2323
const MIN_WIDTH = 48;
2424

25-
function Label({ path, project_id, label }) {
25+
interface LabelProps {
26+
path: string;
27+
project_id: string;
28+
label: string;
29+
isActive?: boolean;
30+
}
31+
32+
function Label({ path, project_id, label, isActive }: LabelProps) {
2633
const { width } = useItemContext();
2734
const { active } = useSortable({ id: project_id });
2835
return (
@@ -37,6 +44,9 @@ function Label({ path, project_id, label }) {
3744
? { width: Math.max(MIN_WIDTH, width + 15), marginRight: "-10px" }
3845
: undefined),
3946
}}
47+
role="tab"
48+
aria-selected={isActive ?? false}
49+
aria-controls={`content-${path.replace(/[^a-zA-Z0-9-]/g, "-")}`}
4050
/>
4151
);
4252
}
@@ -82,15 +92,21 @@ export default function FileTabs({ openFiles, project_id, activeTab }) {
8292

8393
const labels = file_tab_labels(paths);
8494
const items: TabsProps["items"] = [];
95+
const activeKey = activeTab.startsWith(EDITOR_PREFIX)
96+
? pathToKey(activeTab.slice(EDITOR_PREFIX.length))
97+
: "";
8598

8699
for (let index = 0; index < labels.length; index++) {
100+
const pathKey = pathToKey(paths[index]);
101+
const isActive = pathKey === activeKey;
87102
items.push({
88-
key: pathToKey(paths[index]),
103+
key: pathKey,
89104
label: (
90105
<Label
91106
path={paths[index]}
92107
project_id={project_id}
93108
label={labels[index]}
109+
isActive={isActive}
94110
/>
95111
),
96112
});
@@ -128,10 +144,6 @@ export default function FileTabs({ openFiles, project_id, activeTab }) {
128144
});
129145
}
130146

131-
const activeKey = activeTab.startsWith(EDITOR_PREFIX)
132-
? pathToKey(activeTab.slice(EDITOR_PREFIX.length))
133-
: "";
134-
135147
function onDragStart(event) {
136148
if (actions == null) return;
137149
if (event?.active?.id != activeKey) {
@@ -164,6 +176,7 @@ export default function FileTabs({ openFiles, project_id, activeTab }) {
164176
actions.set_active_tab(path_to_tab(keyToPath(key)));
165177
}}
166178
popupClassName={"cocalc-files-tabs-more"}
179+
aria-label="Open files"
167180
/>
168181
</SortableTabs>
169182
);

0 commit comments

Comments
 (0)