Skip to content

fix(table): make the column resize handle keyboard and touch accessible - #227

Merged
ndlabdev merged 2 commits into
devfrom
fix/226-table-resize-a11y
Sep 25, 2026
Merged

ndlabdev merged 2 commits into
devfrom
fix/226-table-resize-a11y

Conversation

@ndlabdev

Copy link
Copy Markdown
Owner

Summary

The column resize handle was a bare span carrying only onmousedown, with no role, no tabindex and no key handler. I measured it before changing anything, with a mouse drag as the control case that proves the probe works:

Table  resize handle : {"role":null,"tabindex":null,"ariaLabel":null,"ariaValueNow":null}
focusable            : false

column width          : 150
after 3 arrow presses : 150   (unchanged)
after an 80px drag    : 230   (changed, so the mouse path works)

Tabbing from a button placed immediately above the table jumped straight past the whole table into the next component, so the table offered no keyboard stop for resizing at all.

The same wiring broke pointer input other than a mouse, because the drag listened for mousemove and mouseup on document:

after a touch pointer drag : 150   (unchanged)
after touchstart           : 150   (unchanged)
handle has touch-none      : true

The touch-none class suggests touch was intended. As it stood the class blocked scrolling that began on the handle without offering anything back.

Closes #226

Type of change

  • 🐛 Bug fix
  • ✨ New feature / component
  • 📖 Documentation
  • ♻️ Refactor / chore
  • ⚠️ Breaking change

Changes

  • The handle is now a focusable separator with aria-orientation, aria-valuenow, aria-valuemin, aria-valuemax and aria-valuetext, so assistive technology can announce the current width.
  • Arrow keys resize by 16px, or 64px while shift is held. Home goes to minWidth and End to maxWidth, and End is inert on a column that sets no maximum.
  • The drag moves onto usePointerDrag, the hook ColorPicker and Resizable already use, so touch and pen work instead of a mouse alone. This also removes the manual document listeners.
  • Width clamping now lives in one place, shared by the pointer and keyboard paths.
  • resizing is a plain local instead of $state, since nothing reactive reads it.

Accessibility notes

The svelte-ignore for static element interactions is gone, because the element is genuinely interactive now. One ignore remains, for the noninteractive tabindex rule, which reads a separator as passive and does not account for one carrying aria-valuenow. That is the same false positive Resizable already suppresses, so the two controls are now consistent.

Checklist

  • Linked the related issue (Closes #226)
  • pnpm check passes (0 errors, 0 warnings)
  • pnpm lint passes
  • pnpm test passes
  • Added or updated tests for the change
  • Updated CHANGELOG.md under [Unreleased]
  • Followed component conventions (no comments outside *.types.ts, Material 3 design tokens)

Notes

Verified on a throwaway page driven through a real browser, with one column bounded only at the bottom and another bounded at both ends:

Name (min 80, no max)
  200 -> Arrow 216 (+16) -> Shift+Arrow 280 (+64) -> Home 80 -> End 80 (inert, correct)

Role (min 100, max 260)
  160 -> End 260 (hits the max) -> Arrow 260 (does not exceed it)

touch pointer drag of 70px : 80 -> 150

Seven regression tests cover the separator attributes, focusability, arrow resizing, the shift step, clamping at the minimum, a touch pointer drag, and that a column without resizable renders no handle. The probe page is deleted.

The handle was a bare span carrying only onmousedown, with no role, no
tabindex and no key handler. Keyboard users could not reach it at all,
let alone resize a column. A probe measured the column staying at 150px
after three arrow presses while a mouse drag moved it to 230px, and one
Tab from a button above the table jumped straight past the whole table.

The drag also listened for mousemove and mouseup on document, so touch
and pen never started a resize, even though the element already carried
touch-none and therefore blocked scrolling that began on it.

It is now a focusable separator with aria-orientation, aria-valuenow,
aria-valuemin, aria-valuemax and aria-valuetext, resized by the arrow
keys with a larger step while shift is held, and by Home and End for the
bounds. The drag moves onto usePointerDrag, which ColorPicker and
Resizable already use, so every pointer type works.

The svelte-ignore for static element interactions is gone, since the
element is now interactive. One remains for the noninteractive tabindex
rule, which misreads a separator carrying aria-valuenow as passive. That
is the same false positive Resizable already suppresses.

Closes #226
@ndlabdev ndlabdev added bug Something isn't working area: a11y Accessibility (ARIA, keyboard, screen readers) priority: P1 High — important, schedule soon labels Sep 25, 2026
@ndlabdev ndlabdev self-assigned this Sep 25, 2026
@ndlabdev
ndlabdev merged commit 21a2416 into dev Sep 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: a11y Accessibility (ARIA, keyboard, screen readers) bug Something isn't working priority: P1 High — important, schedule soon

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant