Skip to main content

Accessibility

Every operation the side panel and the field list offer is reachable with a keyboard, and each control has an accessible name. That was not true before 3.3.0: four operations existed only as mouse gestures, and one of them (renaming a table column) had regressed in 3.2.0, which removed the text field that was its only keyboard path.

What the keyboard reaches​

OperationControl
Select a fieldthe field name in the list is a <button>, with aria-pressed reflecting selection
Rename a fielda pencil button on the row (double-click on the name still works)
Lock / unlock, remove, send-to-back / bring-to-frontbuttons, each with an aria-label naming the field
Focus a KPI sub-element (icon/title/value/caption)the sub-row label is a <button> with aria-pressed
Rename a table columna pencil button on the column chip
Write a column expression, style a column, remove a columnthe ƒx, ⋯ and × buttons on the chip
Hide the active optional tabthe × on the tab, a <button> beside the tab (it used to be a role="button" inside it, which is invalid HTML and had no tab stop)

Both rename inputs open with the text selected, so typing replaces the name rather than appending to it, and commit on Enter or blur, cancel on Escape.

Dialogs​

The three modals — the generic Modal, the ƒx expression editor built on it, and PdfPreviewModal — are real dialogs:

  • role="dialog" and aria-modal="true", so a screen reader announces a dialog instead of continuing to offer the page behind the overlay.
  • aria-labelledby points at the heading already on screen, rather than duplicating the title into an aria-label that could drift from it.
  • Focus moves into the dialog on open — onto the first focusable control, unless something inside already claimed focus (an autoFocus, which the trap deliberately does not steal). Tab and Shift+Tab are trapped inside, and focus returns to the element that opened the dialog on close.
  • Escape closes. Clicking the darkened overlay also closes, as a mouse shortcut — the overlay is marked role="presentation" rather than given a role="button", because a full-screen tab stop you cannot see is worse than no tab stop.

The behaviour lives in src/components/ui/useDialogA11y.ts and is shared, because PdfPreviewModal does not go through the Modal shell (it carries the zoom-to-fit measurement) and would otherwise have drifted — as it had: before 3.3.0 it was the only modal that did not close with Escape, and its close button was a bare × glyph with no accessible name.

What is deliberately mouse-only, and where to do it instead​

The canvas is a direct-manipulation surface. "Drag this 3mm to the right" is not a keystroke, and inventing role="button" plus an empty key handler for a drag target would only mislead a screen reader. So those gestures stay gestures — and every one of them edits something that is also a form field in the property panel:

Canvas gestureSame change, from the panel
Drag / resize a fieldX, Y, Width, Height (mm) inputs
Marquee-select over empty canvasselect in the Fields list
Drag a column edge to resizethe column's Width (mm) input
Drag a KPI sub-elementits position inputs under Style
Double-click an image to replace itthe upload button under Data
Double-click a cell to edit inlinethe cell's ƒx / footer inputs

For contributors​

jsx-a11y is enabled in .oxlintrc.json — it was simply absent from plugins, which is how 25 findings accumulated unnoticed. Fourteen rules are errors. Four are off, each with the reason written beside it in that file: the three rules above that fire on gesture surfaces, and no-autofocus (in an inline editor, moving focus to the input is the correct behaviour — the user just asked to edit that label).

interactive-supports-focus stays an error on purpose. It is the rule that caught the tab-bar ×, and it is the one that catches this whole class: an element that claims an interactive role without being focusable.

test/a11y.test.tsx guards the result — the dialog markup (asserted without a DOM, which is why ModalShell is exported separately from Modal), a source guard per operation that used to be mouse-only, and a guard that the linter plugin stays enabled.

Limits worth stating​

This is not a conformance claim. What is verified is listed above and guarded by tests; what has not been done:

  • No automated accessibility scan (axe or similar) and no screen-reader pass in CI. The guards are markup-level plus the linter.
  • The field list is a list of buttons, not a listbox with arrow-key navigation — Tab steps through rows and their action buttons.
  • No colour-contrast audit of the default theme. If you need a specific contrast ratio, the --jpd-* tokens are the place to set it (see Theming), and reset.css alone lets you supply the whole appearance yourself.