-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
useSlider() and useSliderState() for a Slider component #763
Comments
Hey, thanks for opening this! Let me review the API doc that we have internally, and follow up here tomorrow. One thing is that so far, since we've all been working on Spectrum internally, the Storybook and unit tests are pretty set up around that. I think you could start with a basic version and not worry about the Spectrum styles if you want. We can work on the Spectrum specific bits later on. As long as there's a basic version of a component using the hooks somewhere, you could use that as the example in Storybook and in the tests in the meantime. |
FYI, some work has been done around draggable handle type things in relation to splitview. They have some similar behaviors, so you may find this PR good to look at #37 (It was put on hold because it wasn't a priority component, so some stuff has evolved since, but this is probably a good starting point) |
Thanks for the pointer! I was planning on using |
An important note regarding mobile accessibility. There is currently no way to detect the increment and decrement swipe gestures dispatched by VoiceOver on iOS with javascript, so the expected behavior for mobile screen reader users will not work with a pure javascript WAI-ARIA implementation of the slider design pattern. In HTML, we should implement the slider control using an |
@majornista thanks! Do you mean there should be a visually-hidden |
I had planned to make it more general, I don't think that hook is one of our API's for v3 yet, so we should probably try to make it the more general case. Also, if you can find it there is a series of commits where I experimented with building sliders on top of this... but my git-fu isn't good enough to track that down anymore :( |
@chungwu here's a rough draft of the API we were thinking about. import {ValueBase, RangeInputBase} from '@react-types/shared';
interface SliderProps extends ValueBase<number>, RangeInputBase<number> {
isDisabled?: boolean,
// text label
children?: ReactNode,
// format options for the number formatter used to render the value label
formatOptions?: Intl.NumberFormatOptions,
// onChange would be called as the user drags.
// onChangeEnd could be used if you only need to know the value after the drag completes.
onChangeEnd?: (value: number) => void
}
interface SliderState {
value: number,
setValue(value: number): void,
// possibly more useful methods here?
}
function useSliderState(props: SliderProp): SliderState;
interface SliderAria {
// props for the label element
labelProps: LabelHTMLAttributes<HTMLLabelElement>,
// props for the hidden <input type="range">
inputProps: InputHTMLAttribute<HTMLInputElement>
}
function useSlider(props: SliderProps, state: SliderState): SliderAria; Eventually we'd also have a range slider, and possibly support for multiple more than two handles if needed (e.g. gradient inputs), but we can start simple. I think the pieces are:
Hopefully that's enough to get you started. Let us know if you have any questions. We can also set up a quick video call if that's easier. |
Also if it helps there's some docs from the Spectrum design team here which might be useful to visualize all the pieces we're thinking about here. https://spectrum.adobe.com/page/slider/ |
Great, thanks! I was able to build a prototype of this using I also wanted to support having multiple handles / thumbs for, say, range sliders. This is a bit trickier, since every thumb can and should have its own aria-label, and can be individually focused / disabled, etc. So a <MultiSlider
values={[25, 75]}
maxValue={100}
minValue={0}
step={1}
aria-labels={["Min Value", "Max Value"]}
isDisableds={[false, false]}
onChange={vals => ...}
/> which is a tad odd. Could also go this way: <MultiSlider
values={[25, 75]}
maxValue={100}
minValue={0}
step={1}
onChange={vals => ...}
>
<Thumb aria-label="Min Value" isDisabled={false}/>
<Thumb aria-label="Max Value"/>
</MultiSlider> which is also a tad odd, as the controlled Of course, can also have a simple Since each thumb will need to be its own component so it can install its own set of hooks, we'll end up with something like Here's a sketch of what I ended up with: interface SliderProps extends ValueBase<number[]>, RangeInputBase<number> {
orientation?: Orientation;
formatOptions?: Intl.NumberFormatOptions;
// etc.
}
interface SliderThumbProps extends AriaLabelingProps, FocusableProps {
index: number;
isDisabled?: boolean;
// etc.
}
// SliderState expanded to work with multiple values.
interface SliderState {
readonly values: number[];
setValue: (index: number, value: number) => void;
isDragging: (index: number) => boolean;
setDragging: (index: number, dragging: boolean) => void;
}
function useSliderState(props: MultiSliderProps): SliderState;
/**
* thumbContainerRef needed to map drag position to actual value
*/
function useMultiSlider(
props: MultiSliderProps,
state: SliderState,
thumbContainerRef: React.RefObject<HTMLElement>
): {
// The thumbContainerProps, to be spread onto the thumb container, installs
// a click event, so that when the user clicks on the track, the closest thumb is
// set to that value
thumbContainerProps,
// This is an array of position offsets for each thumb, expressed as percentage
// of the width of the thumbContainer (from 0 to 1).
// This is where each thumb should be placed, but this is also useful
// for the caller for, say, sizing the "colored" part of the track if it makes sense, or to
// position a floating tooltip.
offsetPercents
}
/**
* Primarily, installs `useDrag1D()`
*/
function useSliderThumb(
sliderProps: SliderProps,
thumbProps: SliderThumbProps,
state: SliderState,
thumbContainerRef: React.RefObject<HTMLElement>
): {
// useDrag1D() handlers, focus, aria labels
thumbProps,
// For the visually-hidden input[type="range"]
inputProps,
// Convenient for caller to get value and the valueLabel if they want to do
// things like putting it into a tooltip.
value,
valueLabel,
offsetPercent,
} With the above set of hooks, I was able to pretty easily build some working slider / range slider components. A rough sketch would look something like... function MultiSlider(props) {
const thumbContainerRef = React.useRef(null);
const state = useSliderState(props);
const {thumbContainerProps, offsetPercents} = useMultiSlider(props, state, thumbContainerRef);
return (
<div className="slider">
<div className="thumb-container" {...thumbContainerProps} ref={thumbContainerRef}>
<SliderContext.Provider value={{state, sliderProps: props, thumbContainerRef}}>
{state.values.map((value, index) => (
<Thumb index={index}/>
))}
</SliderContext.Provider>
{state.values.length === 1 &&
// Single thumb, just color to the left of the thumb
<div className="active-track" style={{width: `${offsetPercents[0]*100}%`}}/>
}
{state.values.length === 2 &&
// Two thumbs; color in between the thumbs
<div className="active-track" style={{
left: `${offsetPercents[0]*100}%`,
width: `${(offsetPercents[1]-offsetPercents[0]) * 100}%`
}}/>
}
</div>
</div>
);
}
function Thumb(props) {
const {state, sliderProps, thumbContainerRef} = React.useContext(SliderContext);
const {thumbProps, inputProps, valueLabel} = useSliderThumb(sliderProps, props, thumbContainerRef);
return (
<div className="thumb" {...thumbProps}>
<div className="thumb-tooltip">{valueLabel}</div>
<VisuallyHidden><input type="range" {...inputProps}/></VisuallyHidden>
</div>
);
} Anyway, just an initial stab! Lots of open questions to be figured out, but probably the main one is whether the "Thumb" component should be exposed, or just remain an internal detail... |
Awesome! We did have an API for a interface RangeSlider extends SliderBase<RangeValue<number>> {} Perhaps we could have three component APIs, from simple to more advanced:
Perhaps at the react-aria level though we'd just support the advanced one via Overall your API looks good! I think perhaps (if possible) the returned values from the hooks that aren't props objects could live in the stately layer (e.g. |
I think I'd be inclined to have Thumbs be exposed for composing. This would allow people to put attributes on them like data-*, particularly useful to the automated testing crowd. It could also be used to attach Tooltips. It also follows a lot of our decisions with other components to expose more of the internals. RadioGroup is a great example you brought up. Many things can be controlled at the Group level, but you can also dive into individual Radio's easily. |
@chungwu Correct. We use a visually hidden Labelling the input within a simple Slider is easy. The With a Form controls in React-Spectrum, include a I always explicitly set It is not necessary to add I agree with @snowystinger that it having thumbs exposed for composing may be useful. |
Thanks for the feedback, everyone! Makes a lot of sense 😄 @majornista to clarify on aria-label for the multi-thumb use case... It'd look something like... <div role="group" aria-labelledby="label">
<div id="label">Whatever I passed to label prop</div>
<div className="thumbs">
<div className="thumb">
<div className="thumb-knob"/>
<input type="range" aria-label="Minimum" aria-valuemin/max/now=.../>
</div>
<div className="thumb">
<div className="thumb-knob"/>
<input type="range" aria-label="Maximum" aria-valuemin/max/now=.../>
</div>
</div>
</div> Specifically,
Thanks! |
@chungwu You're close. I would use a Modified version: <div role="group" aria-labelledby="range-input-label-0">
<label id="range-input-label-0" for="range-input-0">Whatever I passed to label prop</label>
<div className="thumbs">
<div className="thumb">
<input id="range-input-0" aria-labelledby="range-input-label-0 range-input-0" type="range" aria-label="Minimum" aria-valuemin/max/now=.../>
<div className="thumb-knob"/>
</div>
<div className="thumb">
<input id="range-input-1" aria-labelledby="range-input-label-0 range-input-1" type="range" aria-label="Maximum" aria-valuemin/max/now=.../>
<div className="thumb-knob"/>
</div>
</div>
</div> Small hint, put thumb-knob after the input so you can render the focused style using adjacent CSS selector. |
|
Hi all! Finally got around to pulling together a draft PR for this 😄 You'll find it in #809 I built the react-aria and react-stately hooks, and a few toy components using those hooks, and some Storybook stories, so we can get a feel for how the component interactions work, and what the DX is like in using these hooks. |
I'd like to implement
useSlider()
for @react-aria anduseSliderState()
for @react-stately. I believe the first step is to draft an RFC?Before I get started, though, if folks from the Spectrum team had already thought about what the API would look like, it'd be very helpful to get a brain dump first!
The text was updated successfully, but these errors were encountered: