Skip to content

feat(a11y): keyboard & ARIA pass on the choice and macro GUIs (#1250)#1251

Merged
chhoumann merged 7 commits into
masterfrom
1250-accessibility-keyboard-aria-pass-on-the-choice-macro-guis-post-svelte-5
May 30, 2026
Merged

feat(a11y): keyboard & ARIA pass on the choice and macro GUIs (#1250)#1251
chhoumann merged 7 commits into
masterfrom
1250-accessibility-keyboard-aria-pass-on-the-choice-macro-guis-post-svelte-5

Conversation

@chhoumann
Copy link
Copy Markdown
Owner

@chhoumann chhoumann commented May 29, 2026

Closes #1250.

The Svelte 5 rewrite (#1248) was deliberately behavior-preserving and didn't touch accessibility. This PR makes the choice & macro editors fully keyboard operable and replaces the leftover faux-button / ARIA patterns.

What changed

Shared components

  • IconButton.svelte — a real <button> replacing every <div|span role="button" tabindex onclick onkeypress> faux-button (Enter/Space, focus, and aria-label for free; optional aria-pressed/aria-haspopup).
  • DragHandle.svelte — a real <button> that keeps pointer drag and adds keyboard reorder (ArrowUp/ArrowDown, with preventDefault+stopPropagation).

Keyboard operability (CRUD + reorder)

  • Reorder both lists from the keyboard via the drag handle; persists through the same path as a pointer-drag finalize. Set autoAriaDisabled: true on both dndzones so svelte-dnd-action stops injecting its own conflicting role/tabindex/keyboard handlers onto rows.
  • Context menu (Rename / Move-to, previously mouse-only) is now reachable via a new "More options" button → Menu.showAtPosition. Removed the dead role="button" row that advertised a menu it couldn't open.
  • aria-pressed on the command-palette toggle; aria-expanded on the Multi collapse toggle; valid <li> command rows (was <ol> > <div> > <li>); the click-to-rename modal <h2> headers (MacroBuilder, ChoiceBuilder, both AI Assistant modals) now wrap a real <button>; FolderList faux-button migrated.

Lint

  • Enabled svelte/valid-compile (eslint-plugin-svelte v3 has no dedicated a11y-* rules — this surfaces the Svelte 5 compiler's a11y_* warnings) as a regression ratchet. Shared button CSS is global with doubled-class specificity so themes can't re-skin the new native buttons.

Bug fixed along the way (data loss)

Reordering a Multi nested inside another Multi (depth ≥ 2) bubbled the root array into an ancestor's onReorderChoices override, overwriting its children — silent data loss + a self-referential cycle persisted to data.json. This is a pre-existing rewrite bug that affected pointer drag too; the new keyboard reorder just exposed it. Fixed by threading a top-level rootReorder handler through every nesting level. Covered by a regression test that fails on the old code and passes on the fix.

Verification

  • svelte-check: 0 errors / 0 warnings
  • eslint (incl. valid-compile): clean
  • vitest: 1452 passed / 31 skipped, incl. 6 new test files (IconButton, DragHandle, keyboard reorder, depth ≥ 2 nested-reorder regression, collapse aria-expanded, keyboard-opened menu)
  • production build succeeds

Notes

  • UI change: choice rows gain a subtle "More options" (⋮) button — the keyboard entry point for Rename/Move; right-click still works for mouse users.
  • Please verify pointer drag + keyboard reorder manually in the dev vault — jsdom can't exercise the real pointer-event sequence.
  • Intentionally deferred low/edge nits: no focus-return after the Obsidian context menu closes (platform limitation), no aria-controls on the collapse toggle, and the rare case of a Multi named with a markdown link nesting an <a> inside the toggle button.

Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Keyboard reordering for commands and choices via ArrowUp/ArrowDown.
    • New accessible DragHandle and IconButton components for list controls.
  • Improvements

    • Headings use real buttons for rename actions with ARIA labels.
    • Unified configure/delete controls, improved focus-visible, spacing, drag cursor, and mobile decluttering.
    • Context menus can be anchored to elements; nested reorders now persist.
  • Tests

    • Added tests for reordering, DragHandle, IconButton, context menus, and choice list accessibility.

Review Change Stack

Make the choice and macro editors fully keyboard operable and replace the
faux-button / ARIA patterns left after the Svelte 5 migration (#1250).

- Replace role="button"+tabindex+onkeypress faux-buttons with native
  <button>s via two shared components, IconButton and DragHandle
  (Enter/Space activation, focus, and aria-label for free).
- Keyboard reorder for both lists: ArrowUp/ArrowDown on a row's drag
  handle moves it and persists via the same path as a pointer drag. Set
  autoAriaDisabled on the dndzones so svelte-dnd-action stops injecting
  its own conflicting role/tabindex/keyboard handling onto rows.
- Keyboard-reachable context menu (Rename / Move-to) via a new
  "More options" button -> Menu.showAtPosition; drop the dead
  role="button" row that announced a menu it couldn't open.
- aria-pressed on the command-palette toggle; aria-expanded on the Multi
  collapse toggle; valid <li> command rows (was <ol> > <div> > <li>);
  keyboard-operable rename in the modal <h2> headers; migrate FolderList.
- Enable svelte/valid-compile as an a11y / regression lint ratchet.

Also fixes a pre-existing data-loss bug this work exposed: reordering a
Multi nested inside another Multi (depth >= 2) bubbled the root array into
an ancestor's onReorderChoices override, overwriting its children (a cycle
that got persisted to data.json). Nested reorders now persist through a
threaded top-level rootReorder handler; this affected both keyboard
reorder and pointer drag.

Tests added: IconButton, DragHandle, keyboard reorder, a depth>=2 nested
reorder regression, collapse aria-expanded, and the keyboard-opened menu.
svelte-check, eslint (incl. valid-compile), 1452 unit tests, and the
production build all pass.

Closes #1250
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds accessible IconButton and DragHandle components, replaces faux-button UI with native buttons in headers and list controls, implements arrow-key reordering for commands and choices (with persistence), centralizes context-menu construction for anchored/keyboard opening, expands tests and stubs, updates styles, and enables svelte/valid-compile in ESLint.

Changes

Keyboard-accessible UI and reordering

Layer / File(s) Summary
Shared UI components, tests, and styles
src/gui/components/IconButton.svelte, src/gui/components/IconButton.test.ts, src/gui/components/DragHandle.svelte, src/gui/components/DragHandle.test.ts, src/styles.css, tests/obsidian-stub.ts
IconButton and DragHandle added as native buttons with typed props, ARIA attributes, keyboard handling, and tests; shared CSS adds icon-button resets, focus-visible outlines, drag-handle cursor rules, and mobile decluttering; test obsidian stub exports Menu/MenuItem for anchored menu tests.
Modal header rename affordances & FolderList delete
src/gui/ChoiceBuilder/FolderList.svelte, src/gui/ChoiceBuilder/choiceBuilder.ts, src/gui/MacroGUIs/MacroBuilder.ts, src/gui/MacroGUIs/AIAssistantCommandSettingsModal.ts, src/gui/MacroGUIs/AIAssistantInfiniteCommandSettingsModal.ts
Header rename affordances moved into real <button> elements inside <h2> headings; FolderList delete actions now use IconButton with accessible labels and click handlers.
Command list keyboard reordering & command rows
src/gui/MacroGUIs/CommandList.svelte, src/gui/MacroGUIs/CommandList.keyboardReorder.test.ts, src/gui/MacroGUIs/Components/*, src/gui/MacroGUIs/Components/macroCommands.test.ts, src/styles.css
CommandList adds moveCommand(id, direction), normalizes startDrag to a no-arg callback, and sets dndzone accessibility options; every command row component accepts optional onMoveUp/onMoveDown and uses IconButton/DragHandle. Tests validate keyboard reordering, clamping, and persistence.
Choice list keyboard reordering, context menus, and right-buttons
src/gui/choiceList/ChoiceList.svelte, src/gui/choiceList/ChoiceList.a11y.test.ts, src/gui/choiceList/ChoiceListItem.svelte, src/gui/choiceList/MultiChoiceListItem.svelte, src/gui/choiceList/ChoiceItemRightButtons.svelte, src/gui/choiceList/contextMenu.ts, src/styles.css
ChoiceList adds moveChoice(choice, direction) and optional rootReorder threading; ChoiceListItem/MultiChoiceListItem convert faux-button semantics to native controls and split context-menu openers into mouse and anchored variants; contextMenu.ts centralizes menu building for both showAtMouseEvent and showAtPosition. Accessibility-focused tests added for nested reordering, collapse toggles, mobile declutter, and keyboard-anchored menus.
ESLint Svelte validation rule
eslint.config.mjs
Adds svelte/valid-compile: 'error' to the Svelte ESLint rules with documentation comments describing its baseline and scope.

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • chhoumann/quickadd#1244: Overlaps on CommandList.svelte reorder/persistence behavior and handling of dnd shadow placeholders.
  • chhoumann/quickadd#1243: Related changes to startDrag typing and drag-handle wiring in command components.
  • chhoumann/quickadd#1248: Larger Svelte 5 UI migration that touches the same components and accessibility groundwork.

Suggested labels

released

Poem

🐰 I stitched buttons proper, labels tidy and clear,
Arrow keys now scuttle — reorder draws near.
Modals and menus answer to keyboard's soft tap,
Icon buttons hum, drag handles snap.
A rabbit's small hop for a smoother UX lap.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly describes the main change: accessibility/keyboard/ARIA improvements to choice and macro GUIs, matching the scope of the linked issue and changeset.
Linked Issues check ✅ Passed Changes fully satisfy issue #1250 objectives: native button/drag components replace role=button patterns, keyboard operability added for CRUD/reorder, ARIA attributes improved, eslint rules enabled, modal headers fixed, and verified with tests.
Out of Scope Changes check ✅ Passed All changes are in-scope for #1250: accessibility/keyboard/ARIA improvements, native button migration, drag handle reordering, context menu accessibility, and supporting infrastructure like test files are directly aligned with stated objectives.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 1250-accessibility-keyboard-aria-pass-on-the-choice-macro-guis-post-svelte-5

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 435b557bdc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gui/components/DragHandle.svelte Outdated
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 29, 2026

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: e9e09cf
Status: ✅  Deploy successful!
Preview URL: https://29a5904c.quickadd.pages.dev
Branch Preview URL: https://1250-accessibility-keyboard.quickadd.pages.dev

View logs

svelte-dnd-action sets zoneItemTabIndex (default 0) on every dndzone row
even with autoAriaDisabled, so each choice/command row was a focusable tab
stop that does nothing. Pass zoneItemTabIndex:-1 so Tab visits only the
action buttons and drag handle inside each row.

Verified in the dev vault: rows are now tabindex=-1, the per-row buttons
remain focusable, no "Context menu for ..." aria-label/tooltip remains
(removed with the old role="button" row), and no runtime errors.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/gui/MacroGUIs/Components/ConditionalCommand.svelte`:
- Around line 43-60: The three IconButton instances for editing ("Edit
condition", "Edit then branch", "Edit else branch") produce identical aria
labels; update them to include the command identity so screen readers can
distinguish rows—e.g. build the label/aria-label using command.name (or another
unique command property) when rendering the IconButton used in
onConfigureCondition, onEditThenBranch, and onEditElseBranch; ensure the label
string includes both the action and command.name so the accessible name becomes
unique per row.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1eccd938-7721-4b6d-b982-cd0e0f31aed0

📥 Commits

Reviewing files that changed from the base of the PR and between 40da1db and 435b557.

📒 Files selected for processing (28)
  • eslint.config.mjs
  • src/gui/ChoiceBuilder/FolderList.svelte
  • src/gui/ChoiceBuilder/choiceBuilder.ts
  • src/gui/MacroGUIs/AIAssistantCommandSettingsModal.ts
  • src/gui/MacroGUIs/AIAssistantInfiniteCommandSettingsModal.ts
  • src/gui/MacroGUIs/CommandList.keyboardReorder.test.ts
  • src/gui/MacroGUIs/CommandList.svelte
  • src/gui/MacroGUIs/Components/AIAssistantCommand.svelte
  • src/gui/MacroGUIs/Components/ConditionalCommand.svelte
  • src/gui/MacroGUIs/Components/NestedChoiceCommand.svelte
  • src/gui/MacroGUIs/Components/OpenFileCommand.svelte
  • src/gui/MacroGUIs/Components/StandardCommand.svelte
  • src/gui/MacroGUIs/Components/UserScriptCommand.svelte
  • src/gui/MacroGUIs/Components/WaitCommand.svelte
  • src/gui/MacroGUIs/Components/macroCommands.test.ts
  • src/gui/MacroGUIs/MacroBuilder.ts
  • src/gui/choiceList/ChoiceItemRightButtons.svelte
  • src/gui/choiceList/ChoiceList.a11y.test.ts
  • src/gui/choiceList/ChoiceList.svelte
  • src/gui/choiceList/ChoiceListItem.svelte
  • src/gui/choiceList/MultiChoiceListItem.svelte
  • src/gui/choiceList/contextMenu.ts
  • src/gui/components/DragHandle.svelte
  • src/gui/components/DragHandle.test.ts
  • src/gui/components/IconButton.svelte
  • src/gui/components/IconButton.test.ts
  • src/styles.css
  • tests/obsidian-stub.ts

Comment thread src/gui/MacroGUIs/Components/ConditionalCommand.svelte
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 7 additional findings.

Open in Devin Review

The shared DragHandle armed the list's pointer drag by calling startDrag(e)
on pointerdown, and startDrag called e.preventDefault(). preventDefault on
pointerdown suppresses the compatibility mousedown event in Chromium, and
svelte-dnd-action starts its drag from mousedown (pointerAction attaches
mousedown/touchstart only) — so the drag silently stopped working.

The pre-refactor choice-row handle passed no event (never preventDefaulted
on pointerdown), which is why drag worked there. Fix: DragHandle fires
onDragStart() with no event purely to flip dragDisabled=false, and the
startDrag handlers no longer call preventDefault (the library handles
preventDefault during the actual drag). Drop the now-unused event params.

Regression test: DragHandle "arms the drag on pointerdown WITHOUT preventing
default" asserts pointerdown is not canceled. Verified in the dev vault (no
console errors); svelte-check, eslint, 1452 tests, and the build pass.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/gui/components/DragHandle.svelte (1)

1-66: ⚡ Quick win

Use tab indentation for this new file.

This new .svelte file uses 4-space indentation throughout. As per coding guidelines, src/**/*.{ts,tsx,svelte,css,js}: "Use tab indentation and LF endings (see .editorconfig)". Please convert to tabs to match the project's editor settings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gui/components/DragHandle.svelte` around lines 1 - 66, The file
DragHandle.svelte uses 4-space indentation but the project requires tabs and LF
endings; update the file to replace the 4-space indents with tab characters
throughout (including inside the <script> block, JSX-like markup, and any nested
blocks) while preserving all identifiers and behavior (props: label,
dragDisabled, onDragStart, onMoveUp, onMoveDown, size; derived const
reorderable; function onKeyDown; and the <button> with its attributes and event
handlers). Ensure line endings are LF and re-run a quick lint/format to confirm
no functional changes were introduced.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/gui/components/DragHandle.svelte`:
- Around line 1-66: The file DragHandle.svelte uses 4-space indentation but the
project requires tabs and LF endings; update the file to replace the 4-space
indents with tab characters throughout (including inside the <script> block,
JSX-like markup, and any nested blocks) while preserving all identifiers and
behavior (props: label, dragDisabled, onDragStart, onMoveUp, onMoveDown, size;
derived const reorderable; function onKeyDown; and the <button> with its
attributes and event handlers). Ensure line endings are LF and re-run a quick
lint/format to confirm no functional changes were introduced.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 367ef321-c758-4a3d-aea5-d33982067dca

📥 Commits

Reviewing files that changed from the base of the PR and between 15425a8 and 9f38331.

📒 Files selected for processing (10)
  • src/gui/MacroGUIs/CommandList.svelte
  • src/gui/MacroGUIs/Components/AIAssistantCommand.svelte
  • src/gui/MacroGUIs/Components/ConditionalCommand.svelte
  • src/gui/MacroGUIs/Components/NestedChoiceCommand.svelte
  • src/gui/MacroGUIs/Components/OpenFileCommand.svelte
  • src/gui/MacroGUIs/Components/StandardCommand.svelte
  • src/gui/MacroGUIs/Components/UserScriptCommand.svelte
  • src/gui/MacroGUIs/Components/WaitCommand.svelte
  • src/gui/components/DragHandle.svelte
  • src/gui/components/DragHandle.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/gui/MacroGUIs/Components/WaitCommand.svelte
  • src/gui/MacroGUIs/Components/NestedChoiceCommand.svelte
  • src/gui/MacroGUIs/Components/ConditionalCommand.svelte
  • src/gui/components/DragHandle.test.ts

chhoumann added 4 commits May 30, 2026 00:31
Converting the row icons from <div>/<span> to native <button> meant Obsidian's
mobile touch-target sizing (a high-specificity .is-mobile settings rule,
padding:10px -> 40px boxes) inflated every icon, so six 40px buttons crushed
the choice name into a one-word-per-line column. The old faux-buttons were
never touch-sized. Force the compact reset (padding/min-*/width/height) with
!important so it wins over that rule; the buttons render at icon size again
and names wrap normally. Verified via mobile screenshot in the dev vault.
On mobile the six per-row icons were cramped and tiny. Hide the four inline
action icons (toggle / configure / duplicate / delete) on .is-mobile and keep
only the "More options" (⋮) menu — which already exposes every action incl.
rename & move — plus the drag handle, both enlarged to 36px touch targets.
Names now get the full row width. Desktop is unchanged (rules are
.is-mobile-scoped); macro command rows are unaffected (no menu). Verified via
mobile screenshot in the dev vault.

Test: asserts the secondary action buttons carry the qa-row-secondary-action
collapse hook while More / drag handle do not.
On mobile the drag handle next to the More (⋮) menu felt redundant. Replace it
with the native mobile gesture: long-press the row to reorder. Both lists set
delayTouchStart:200 so a hold initiates the drag while a quick swipe still
scrolls, and dragging is enabled by default on .is-mobile (no handle to arm).
The handle is hidden on mobile (choice and command rows), leaving choice rows
as just name + ⋮.

Desktop is unchanged: the handle stays and arms the drag on grab (the shared
state is now a `dragArmed` flag with a derived `dragDisabled`, behaviourally
identical to before). Platform.isMobile gates the difference; added to the
obsidian test stub (defaults to desktop so existing tests are unaffected).
Addresses CodeRabbit review: the three Edit buttons on a conditional row had
static aria-labels ("Edit condition"/"Edit then branch"/"Edit else branch"),
identical across rows. The reviewer suggested appending command.name, but a
conditional's name defaults to the non-unique "If condition", so that wouldn't
disambiguate. Use the condition summary instead (the per-row-unique identifier
already shown in the row): "Edit condition for <summary>", etc.

Updated the unit assertions (macroCommands, CommandList.conditional) and the
e2e selector (conditional-branch-persistence) — the latter now matches the
aria-label by prefix so it survives the summary suffix and future wording.

Verified via adversarial review of the change: svelte-check 0/0, eslint clean,
1453 unit tests pass, build OK.
@chhoumann chhoumann merged commit 9247e00 into master May 30, 2026
8 checks passed
@chhoumann chhoumann deleted the 1250-accessibility-keyboard-aria-pass-on-the-choice-macro-guis-post-svelte-5 branch May 30, 2026 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Accessibility / keyboard-ARIA pass on the choice & macro GUIs (post Svelte 5)

1 participant