Skip to content

Commit 7ea5f91

Browse files
committed
frontend/app: Add ARIA semantics to app shell and navigation (Phase 12 P0)
1 parent 0fc1c9e commit 7ea5f91

File tree

5 files changed

+88
-18
lines changed

5 files changed

+88
-18
lines changed

src/dev/ARIA.md

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -468,39 +468,63 @@ Location: `packages/frontend/project/page/flyouts/`
468468
- [ ] Settings: Proper form labeling
469469
- [ ] Status: Save status announcements
470470

471-
### Phase 12: App Shell & Navigation PENDING
471+
### Phase 12: App Shell & Navigation ✅ P0 COMPLETED | P1 PENDING
472472

473473
**Priority: HIGH** - Framework for entire app
474474

475475
Location: `packages/frontend/app/`
476476

477-
- [ ] **page.tsx** - Main application container (323 lines)
478-
- [ ] Root structure: Ensure proper landmark hierarchy
479-
- [ ] Content switching: Announce page changes
480-
- [ ] Notification center connection display
477+
#### **P0 - Critical Foundation** ✅ COMPLETED
481478

482-
- [ ] **nav-tab.tsx** - Top navigation tabs
483-
- [ ] Tab items: `role="tab"` with `aria-selected`, `aria-controls`
484-
- [ ] Each tab: Clear context (Projects, Account, Admin, etc.)
485-
- [ ] Hover/focus states: Visual + ARIA
479+
- [x] **page.tsx** - Main application container
480+
- [x] Root `<main role="main" aria-label="{site_name} application">` (line 368-369)
481+
- [x] Dynamic label uses customizable site_name from customize store
482+
- [x] Fallback to SITE_NAME constant from @cocalc/util/theme
483+
- [x] Right nav region: `role="region" aria-label="Top navigation controls"` (line 292-293)
484+
485+
- [x] **nav-tab.tsx** - Top navigation tabs with keyboard support
486+
- [x] NavTab component: Added optional `role` and `aria-label` props
487+
- [x] Made keyboard accessible: `tabIndex={0}` + `onKeyDown` for Enter/Space
488+
- [x] Default `role="button"` with override capability
489+
- [x] Supports all navigation items: Projects, Account, Admin, Help, Sign In
490+
491+
- [x] **connection-indicator.tsx** - Network status live region
492+
- [x] Status indicator: `role="status"` (line 119)
493+
- [x] Live region: `aria-live="polite"` to announce connection changes (line 121)
494+
- [x] Busy state: `aria-busy={true}` when connecting (line 122)
495+
- [x] Dynamic label: `aria-label={getConnectionLabel()}` showing current state (line 120)
496+
- [x] Keyboard support: `tabIndex={0}` + Enter/Space activation
497+
- [x] Added `labels.connected` to i18n/common.ts for proper translation
498+
499+
**Files Modified**:
500+
501+
- `packages/frontend/app/page.tsx` - Root structure with site_name and right-nav region
502+
- `packages/frontend/app/nav-tab.tsx` - ARIA props and keyboard accessibility
503+
- `packages/frontend/app/connection-indicator.tsx` - Status live region with i18n labels
504+
- `packages/frontend/i18n/common.ts` - Added labels.connected
505+
506+
#### **P1 - Important Improvements** ⏳ PENDING
486507

487508
- [ ] **active-content.tsx** - Content router
488509
- [ ] Dynamic content: Announce when switching pages
489510
- [ ] Loading states: `aria-busy` indication
490511
- [ ] Error states: ARIA alert or live region
491512

492-
- [ ] **Banners** - Informational/warning banners
493-
- [ ] All banners: `role="region" aria-label="Information"`
513+
- [ ] **Banners** - Informational/warning banners (5 files)
514+
- [ ] All banners: `role="region" aria-label="..."`
494515
- [ ] `i18n-banner.tsx` - Language selection
495516
- [ ] `verify-email-banner.tsx` - Email verification
496517
- [ ] `version-warning.tsx` - Version alerts
497518
- [ ] `insecure-test-mode-banner.tsx` - Test mode warning
498-
- [ ] Close buttons: `aria-label="Close"`
519+
- [ ] `warnings.tsx` - Cookie/storage warnings
520+
521+
- [ ] **Notifications** - Notification indicators
522+
- [ ] Notification badges: `aria-label` with count
523+
- [ ] Live region: `aria-live="polite"` for count changes
499524

500-
- [ ] **Connection Status** - Network status indicator
501-
- [ ] Status indicator: `aria-label` with current state
502-
- [ ] Updates: `aria-live="polite"` for status changes
503-
- [ ] Tooltip text: Accessible to keyboard users
525+
- [ ] **projects-nav.tsx** - Project tabs navigation
526+
- [ ] Container: `aria-label="Open projects"`
527+
- [ ] Tab semantics already handled by Ant Design Tabs
504528

