Walk a chip set with the arrow keys
M3's chip keyboard table makes a set one tab stop that the arrows move through; ten filter chips cost ten Tab presses today. The set now keeps a roving tabindex — re-applied whenever a Livewire morph rewrites the chips — and walks left, right, Home and End, following the writing direction. The input chip's remove button also grows to a full 48x48 target, where it was 34px wide. Plan step 20, findings IN-13 and IN-14. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
47ed4603d0
commit
dd3d8afdb9
+76
-3
@@ -9,6 +9,12 @@
|
||||
* A scrolling set marks the edges it can still scroll towards (`data-scroll-start`,
|
||||
* `data-scroll-end`), and the set fades those edges. Its row carries `wire:ignore.self`, so a
|
||||
* Livewire morph keeps the marks.
|
||||
*
|
||||
* A set is also one tab stop with a roving tabindex, and the arrow keys walk its controls, which is
|
||||
* M3's chip keyboard table ("Arrows: moves focus between chips"; "only one chip can be in focus
|
||||
* even though many can be selected"). Left and right follow the writing direction; the ring wraps,
|
||||
* and Home and End go to its ends. The roving mark is re-applied whenever the set changes, because
|
||||
* a Livewire morph rewrites the chips underneath it.
|
||||
*/
|
||||
const CONTROLS = 'button:not(:disabled), a[href]:not([tabindex="-1"]), input:not(:disabled):not([type="hidden"])'
|
||||
|
||||
@@ -143,15 +149,82 @@ document.addEventListener('alpine:init', () => {
|
||||
resize.observe(row)
|
||||
this.observers.push(() => resize.disconnect())
|
||||
|
||||
const mutation = new MutationObserver(mark)
|
||||
mutation.observe(row, { childList: true, subtree: true })
|
||||
const settle = () => {
|
||||
mark()
|
||||
this.rove()
|
||||
}
|
||||
|
||||
const mutation = new MutationObserver(settle)
|
||||
mutation.observe(row, { childList: true, subtree: true, attributeFilter: ['tabindex', 'disabled'] })
|
||||
this.observers.push(() => mutation.disconnect())
|
||||
|
||||
mark()
|
||||
settle()
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this.observers.forEach((stop) => stop())
|
||||
},
|
||||
|
||||
/** Everything in the set the keyboard can reach, in the order it is written. */
|
||||
controls() {
|
||||
return [...this.$el.querySelectorAll(CONTROLS)]
|
||||
},
|
||||
|
||||
/** One tab stop: the control focus is on, or the first one, is the only one Tab reaches. */
|
||||
rove(focused = null) {
|
||||
const controls = this.controls()
|
||||
|
||||
if (controls.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const current = (focused !== null && controls.includes(focused) ? focused : null) ?? controls.find((control) => control.tabIndex === 0) ?? controls[0]
|
||||
|
||||
// Only where it differs: the mutation observer watches tabindex, and writing it back
|
||||
// every time would call this from its own changes.
|
||||
for (const control of controls) {
|
||||
const wanted = control === current ? 0 : -1
|
||||
|
||||
if (control.tabIndex !== wanted) {
|
||||
control.tabIndex = wanted
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
key(event) {
|
||||
if (event.ctrlKey || event.metaKey || event.altKey) {
|
||||
return
|
||||
}
|
||||
|
||||
const controls = this.controls()
|
||||
const index = controls.indexOf(document.activeElement)
|
||||
|
||||
if (controls.length === 0 || index === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const forwards = getComputedStyle(this.$el).direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight'
|
||||
const backwards = forwards === 'ArrowRight' ? 'ArrowLeft' : 'ArrowRight'
|
||||
|
||||
let next = null
|
||||
|
||||
if (event.key === forwards || event.key === 'ArrowDown') {
|
||||
next = (index + 1) % controls.length
|
||||
} else if (event.key === backwards || event.key === 'ArrowUp') {
|
||||
next = (index - 1 + controls.length) % controls.length
|
||||
} else if (event.key === 'Home') {
|
||||
next = 0
|
||||
} else if (event.key === 'End') {
|
||||
next = controls.length - 1
|
||||
}
|
||||
|
||||
if (next === null) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
this.rove(controls[next])
|
||||
controls[next].focus()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user