Draw the slider without Tailwind
<x-slider> renders data-md-slider with its size, colour, value label, orientation and centring as data-md-* attributes and its parts as data-md-slider-*; every size, colour role, disabled state, the vertical quarter turn, RTL mirroring and the no-script fallback move into components/slider.css as custom properties per size and colour (plan step 36). slider.js reads the props from the root and marks the renamed handle, tick and icon hooks. Browser tests pin the handle centred on the track at each size, Space held with an arrow, and the vertical slider's drag along Y, keys and value label beside the handle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
1bb5adab0e
commit
281c21b0a4
+20
-18
@@ -200,8 +200,10 @@ function sliderGeometry({ width, size, fractions, centered = false, range = fals
|
||||
* effect, and anything reactive read or written on the way to the drawing would re-run it.
|
||||
*/
|
||||
function slider(root) {
|
||||
// The component's root carries the props; `root` is the control inside it the pointer presses.
|
||||
const host = root.closest('[data-md-slider]') ?? root
|
||||
const inputs = [...root.querySelectorAll(':scope > input[type="range"]')]
|
||||
const drawing = root.querySelector('[data-slider-drawing]')
|
||||
const drawing = root.querySelector('[data-md-slider-drawing]')
|
||||
const range = inputs.length > 1
|
||||
const cleanups = []
|
||||
let queued = false
|
||||
@@ -228,24 +230,24 @@ function slider(root) {
|
||||
}
|
||||
|
||||
/** M3 Expressive's second orientation: the drawing is turned a quarter, the geometry is not. */
|
||||
const vertical = () => root.dataset.orientation === 'vertical'
|
||||
const vertical = () => host.dataset.mdOrientation === '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'}"]`)
|
||||
const thumbOf = (input) => drawing?.querySelector(`[data-md-slider-handle="${inputs.indexOf(input) === 1 ? 'end' : 'start'}"]`)
|
||||
|
||||
const tickFractions = () => [...(drawing?.querySelectorAll('[data-tick]') ?? [])].map((tick) => Number.parseFloat(tick.dataset.tick))
|
||||
const tickFractions = () => [...(drawing?.querySelectorAll('[data-md-slider-tick]') ?? [])].map((tick) => Number.parseFloat(tick.dataset.mdSliderTick))
|
||||
|
||||
const geometry = (width) => {
|
||||
const { min, max } = bounds()
|
||||
|
||||
return sliderGeometry({
|
||||
width,
|
||||
size: root.dataset.size,
|
||||
size: host.dataset.mdSize,
|
||||
fractions: values().map((value) => (Number.isFinite(value) ? clamp((value - min) / (max - min), 0, 1) : 0)),
|
||||
centered: root.hasAttribute('data-centered'),
|
||||
centered: host.hasAttribute('data-md-centered'),
|
||||
range,
|
||||
tickFractions: tickFractions(),
|
||||
})
|
||||
@@ -257,13 +259,13 @@ function slider(root) {
|
||||
}
|
||||
|
||||
const width = trackLength()
|
||||
const size = SIZES[root.dataset.size] ?? SIZES.xs
|
||||
const size = SIZES[host.dataset.mdSize] ?? SIZES.xs
|
||||
const { segments, stops, ticks, handles, activeTrack, endTrackStart } = geometry(width)
|
||||
const part = (selector) => drawing.querySelector(selector)
|
||||
const px = (number) => `${Math.round(number * 100) / 100}px`
|
||||
|
||||
for (const name of ['start', 'active', 'end']) {
|
||||
const element = part(`[data-segment="${name}"]`)
|
||||
const element = part(`[data-md-slider-segment="${name}"]`)
|
||||
const segment = segments[name]
|
||||
|
||||
if (element) {
|
||||
@@ -277,7 +279,7 @@ function slider(root) {
|
||||
}
|
||||
|
||||
for (const name of ['start', 'end']) {
|
||||
const element = part(`[data-stop="${name}"]`)
|
||||
const element = part(`[data-md-slider-stop="${name}"]`)
|
||||
|
||||
if (element) {
|
||||
element.hidden = stops[name] === undefined
|
||||
@@ -288,20 +290,20 @@ function slider(root) {
|
||||
const fractions = tickFractions()
|
||||
const spacing = fractions.length > 1 ? (width - size.corner * 2) * Math.abs(fractions[1] - fractions[0]) : Infinity
|
||||
|
||||
drawing.querySelectorAll('[data-tick]').forEach((element, i) => {
|
||||
drawing.querySelectorAll('[data-md-slider-tick]').forEach((element, i) => {
|
||||
const tick = ticks[i]
|
||||
|
||||
element.hidden = !tick || spacing < TICK_MIN_SPACING
|
||||
|
||||
if (tick) {
|
||||
element.style.left = px(tick.x)
|
||||
element.toggleAttribute('data-active', tick.active)
|
||||
element.toggleAttribute('data-md-active', tick.active)
|
||||
}
|
||||
})
|
||||
|
||||
handles.forEach((x, i) => {
|
||||
const thumb = part(`[data-handle="${i === 1 ? 'end' : 'start'}"]`)
|
||||
const label = thumb?.querySelector('[data-value-label]')
|
||||
const thumb = part(`[data-md-slider-handle="${i === 1 ? 'end' : 'start'}"]`)
|
||||
const label = thumb?.querySelector('[data-md-slider-value]')
|
||||
|
||||
if (thumb) {
|
||||
thumb.style.left = px(x)
|
||||
@@ -313,7 +315,7 @@ function slider(root) {
|
||||
})
|
||||
|
||||
// The inset icon: at the start of the active track while it fits, else of the inactive one.
|
||||
const icon = part('[data-track-icon]')
|
||||
const icon = part('[data-md-slider-icon]')
|
||||
|
||||
if (icon) {
|
||||
const room = size.icon + ICON_PADDING * 2
|
||||
@@ -321,7 +323,7 @@ function slider(root) {
|
||||
const inInactive = !inActive && endTrackStart !== null && width - endTrackStart >= room
|
||||
|
||||
icon.hidden = !inActive && !inInactive
|
||||
icon.toggleAttribute('data-active', inActive)
|
||||
icon.toggleAttribute('data-md-active', inActive)
|
||||
icon.style.left = px((inActive ? activeTrack[0] : (endTrackStart ?? 0)) + ICON_PADDING)
|
||||
}
|
||||
}
|
||||
@@ -371,10 +373,10 @@ function slider(root) {
|
||||
}
|
||||
|
||||
/** Whether a handle shows keyboard focus: narrowed, ringed and labelled. */
|
||||
const showFocus = (input, shown) => thumbOf(input)?.toggleAttribute('data-focused', shown)
|
||||
const showFocus = (input, shown) => thumbOf(input)?.toggleAttribute('data-md-focused', shown)
|
||||
|
||||
/** Compose narrows a handle and shows its label while it is pressed or dragged. */
|
||||
const pressed = (index) => drawing?.querySelectorAll('[data-handle]').forEach((thumb, i) => thumb.toggleAttribute('data-pressed', i === index))
|
||||
const pressed = (index) => drawing?.querySelectorAll('[data-md-slider-handle]').forEach((thumb, i) => thumb.toggleAttribute('data-md-pressed', i === index))
|
||||
|
||||
inputs.forEach((input) => {
|
||||
// A value written by script fires no event: catch it on the input's own setters.
|
||||
@@ -493,7 +495,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', 'data-orientation', 'dir'] })
|
||||
mutations.observe(host, { attributes: true, attributeFilter: ['data-md-size', 'data-md-centered', 'data-md-orientation', 'dir'] })
|
||||
|
||||
const resizes = new ResizeObserver(schedule)
|
||||
resizes.observe(root)
|
||||
|
||||
Reference in New Issue
Block a user