505529
### Phase 13: Forms & Settings ⏳ PENDING
506530

src/packages/frontend/app/connection-indicator.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,19 @@ export const ConnectionIndicator: React.FC<Props> = React.memo(
6868
: {}),
6969
} as const;
7070

71+
function getConnectionLabel() {
72+
switch (connection_status) {
73+
case "connected":
74+
return intl.formatMessage(labels.connected);
75+
case "connecting":
76+
return intl.formatMessage(labels.connecting);
77+
case "disconnected":
78+
return intl.formatMessage(labels.disconnected);
79+
default:
80+
return "Connection status unknown";
81+
}
82+
}
83+
7184
function render_connection_status() {
7285
if (connection_status === "connected") {
7386
return (
@@ -103,12 +116,22 @@ export const ConnectionIndicator: React.FC<Props> = React.memo(
103116
return (
104117
<div
105118
className={TOP_BAR_ELEMENT_CLASS}
119+
role="status"
120+
aria-label={getConnectionLabel()}
121+
aria-live="polite"
122+
aria-busy={connection_status === "connecting"}
106123
style={outer_style}
107124
onClick={connection_click}
125+
onKeyDown={(e) => {
126+
if (e.key === "Enter" || e.key === " ") {
127+
e.preventDefault();
128+
connection_click();
129+
}
130+
}}
131+
tabIndex={0}
108132
>
109133
{render_connection_status()}
110134
</div>
111135
);
112136
},
113137
);
114-

src/packages/frontend/app/nav-tab.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ interface Props {
2727
on_click?: () => void;
2828
style?: CSS;
2929
tooltip?: string;
30+
// ARIA attributes for tab semantics
31+
role?: string;
32+
"aria-label"?: string;
3033
}
3134

3235
export const NavTab: React.FC<Props> = React.memo((props: Props) => {
@@ -140,6 +143,15 @@ export const NavTab: React.FC<Props> = React.memo((props: Props) => {
140143
return (
141144
<div
142145
onClick={onClick}
146+
onKeyDown={(e) => {
147+
if (e.key === "Enter" || e.key === " ") {
148+
e.preventDefault();
149+
onClick();
150+
}
151+
}}
152+
role={props.role ?? "button"}
153+
aria-label={props["aria-label"]}
154+
tabIndex={0}
143155
style={outer_style}
144156
className={TOP_BAR_ELEMENT_CLASS}
145157
>

src/packages/frontend/app/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import BalanceButton from "@cocalc/frontend/purchases/balance-button";
3636
import PayAsYouGoModal from "@cocalc/frontend/purchases/pay-as-you-go/modal";
3737
import openSupportTab from "@cocalc/frontend/support/open";
3838
import { webapp_client } from "@cocalc/frontend/webapp-client";
39-
import { COLORS } from "@cocalc/util/theme";
39+
import { COLORS, SITE_NAME } from "@cocalc/util/theme";
4040
import { IS_IOS, IS_MOBILE, IS_SAFARI } from "../feature";
4141
import { ActiveContent } from "./active-content";
4242
import { ConnectionIndicator } from "./connection-indicator";
@@ -124,6 +124,7 @@ export const Page: React.FC = () => {
124124

125125
const is_commercial = useTypedRedux("customize", "is_commercial");
126126
const insecure_test_mode = useTypedRedux("customize", "insecure_test_mode");
127+
const site_name = useTypedRedux("customize", "site_name") ?? SITE_NAME;
127128

128129
function account_tab_icon(): IconName | React.JSX.Element {
129130
if (is_anonymous) {
@@ -289,6 +290,8 @@ export const Page: React.FC = () => {
289290
return (
290291
<div
291292
className="smc-right-tabs-fixed"
293+
role="region"
294+
aria-label="Top navigation controls"
292295
style={{
293296
display: "flex",
294297
flex: "0 0 auto",
@@ -362,6 +365,8 @@ export const Page: React.FC = () => {
362365
// ARIA: main element serves as the primary landmark for the entire application
363366
const body = (
364367
<main
368+
role="main"
369+
aria-label={`${site_name} application`}
365370
style={PAGE_STYLE}
366371
onDragOver={(e) => e.preventDefault()}
367372
onDrop={drop}

src/packages/frontend/i18n/common.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,12 @@ export const labels = defineMessages({
725725
description:
726726
"Short label, telling the user a possible connection has not been established.",
727727
},
728+
connected: {
729+
id: "labels.connected",
730+
defaultMessage: "Connected",
731+
description:
732+
"Short label, telling the user the connection has been established.",
733+
},
728734
connection: {
729735
id: "labels.connection",
730736
defaultMessage: "Connection",

0 commit comments

Comments
 (0)