Draw the slider on M3's own specs table

The md handle is 52px, not the 44 the MDC tokens gave it; the value
indicator is M3's 44x48 label container, not Flutter's 32px pill; and a
stop indicator, a tick and the inset icon now take the "on" colour of the
part of the track they sit on, which M3's nine colour roles spell out and
the library had inverted. Space held with an arrow moves by the same
large interval as PageUp, M3's "Space & Arrows" row.

Plan step 20, findings IN-04, IN-05, IN-06 and IN-24.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 05:55:41 +02:00
co-authored by Claude Fable 5.1
parent da4952e684
commit 66f9d36a37
4 changed files with 114 additions and 33 deletions
+60 -4
View File
@@ -18,7 +18,9 @@
* - Range: the handles never cross; an input that would pass the other is held at its value
* before any listener (x-model, wire:model) reads it.
* - PageUp and PageDown move by Compose's page: a tenth of the steps, at least one and at most
* ten (arrows, Home and End are the inputs' own).
* ten (arrows, Home and End are the inputs' own). An arrow pressed while Space is held moves by
* that same large interval, which is M3's "Space & Arrows" keyboard row; in a right-to-left
* slider the left and right arrows swap, as they do natively.
* - Ticks hide while they would sit closer than 8px (Material Components' tick_min_spacing).
*
* ---------------------------------------------------------------------------------------
@@ -203,6 +205,8 @@ function slider(root) {
let release = null
// Focus this script gave an input on a press, which draws no ring until a key is pressed.
let focusedByPointer = null
// Space held down, which turns the arrows into M3's large interval.
let spaceHeld = false
const listen = (target, type, handler, capture = false) => {
target.addEventListener(type, handler, capture)
@@ -386,15 +390,31 @@ function slider(root) {
listen(input, 'focus', () => showFocus(input, focusedByPointer !== input && input.matches(':focus-visible')))
listen(input, 'blur', () => {
focusedByPointer = null
spaceHeld = false
showFocus(input, false)
})
// slideOnKeyEvents: PageUp and PageDown move a tenth of the steps, one to ten of them.
// slideOnKeyEvents: PageUp and PageDown move a tenth of the steps, one to ten of them
// and so do the arrows while Space is held, M3's large interval.
listen(input, 'keydown', (event) => {
focusedByPointer = null
showFocus(input, true)
if (!['PageUp', 'PageDown'].includes(event.key) || input.disabled) {
if (input.disabled) {
return
}
if (event.key === ' ' || event.code === 'Space') {
// Space alone does nothing to a range input; hold the page still while it waits.
spaceHeld = true
event.preventDefault()
return
}
const direction = largeStepDirection(event)
if (direction === 0) {
return
}
@@ -404,12 +424,48 @@ function slider(root) {
const steps = step === null ? 100 : Math.max(Math.round((max - min) / step), 1)
const delta = ((max - min) / steps) * clamp(Math.floor(steps / 10), 1, 10)
if (commit(inputs.indexOf(input), Number.parseFloat(input.value) + (event.key === 'PageUp' ? delta : -delta))) {
if (commit(inputs.indexOf(input), Number.parseFloat(input.value) + delta * direction)) {
input.dispatchEvent(new Event('change', { bubbles: true }))
}
})
listen(input, 'keyup', (event) => {
if (event.key === ' ' || event.code === 'Space') {
spaceHeld = false
}
})
})
/** +1, -1 or 0: which way a key moves the value by one large interval. */
function largeStepDirection(event) {
if (event.key === 'PageUp') {
return 1
}
if (event.key === 'PageDown') {
return -1
}
if (!spaceHeld) {
return 0
}
const rtl = getComputedStyle(root).direction === 'rtl'
switch (event.key) {
case 'ArrowUp':
return 1
case 'ArrowDown':
return -1
case 'ArrowRight':
return rtl ? -1 : 1
case 'ArrowLeft':
return rtl ? 1 : -1
default:
return 0
}
}
// Before x-model or wire:model reads it, hold a range input at its neighbour instead of passing it.
listen(
root,