Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5b74a7c
frontend/aria: get started annotating elements
haraldschilly Oct 24, 2025
a68d6d3
Merge remote-tracking branch 'origin/master' into aria-20251024
haraldschilly Oct 28, 2025
2ece112
frontend/aria: Complete Phase 10 - Editor Frame Infrastructure
haraldschilly Oct 28, 2025
f7b51b0
docs/aria: Add best practices for direct ARIA on components
haraldschilly Oct 28, 2025
c15b155
Merge remote-tracking branch 'origin/master' into aria-20251024
haraldschilly Oct 30, 2025
0fc1c9e
frontend/project-page: Add tab semantics to activity bar and file tab…
haraldschilly Oct 30, 2025
7ea5f91
frontend/app: Add ARIA semantics to app shell and navigation (Phase 1…
haraldschilly Oct 30, 2025
daa7260
frontend/aria: ARIA for app itself, phase 12/P1
haraldschilly Oct 30, 2025
13bec8c
frontend/aria: make top projects tab a landmark and make tab+return work
haraldschilly Nov 6, 2025
26c30b7
frontend/aria: start making autoFocus configurable
haraldschilly Nov 6, 2025
0a92469
frontend/aria: steps towards focussing on content
haraldschilly Nov 7, 2025
f66337d
frontend/aria: hotkey nav
haraldschilly Nov 7, 2025
db006b2
Merge remote-tracking branch 'origin/master' into aria-20251024
haraldschilly Nov 10, 2025
7e62e50
frontend/aria/hotkey: improve dialog …
haraldschilly Nov 11, 2025
8a47776
frontend/aria: page in general, lighthouse accessibility testing, ...
haraldschilly Nov 13, 2025
24cb036
frontend/projects/aria: keyboard nav + aria tweaks
haraldschilly Nov 13, 2025
41a841e
frontend/aria/hotkey: compute tree only when dialog is visible
haraldschilly Nov 14, 2025
436f9bd
frontend/aria/hotkey: toggle side chat with 0
haraldschilly Nov 14, 2025
e90aaf8
frontend/aria/hotkey: recent files and bugfixes
haraldschilly Nov 14, 2025
9e0a0f3
Merge remote-tracking branch 'origin/master' into aria-20251024
haraldschilly Nov 17, 2025
ed156c4
dev/ARIA: Shorten WCAG AA section and improve Lighthouse documentation
haraldschilly Nov 17, 2025
4f04af8
frontend/aria: Fix top navigation layout and note about zooming, debu…
haraldschilly Nov 17, 2025
85e6e76
Merge upstream master: resolve chat conflicts with ARIA improvements
haraldschilly Nov 20, 2025
bfbfca0
Merge remote-tracking branch 'origin/master' into aria-20251024
haraldschilly Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
frontend/aria: make top projects tab a landmark and make tab+return work
  • Loading branch information
haraldschilly committed Nov 6, 2025
commit 13bec8cdc554c23e100374461d3cc4a2a15032de
82 changes: 82 additions & 0 deletions src/dev/ARIA.md
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,88 @@ const { label, icon, tooltip, onClick, isRunning } = getRunStopButton();

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.

## Keyboard Event Handling & Event Propagation ✅ (2025-11-06)

### Problem Identified

When keyboard events activate menu items or navigation tabs, events were bubbling up to parent elements, causing:

1. Multiple handlers to trigger for a single keyboard action
2. Menu items activating while also triggering parent keyboard shortcuts
3. Return/Enter key causing unexpected behavior in editor context

### Solution Implemented

#### 1. Enhanced `ariaKeyDown()` Handler

**File**: `packages/frontend/app/aria.tsx`

```tsx
export function ariaKeyDown(
handler: (e?: React.KeyboardEvent | React.MouseEvent) => void,
stopPropagation: boolean = true, // ← New parameter (default: true)
): (e: React.KeyboardEvent) => void {
return (e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (stopPropagation) {
e.stopPropagation(); // ← Prevents event bubbling
}
handler(e);
}
};
}
```

**Impact**: All navigation buttons, tabs, and custom button elements now prevent keyboard event bubbling by default. Optional parameter allows disabling if needed (backwards compatible).

#### 2. Menu Item Click Handlers

**File**: `packages/frontend/frame-editors/frame-tree/commands/manage.tsx` (line 541+)

```tsx
const onClick = async (event) => {
// Prevent event bubbling from menu item clicks
event?.stopPropagation?.();
event?.preventDefault?.();
// ... rest of handler
};
```

**Impact**: Menu items from all editor types (File, Edit, View menus, etc.) now prevent event propagation when activated.

#### 3. DropdownMenu Handler

**File**: `packages/frontend/components/dropdown-menu.tsx` (line 99+)

```tsx
const handleMenuClick: MenuProps["onClick"] = (e) => {
// Prevent event bubbling from menu clicks
e?.domEvent?.stopPropagation?.();
e?.domEvent?.preventDefault?.();
// ... rest of handler
};
```

**Impact**: Ant Design's menu click events are properly contained and don't bubble to parent components.

### Benefits

- ✅ Menu items activate correctly without side effects
- ✅ Keyboard navigation (Enter/Space) is isolated to the activated element
- ✅ Return key in menus doesn't trigger editor keyboard shortcuts
- ✅ Navigation tabs don't interfere with other page interactions
- ✅ Backwards compatible - existing code works unchanged

### Testing Notes

When keyboard testing menus:

1. Open a menu with mouse click
2. Navigate with arrow keys (Ant Design handles this)
3. Press Enter to activate item - should NOT trigger parent handlers
4. Verify the menu closes and the action executes cleanly

## Session Summary - October 28, 2025

### Session Accomplishments
Expand Down
15 changes: 7 additions & 8 deletions src/packages/frontend/app/aria.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,9 @@
* - Works with Enter key (standard for buttons) and Space (acceptable alternative)
* - Prevents default browser behavior (e.g., page scroll on Space)
* - Single source of truth for this common accessibility pattern
*/

/**
* Create a keyboard event handler for ARIA interactive elements
*
* Returns a handler that activates click handlers when users press Enter or Space keys,
* mimicking native button behavior for custom interactive elements
* with role="button", role="tab", role="region", etc.
*
* @param handler - The click handler to invoke (typically your onClick function)
* @param stopPropagation - Whether to prevent event bubbling (default: true)
* @returns A keyboard event handler function
*
* @example
Expand All @@ -57,6 +50,7 @@
*/
export function ariaKeyDown(
handler: (e?: React.KeyboardEvent | React.MouseEvent) => void,
stopPropagation: boolean = true,
): (e: React.KeyboardEvent) => void {
return (e: React.KeyboardEvent) => {
// Activate on Enter (standard button behavior) or Space (accessible alternative)
Expand All @@ -65,6 +59,11 @@ export function ariaKeyDown(
// - Enter: prevents form submission or other default actions
// - Space: prevents page scroll
e.preventDefault();
// Stop event bubbling to parent elements (default: true)
// This prevents parent handlers from also triggering
if (stopPropagation) {
e.stopPropagation();
}
// Call the handler (usually onClick)
handler(e);
}
Expand Down
8 changes: 3 additions & 5 deletions src/packages/frontend/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -362,11 +362,9 @@ export const Page: React.FC = () => {

// Children must define their own padding from navbar and screen borders
// Note that the parent is a flex container
// ARIA: main element serves as the primary landmark for the entire application
// ARIA: content container (main landmarks are defined at the page level below)
const body = (
<main
role="main"
aria-label={`${site_name} application`}
<div
style={PAGE_STYLE}
onDragOver={(e) => e.preventDefault()}
onDrop={drop}
Expand Down Expand Up @@ -408,7 +406,7 @@ export const Page: React.FC = () => {
<PayAsYouGoModal />
<PopconfirmModal />
<SettingsModal />
</main>
</div>
);
return (
<ClientContext.Provider value={{ client: webapp_client }}>
Expand Down
4 changes: 4 additions & 0 deletions src/packages/frontend/components/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ export function DropdownMenu({
}

const handleMenuClick: MenuProps["onClick"] = (e) => {
// Prevent event bubbling from menu clicks
e?.domEvent?.stopPropagation?.();
e?.domEvent?.preventDefault?.();

if (e.key?.includes(STAY_OPEN_ON_CLICK)) {
setOpen(true);
} else {
Expand Down
18 changes: 17 additions & 1 deletion src/packages/frontend/components/sortable-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
} from "react";
import useResizeObserver from "use-resize-observer";

import { ariaKeyDown } from "@cocalc/frontend/app/aria";

export { useSortable };

interface Props {
Expand Down Expand Up @@ -130,9 +132,22 @@ export function SortableTabs(props: Props) {
);
}

export function SortableTab({ children, id, style }) {
interface SortableTabProps {
children: React.ReactNode;
id: string | number;
style?: CSSProperties;
onKeyReturn?: () => void;
}

export function SortableTab({
children,
id,
style,
onKeyReturn,
}: SortableTabProps) {
const { attributes, listeners, setNodeRef, transform, transition, active } =
useSortable({ id });

return (
<div
ref={setNodeRef}
Expand All @@ -146,6 +161,7 @@ export function SortableTab({ children, id, style }) {
}}
{...attributes}
{...listeners}
onKeyDown={onKeyReturn ? ariaKeyDown(onKeyReturn, false) : undefined}
>
{children}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,10 @@ export class ManageCommands {
);
}
const onClick = async (event) => {
// Prevent event bubbling from menu item clicks
event?.stopPropagation?.();
event?.preventDefault?.();

let { popconfirm } = cmd;
if (popconfirm != null) {
if (typeof popconfirm === "function") {
Expand Down
6 changes: 3 additions & 3 deletions src/packages/frontend/project/page/activity-bar-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,11 @@ export default function ProjectTabs(props: PTProps) {
//if (openFiles.size == 0) return <></>;

return (
<div
<nav
className="smc-file-tabs"
style={{
width: "100%",
height: "40px",
overflow: "hidden",
}}
>
<div style={{ display: "flex" }}>
Expand All @@ -86,7 +85,7 @@ export default function ProjectTabs(props: PTProps) {
<ChatIndicatorTab activeTab={activeTab} project_id={project_id} />
</div>
</div>
</div>
</nav>
);
}

Expand Down Expand Up @@ -230,6 +229,7 @@ export function VerticalFixedTabs({
role="tab"
aria-selected={isActive}
aria-controls={`activity-panel-${name}`}
tabIndex={0}
/>
);
if (tab != null) items.push(tab);
Expand Down
30 changes: 30 additions & 0 deletions src/packages/frontend/project/page/file-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
useRedux,
useTypedRedux,
} from "@cocalc/frontend/app-framework";
import { ariaKeyDown } from "@cocalc/frontend/app/aria";
import { Icon, IconName, r_join } from "@cocalc/frontend/components";
import ComputeServerSpendRate from "@cocalc/frontend/compute/spend-rate";
import { IS_MOBILE } from "@cocalc/frontend/feature";
Expand Down Expand Up @@ -188,6 +189,7 @@ interface Props0 {
role?: string;
"aria-selected"?: boolean;
"aria-controls"?: string;
tabIndex?: number;
}
interface PropsPath extends Props0 {
path: string;
Expand Down Expand Up @@ -300,6 +302,28 @@ export function FileTab(props: Readonly<Props>) {
}
}

function handleKeyActivation() {
if (actions == null) return;
if (path != null) {
actions.set_active_tab(path_to_tab(path));
track("switch-to-file-tab", {
project_id,
path,
how: "keyboard",
});
} else if (name != null) {
if (flyout != null && FIXED_PROJECT_TABS[flyout].noFullPage) {
// this tab can't be opened in a full page
actions?.toggleFlyout(flyout);
} else if (flyout != null && actBar !== "both") {
// keyboard activation just activates, no modifier key logic
setActiveTab(name);
} else {
setActiveTab(name);
}
}
}

function renderFlyoutCaret() {
if (flyout == null || actBar !== "both") return;

Expand Down Expand Up @@ -470,6 +494,12 @@ export function FileTab(props: Readonly<Props>) {
cocalc-test={label}
onClick={click}
onMouseUp={onMouseUp}
onKeyDown={
props.tabIndex != null
? ariaKeyDown(handleKeyActivation, false)
: undefined
}
tabIndex={props.tabIndex}
role={props.role}
aria-selected={props["aria-selected"]}
aria-controls={props["aria-controls"]}
Expand Down
Loading
Loading