feat: add four range slider designs - #131
Conversation
Plainer descriptions for the five slider designs in the registry, the AGENTS catalog row and the bubble preview label.
|
Great work on these new slider variants—the visual direction, motion details, accessibility considerations, and shared plumbing are thoughtfully done. I found a few things worth addressing before merge:
Could you address these and add coverage for non-divisible ranges and fractional Bubble values? |
|
Sure @starc007 |
The base slider carried its own copy of the value state, step snapping, pointer drag and key handling, so it drifted from the four newer designs: no PageUp/PageDown, no formatValueText, no float-dust trim on fractional steps. Delete all of it in favour of useSlider, which the catalog already claims every design shares.
…not divide steps rounded (max - min) / step, so a scale like 0-10 by 4 drew a tick at 12 and let the strip settle there while the slider reported 10. Whole ticks now stop at the last full step and max gets a tick of its own, centred at its true offset; the drag bounds and the flick snap clamp to that same offset so the needle can no longer rest past the reported value.
…them The bubble read Math.round(current), so a half-step scale showed 73 while the thumb sat at 72.5 and aria-valuenow announced 72.5. The value arrives already snapped to the step with its float dust trimmed, so it can be read as it is.
…n one Each design spread its own formatValueText over the caller's options, so a formatValueText passed to the ruler without a unit — or to the bubble without a format — was replaced by undefined and the accessible value text was lost. Fall back to the built-in one instead of overwriting.
Runs the base slider through the shared variant suite now that it uses the same hook, and adds cases for a range the step does not divide, a ruler scale that ends mid-step, a caller-supplied formatValueText and a fractional value in the bubble.
Every design resolves values through snapSliderValue now. It treats max as a snap candidate when the step does not divide the range. - ruler: position ticks by offset instead of appending a flex box for the remainder. That box sat after wholeSteps + 1 full ones, so it centred half a gap right of the value it marked. No width fixes that when the remainder is under half a step. - ruler: gate the momentum snap on gesture and hold refs, so a late settle cannot clear interacting when you grab the scale again or use the keyboard. - fluid: drop the rounding from the default format. It announced "72%" while aria-valuenow said 72.5. - bubble: a caller's formatValueText now beats format, as it already did in fluid. - base: trim float dust before flooring the tick count. The dot row is inset by half the thumb width too, so a dot sits where the thumb lands. - hook: hold the drag flag in a ref so the first move after pointerdown is not dropped. Focus the handle when the track is pressed, and release pointer capture only when it is held.
|
@starc007 I have made all the changes |
Adds four slider designs to the Range Slider page, each with its own
installSlug, alongside the existing stepped slider.@beui/range-slider-fluidclip-pathinset with a rounded cap, and the label is drawn twice — once on the track, once inside the clip — so it inverts as the fill covers it.@beui/range-slider-wave@beui/range-slider-bubble@beui/range-slider-ruler0.5→ "72.5 kg").Shared plumbing
All four take their value, step snapping, pointer-capture drag and keyboard handling from
lib/hooks/use-slider.ts— controlled/uncontrolled, arrows plus Home/End/PageUp/PageDown,aria-valuemin/max/now, andaria-valuetextwhen the value carries a unit or suffix. The registry follows the import, so each variant still installs standalone.The base
RangeSlideralready used a{ 700, 50, 0.5 }spring and two of the new designs wanted the same feel, so it moved tolib/ease.tsasSPRING_GLIDErather than being copied a third time.Why clip-path rather than width
The fill started as an animated
width. Two artefacts came out of that, both fixed here:Clipping a full-width layer solves both — at 0% the clip is empty, so nothing paints — and it keeps the label from being scaled or re-laid out. The spring is also critically damped on purpose: an under-damped fill undershoots past 0% and rebounds, which reads as a stuck sliver rather than as liquid.
Accessibility
Verified in the browser rather than assumed:
ring-inset; an outset ring is clipped away by the track'soverflow-hiddenand leaves the control with no visible focus at all./45— measured 4.18:1 dark and 3.14:1 light, above the 3:1 floor in both themes. Filled bars stay at 16–19:1, so the filled-vs-unfilled read is unaffected.useReducedMotion().Ruler's edge fade is a
mask-image, not twofrom-backgroundgradients — the gradient version painted a visible haze band when the slider sat on any surface other than--background.One behaviour bug worth calling out
Pressing a key while the ruler's momentum was still running swallowed the input: the coasting strip kept committing its own value over the keyboard's, so Home landed on 49.5 instead of 40. A key press now stops the motion value and takes ownership back before delegating.
Checks
bun run check(typecheck, lint, registry) andbun testpass — 97 tests across 13 files, 55 registry components.tests/range-slider-variants.test.tsxadds 21 covering keyboard stepping and clamping, controlled mode, disabled, pointer drag including past-the-edge clamping, thearia-valuetextcontract and fractional-step snapping (0.1stepping stays72.6/72.7, no float dust). The four sliders are also rows in the axe matrix intests/a11y.test.tsx.Measured in the browser during drag, on each design: median frame 16.7ms, worst 17.7ms, no frames over 32ms — including the ruler at 342 tick nodes and the wave at 32 concurrent springs. Also checked at 375px, in both themes, across overdrag, release-outside-the-element, rapid key repeat, window resize and cross-talk between sliders on the same page.
Every tick is in the DOM on the ruler, which is fine into the low hundreds (80 units at step 0.5 is 161) and marked in the source with the windowing upgrade path if a finer step is ever needed.