Stand a slider up with orientation="vertical"
M3 Expressive's configuration table lists a vertical orientation the library had no prop for (plan step 24, audit docs/audits/m3-alignment/inputs.md § Missing). The drawing is the horizontal one, laid out as long as the slider is tall inside a size container and turned a quarter anticlockwise, so the geometry, the sizes and the tokens are untouched; the value label and the inset icon are turned back so they read across. The keyboard stays the native range's — Up and Right raise, Down and Left lower — and the inputs say aria-orientation. A vertical slider takes its length from its wrapper, so the header, the SKILL.md entry and the showcase all say to give it a height. M3 keeps range sliders horizontal, so `range` wins over `orientation`. 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
cc91e99a5d
commit
790ef93c6a
+41
-19
@@ -15,6 +15,9 @@
|
||||
* Alpine writes into an input (`input.value = …`, which fires no event) is caught on the input's
|
||||
* own `value` setter, and attribute changes by a MutationObserver.
|
||||
*
|
||||
* - Orientation: the geometry below is always the horizontal one. A vertical slider is that same
|
||||
* drawing turned a quarter by CSS, so only two things change here — the track's length is the
|
||||
* slider's height rather than its width, and a pointer is read along Y from the bottom edge up.
|
||||
* - 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
|
||||
@@ -224,6 +227,12 @@ function slider(root) {
|
||||
return { min, max: max > min ? max : min + 100, step: input.step === 'any' ? null : step > 0 ? step : 1 }
|
||||
}
|
||||
|
||||
/** M3 Expressive's second orientation: the drawing is turned a quarter, the geometry is not. */
|
||||
const vertical = () => root.dataset.orientation === 'vertical'
|
||||
|
||||
/** The track's length: the slider's height while it stands up, its width while it lies down. */
|
||||
const trackLength = () => Math.max((vertical() ? root.clientHeight : root.clientWidth) - HANDLE_WIDTH, 0)
|
||||
|
||||
const values = () => inputs.map((input) => Number.parseFloat(input.value))
|
||||
const thumbOf = (input) => drawing?.querySelector(`[data-handle="${inputs.indexOf(input) === 1 ? 'end' : 'start'}"]`)
|
||||
|
||||
@@ -247,7 +256,7 @@ function slider(root) {
|
||||
return
|
||||
}
|
||||
|
||||
const width = Math.max(root.clientWidth - HANDLE_WIDTH, 0)
|
||||
const width = trackLength()
|
||||
const size = SIZES[root.dataset.size] ?? SIZES.xs
|
||||
const { segments, stops, ticks, handles, activeTrack, endTrackStart } = geometry(width)
|
||||
const part = (selector) => drawing.querySelector(selector)
|
||||
@@ -484,7 +493,7 @@ function slider(root) {
|
||||
|
||||
const mutations = new MutationObserver(schedule)
|
||||
inputs.forEach((input) => mutations.observe(input, { attributes: true, attributeFilter: ['min', 'max', 'step', 'value', 'disabled'] }))
|
||||
mutations.observe(root, { attributes: true, attributeFilter: ['data-size', 'data-centered', 'dir'] })
|
||||
mutations.observe(root, { attributes: true, attributeFilter: ['data-size', 'data-centered', 'data-orientation', 'dir'] })
|
||||
|
||||
const resizes = new ResizeObserver(schedule)
|
||||
resizes.observe(root)
|
||||
@@ -511,39 +520,52 @@ function slider(root) {
|
||||
event.preventDefault()
|
||||
|
||||
const box = root.getBoundingClientRect()
|
||||
const width = Math.max(box.width - HANDLE_WIDTH, 0)
|
||||
const upright = vertical()
|
||||
const width = Math.max((upright ? box.height : box.width) - HANDLE_WIDTH, 0)
|
||||
const rtl = getComputedStyle(root).direction === 'rtl'
|
||||
const { min, max } = bounds()
|
||||
const handles = geometry(width).handles
|
||||
|
||||
const offset = (clientX) => clamp(rtl ? box.right - HANDLE_WIDTH / 2 - clientX : clientX - box.left - HANDLE_WIDTH / 2, 0, width)
|
||||
const valueAt = (clientX) => min + (width > 0 ? offset(clientX) / width : 0) * (max - min)
|
||||
// Where a pointer is along the track, and how far into it that is from the low end —
|
||||
// the left edge lying down (the right one in a right-to-left page), the bottom standing up.
|
||||
const along = (pointer) => (upright ? pointer.clientY : pointer.clientX)
|
||||
const offset = (coordinate) =>
|
||||
clamp(
|
||||
upright
|
||||
? box.bottom - HANDLE_WIDTH / 2 - coordinate
|
||||
: rtl
|
||||
? box.right - HANDLE_WIDTH / 2 - coordinate
|
||||
: coordinate - box.left - HANDLE_WIDTH / 2,
|
||||
0,
|
||||
width,
|
||||
)
|
||||
const valueAt = (coordinate) => min + (width > 0 ? offset(coordinate) / width : 0) * (max - min)
|
||||
|
||||
const startX = event.clientX
|
||||
const start = along(event)
|
||||
const before = values()
|
||||
let dragging = event.pointerType !== 'touch'
|
||||
let index = null
|
||||
|
||||
// The nearest handle; handles on top of each other wait for the first move's direction.
|
||||
const choose = (clientX) => {
|
||||
const choose = (coordinate) => {
|
||||
if (!range) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const x = offset(clientX)
|
||||
const [start, end] = handles.map((handle) => Math.abs(handle - x))
|
||||
const x = offset(coordinate)
|
||||
const [toStart, toEnd] = handles.map((handle) => Math.abs(handle - x))
|
||||
|
||||
if (start !== end) {
|
||||
return start < end ? 0 : 1
|
||||
if (toStart !== toEnd) {
|
||||
return toStart < toEnd ? 0 : 1
|
||||
}
|
||||
|
||||
const moved = rtl ? startX - clientX : clientX - startX
|
||||
const moved = upright || rtl ? start - coordinate : coordinate - start
|
||||
|
||||
return moved === 0 ? null : moved < 0 ? 0 : 1
|
||||
}
|
||||
|
||||
const follow = (clientX) => {
|
||||
index ??= choose(clientX)
|
||||
const follow = (coordinate) => {
|
||||
index ??= choose(coordinate)
|
||||
|
||||
if (index === null) {
|
||||
return
|
||||
@@ -555,7 +577,7 @@ function slider(root) {
|
||||
}
|
||||
|
||||
pressed(index)
|
||||
commit(index, valueAt(clientX))
|
||||
commit(index, valueAt(coordinate))
|
||||
}
|
||||
|
||||
const move = (moveEvent) => {
|
||||
@@ -563,12 +585,12 @@ function slider(root) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!dragging && Math.abs(moveEvent.clientX - startX) < TOUCH_SLOP) {
|
||||
if (!dragging && Math.abs(along(moveEvent) - start) < TOUCH_SLOP) {
|
||||
return
|
||||
}
|
||||
|
||||
dragging = true
|
||||
follow(moveEvent.clientX)
|
||||
follow(along(moveEvent))
|
||||
}
|
||||
|
||||
const end = (endEvent) => {
|
||||
@@ -577,7 +599,7 @@ function slider(root) {
|
||||
}
|
||||
|
||||
if (endEvent.type === 'pointerup' && !dragging) {
|
||||
follow(endEvent.clientX)
|
||||
follow(along(endEvent))
|
||||
}
|
||||
|
||||
release()
|
||||
@@ -602,7 +624,7 @@ function slider(root) {
|
||||
window.addEventListener('pointercancel', end)
|
||||
|
||||
if (dragging) {
|
||||
follow(event.clientX)
|
||||
follow(start)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user