Put the carousel's tab stop on its items, as M3 asks

Plan step 19, containment.md C-18. The row was the focusable `region`
and the items were not focusable at all, which is the thing M3's
accessibility page draws a Don't for: "use Tab to place initial focus on
the first carousel item", "avoid focusing on the carousel container".
Each item is now `tabindex="0"` with the focus ring drawn inside it, the
row is out of the tab order, and from a focused item the arrows move one
item (moving focus with them), Home and End go to the ends, and Space or
Enter opens one that is not fully in view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 06:14:34 +02:00
co-authored by Claude Opus 5
parent ad37862a9f
commit 9875ba156e
7 changed files with 70 additions and 34 deletions
+38 -11
View File
@@ -1067,31 +1067,58 @@ document.addEventListener('alpine:init', () => {
})
},
/** The row's own keys, while the row itself has focus: a control inside an item keeps its keys. */
/**
* The items' keys, while an item itself has focus — M3: "Tab or Arrows moves to the
* previous or next carousel item; Space or Enter activates the focused carousel item".
* A control inside an item keeps its own keys, because focus is then on the control
* and not on the item.
*/
navigate(event) {
if (event.target !== this.$refs.scroller || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return
}
const index = state.items.findIndex((item) => item.element === event.target)
if (index < 0) {
return
}
if (event.key === 'Enter' || event.key === ' ') {
if (this.isMasked(index)) {
event.preventDefault()
this.scrollToItem(index)
}
return
}
const forward = state.rtl ? 'ArrowLeft' : 'ArrowRight'
const backward = state.rtl ? 'ArrowRight' : 'ArrowLeft'
const action = {
[forward]: () => this.next(),
[backward]: () => this.previous(),
Home: () => this.scrollToItem(0),
End: () => this.scrollToItem(state.items.length - 1),
const to = {
[forward]: index + 1,
[backward]: index - 1,
Home: 0,
End: state.items.length - 1,
}[event.key]
if (action) {
event.preventDefault()
action()
const target = to === undefined ? undefined : state.items[to]
if (!target) {
return
}
// The browser would jump the row to the newly focused item; the smooth scroll to
// its snap position is this script's.
event.preventDefault()
target.element.focus({ preventScroll: true })
this.scrollToItem(to)
},
/** Focus inside an item brings that item into focus, as Compose's bring-into-view does. */
reveal(event) {
const element = event.target === this.$refs.scroller ? null : event.target.closest(ITEM)
const element = event.target.closest?.(ITEM)
const index = state.items.findIndex((item) => item.element === element)
if (index >= 0 && this.isMasked(index)) {