Skip to content

Commit c44f408

Browse files
committed
frontend/aria: make top projects tab a landmark and make tab+return work
1 parent daa7260 commit c44f408

File tree

6 files changed

+123
-6
lines changed

6 files changed

+123
-6
lines changed

src/dev/ARIA.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,88 @@ const { label, icon, tooltip, onClick, isRunning } = getRunStopButton();
14821482

14831483
3. **Live Regions**: Use `aria-live="polite"` for status updates and `aria-live="assertive"` only for urgent alerts. Always test with screen readers to ensure announcements are clear.
14841484

1485+
## Keyboard Event Handling & Event Propagation ✅ (2025-11-06)
1486+
1487+
### Problem Identified
1488+
1489+
When keyboard events activate menu items or navigation tabs, events were bubbling up to parent elements, causing:
1490+
1491+
1. Multiple handlers to trigger for a single keyboard action
1492+
2. Menu items activating while also triggering parent keyboard shortcuts
1493+
3. Return/Enter key causing unexpected behavior in editor context
1494+
1495+
### Solution Implemented
1496+
1497+
#### 1. Enhanced `ariaKeyDown()` Handler
1498+
1499+
**File**: `packages/frontend/app/aria.tsx`
1500+
1501+
```tsx
1502+
export function ariaKeyDown(
1503+
handler: (e?: React.KeyboardEvent | React.MouseEvent) => void,
1504+
stopPropagation: boolean = true, // ← New parameter (default: true)
1505+
): (e: React.KeyboardEvent) => void {
1506+
return (e: React.KeyboardEvent) => {
1507+
if (e.key === "Enter" || e.key === " ") {
1508+
e.preventDefault();
1509+
if (stopPropagation) {
1510+
e.stopPropagation(); // ← Prevents event bubbling
1511+
}
1512+
handler(e);
1513+
}
1514+
};
1515+
}
1516+
```
1517+
1518+
**Impact**: All navigation buttons, tabs, and custom button elements now prevent keyboard event bubbling by default. Optional parameter allows disabling if needed (backwards compatible).
1519+
1520+
#### 2. Menu Item Click Handlers
1521+
1522+
**File**: `packages/frontend/frame-editors/frame-tree/commands/manage.tsx` (line 541+)
1523+
1524+
```tsx
1525+
const onClick = async (event) => {
1526+
// Prevent event bubbling from menu item clicks
1527+
event?.stopPropagation?.();
1528+
event?.preventDefault?.();
1529+
// ... rest of handler
1530+
};
1531+
```
1532+
1533+
**Impact**: Menu items from all editor types (File, Edit, View menus, etc.) now prevent event propagation when activated.
1534+
1535+
#### 3. DropdownMenu Handler
1536+
1537+
**File**: `packages/frontend/components/dropdown-menu.tsx` (line 99+)
1538+
1539+
```tsx
1540+
const handleMenuClick: MenuProps["onClick"] = (e) => {
1541+
// Prevent event bubbling from menu clicks
1542+
e?.domEvent?.stopPropagation?.();
1543+
e?.domEvent?.preventDefault?.();
1544+
// ... rest of handler
1545+
};
1546+
```
1547+
1548+
**Impact**: Ant Design's menu click events are properly contained and don't bubble to parent components.
1549+
1550+
### Benefits
1551+
1552+
- ✅ Menu items activate correctly without side effects
1553+
- ✅ Keyboard navigation (Enter/Space) is isolated to the activated element
1554+
- ✅ Return key in menus doesn't trigger editor keyboard shortcuts
1555+
- ✅ Navigation tabs don't interfere with other page interactions
1556+
- ✅ Backwards compatible - existing code works unchanged
1557+
1558+
### Testing Notes
1559+
1560+
When keyboard testing menus:
1561+
1562+
1. Open a menu with mouse click
1563+
2. Navigate with arrow keys (Ant Design handles this)
1564+
3. Press Enter to activate item - should NOT trigger parent handlers
1565+
4. Verify the menu closes and the action executes cleanly
1566+
14851567
## Session Summary - October 28, 2025
14861568

14871569
### Session Accomplishments

src/packages/frontend/app/aria.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
* with role="button", role="tab", role="region", etc.
4343
*
4444
* @param handler - The click handler to invoke (typically your onClick function)
45+
* @param stopPropagation - Whether to prevent event bubbling (default: true)
4546
* @returns A keyboard event handler function
4647
*
4748
* @example
@@ -57,6 +58,7 @@
5758
*/
5859
export function ariaKeyDown(
5960
handler: (e?: React.KeyboardEvent | React.MouseEvent) => void,
61+
stopPropagation: boolean = true,
6062
): (e: React.KeyboardEvent) => void {
6163
return (e: React.KeyboardEvent) => {
6264
// Activate on Enter (standard button behavior) or Space (accessible alternative)
@@ -65,6 +67,11 @@ export function ariaKeyDown(
6567
// - Enter: prevents form submission or other default actions
6668
// - Space: prevents page scroll
6769
e.preventDefault();
70+
// Stop event bubbling to parent elements (default: true)
71+
// This prevents parent handlers from also triggering
72+
if (stopPropagation) {
73+
e.stopPropagation();
74+
}
6875
// Call the handler (usually onClick)
6976
handler(e);
7077
}

src/packages/frontend/components/dropdown-menu.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ export function DropdownMenu({
9797
}
9898

9999
const handleMenuClick: MenuProps["onClick"] = (e) => {
100+
// Prevent event bubbling from menu clicks
101+
e?.domEvent?.stopPropagation?.();
102+
e?.domEvent?.preventDefault?.();
103+
100104
if (e.key?.includes(STAY_OPEN_ON_CLICK)) {
101105
setOpen(true);
102106
} else {

src/packages/frontend/components/sortable-tabs.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
} from "react";
3131
import useResizeObserver from "use-resize-observer";
3232

33+
import { ariaKeyDown } from "@cocalc/frontend/app/aria";
34+
3335
export { useSortable };
3436

3537
interface Props {
@@ -130,9 +132,22 @@ export function SortableTabs(props: Props) {
130132
);
131133
}
132134

133-
export function SortableTab({ children, id, style }) {
135+
interface SortableTabProps {
136+
children: React.ReactNode;
137+
id: string | number;
138+
style?: CSSProperties;
139+
onKeyReturn?: () => void;
140+
}
141+
142+
export function SortableTab({
143+
children,
144+
id,
145+
style,
146+
onKeyReturn,
147+
}: SortableTabProps) {
134148
const { attributes, listeners, setNodeRef, transform, transition, active } =
135149
useSortable({ id });
150+
136151
return (
137152
<div
138153
ref={setNodeRef}
@@ -146,6 +161,7 @@ export function SortableTab({ children, id, style }) {
146161
}}
147162
{...attributes}
148163
{...listeners}
164+
onKeyDown={onKeyReturn ? ariaKeyDown(onKeyReturn, false) : undefined}
149165
>
150166
{children}
151167
</div>

src/packages/frontend/frame-editors/frame-tree/commands/manage.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,10 @@ export class ManageCommands {
539539
);
540540
}
541541
const onClick = async (event) => {
542+
// Prevent event bubbling from menu item clicks
543+
event?.stopPropagation?.();
544+
event?.preventDefault?.();
545+
542546
let { popconfirm } = cmd;
543547
if (popconfirm != null) {
544548
if (typeof popconfirm === "function") {

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,12 @@ export function ProjectsNav(props: ProjectsNavProps) {
326326
// });
327327

328328
return (
329-
<SortableTab key={node.key} id={node.key} style={wrapperStyle}>
329+
<SortableTab
330+
key={node.key}
331+
id={node.key}
332+
style={wrapperStyle}
333+
onKeyReturn={() => actions.set_active_tab(project_id)}
334+
>
330335
{node}
331336
</SortableTab>
332337
);
@@ -336,9 +341,9 @@ export function ProjectsNav(props: ProjectsNavProps) {
336341
}
337342

338343
return (
339-
<div
344+
<nav
345+
aria-label="Open projects"
340346
style={{
341-
overflow: "hidden",
342347
height: `${height}px`,
343348
...style,
344349
}}
@@ -360,10 +365,9 @@ export function ProjectsNav(props: ProjectsNavProps) {
360365
type={"editable-card"}
361366
renderTabBar={renderTabBar0}
362367
items={items}
363-
aria-label="Open projects"
364368
/>
365369
</SortableTabs>
366370
)}
367-
</div>
371+
</nav>
368372
);
369373
}

0 commit comments

Comments
 (0)