Giter VIP home page Giter VIP logo

Comments (16)

chungwu avatar chungwu commented on May 22, 2024 2

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.

from react-spectrum.

devongovett avatar devongovett commented on May 22, 2024 1

Awesome! We did have an API for a RangeSlider in addition to the normal Slider. Basically the difference is rather than exposing a number it exposes a RangeValue<number>.

interface RangeSlider extends SliderBase<RangeValue<number>> {}

Perhaps we could have three component APIs, from simple to more advanced:

  • Slider - a simple slider with a single thumb
  • RangeSlider - a range slider with two thumbs
  • MultiSlider (name TBD) - base slider with support for any number of thumbs.

Perhaps at the react-aria level though we'd just support the advanced one via useSlider, similar to what you have. i.e. under the hood sliders can always have 1 or more thumbs. Then Slider and RangeSlider could easily be built with a simpler API at the Spectrum/design system level.

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. offsetPercents)? We usually try to keep the ARIA hooks as close to just DOM props as possible. Plus, things like offset percents are useful outside just the web (e.g. could be used in react-native), so putting them in Stately would make sense to share that logic too. For the ones returned by useSliderThumb, perhaps these could live as methods on the state object? Like getValueForThumb(index) or something?

from react-spectrum.

snowystinger avatar snowystinger commented on May 22, 2024 1

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...

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.

from react-spectrum.

majornista avatar majornista commented on May 22, 2024 1

@chungwu You're close. I would use a label for the div with id="label" and have its for/htmlFor attribute point to the id attribute on the first input, which could be assigned on the React component, but if not be generated internally. Each input should have aria-labelledby="label THE-GENERATED-ID-FOR-THE-INPUT". So that the Min should announce as "label Minimum", and the Max should announce as "label Maximum".

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.

from react-spectrum.

devongovett avatar devongovett commented on May 22, 2024

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.

from react-spectrum.

snowystinger avatar snowystinger commented on May 22, 2024

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)
https://www.w3.org/TR/wai-aria-practices-1.1/#windowsplitter
https://www.w3.org/TR/wai-aria-practices-1.1/#slider
https://www.w3.org/TR/wai-aria-practices-1.1/#slidertwothumb

from react-spectrum.

chungwu avatar chungwu commented on May 22, 2024

Thanks for the pointer! I was planning on using useDrag1D(); is the plan to replace it with useMove()? Should I be incorporating useMove() or go ahead with just useDrag1D() for now (since slider only requires 1D movement anyway)?

from react-spectrum.

majornista avatar majornista commented on May 22, 2024

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 input[type="range"], or for a two, or more, thumb slider, an input[type="range"] for each value being controlled.

from react-spectrum.

chungwu avatar chungwu commented on May 22, 2024

@majornista thanks! Do you mean there should be a visually-hidden input[type="range"] (as there is for checkbox etc) for each thumb? Should the focus / key event listeners be on the input instead of the thumb (as in WAI-ARIA examples), so that it can be controlled via VoiceOver? (Sorry, this is new territory for me 😅)

from react-spectrum.

snowystinger avatar snowystinger commented on May 22, 2024

Thanks for the pointer! I was planning on using useDrag1D(); is the plan to replace it with useMove()? Should I be incorporating useMove() or go ahead with just useDrag1D() for now (since slider only requires 1D movement anyway)?

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 :(

from react-spectrum.

devongovett avatar devongovett commented on May 22, 2024

@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:

  • useMove - new name for useDrag1D, moved into @react-aria/interactions and updated to use pointer events with fallbacks for touch since it only supports mouse events right now. I don't think I would start here though. Perhaps you can try out useDrag1D as it is to start?
  • useSliderState - state in @react-stately/slider. Holds the current slider value. You can use the useControlledState hook to store it and handle calling the onChange handler. There may be some other state/methods we haven't thought about that end up here as well.
  • useSlider - hook in @react-aria/slider. Handles events for dragging, and updates DOM props/ARIA props for the label/hidden range input. There may be more here, so we'll follow up with more info about the a11y implementation soon.

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.

from react-spectrum.

devongovett avatar devongovett commented on May 22, 2024

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/

from react-spectrum.

chungwu avatar chungwu commented on May 22, 2024

Great, thanks! I was able to build a prototype of this using useDrag1D.

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 React component might look something like...

<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 values is on MultiSlider instead of a controlled value on Thumb. It makes sense to go there though, since you probably want an onChange for all the values instead of each individual value.

Of course, can also have a simple <Slider/> with only one thumb that wraps around all this.

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 useSlider() for the containing component, and useSliderThumb() for each thumb. The thumb and its container can communicate via React context, like RadioGroup and Radio.

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...

from react-spectrum.

majornista avatar majornista commented on May 22, 2024

@majornista thanks! Do you mean there should be a visually-hidden input[type="range"] (as there is for checkbox etc) for each thumb? Should the focus / key event listeners be on the input instead of the thumb (as in WAI-ARIA examples), so that it can be controlled via VoiceOver? (Sorry, this is new territory for me 😅)

@chungwu Correct. We use a visually hidden input[type="range"] for each thumb, so that focus goes to the html5 input instead of some generic DOM element. Browser-implemented input[type="range"] elements can handle the increment and decrement gesture events triggers by screen readers as stepUp and stepDown.

Labelling the input within a simple Slider is easy. The id prop for the component can get propagated to the rendered id attribute on the input, and a <label> can reference this IDREF using the for/htmlFor attribute. Alternatively, an aria-label or aria-labelledby prop on the component can get propagated to the input directly.

With a RangeSlider, the component should be labelled using a fieldset or WAI-ARIA role="group" that contains two input[type="range"]s with aria-label="Minimum"/aria-label="Maximum" (strings should be localized with default values), and aria-labelledby referencing the external label and the input itself.

Form controls in React-Spectrum, include a label prop and render the label as part of the component, to ensure that things layout correctly and that the internal controls get appropriately labelled for accessibility.

I always explicitly set aria-valuetext on input[type="range"], even if it is just a numeric value, because some screen readers announce the input value as a percent between the min and max, but it should be possible to explicitly set the aria-valuetext, and tooltip text, independently. So that a slider for a time range, for example, can communicate a time as a string to someone using assistive technology.

It is not necessary to add aria-valuemin, aria-valuemax or aria-valuenow for the simple Slider use case, because those values will be the same as min, max and value for the input. However, for the RangeSlider use case, each input will have min and max relative to the entire slider range, but aria-valuemin and aria-valuemax can be used to communicate the range limits for each input. The "Minimum" input will have aria-valuemin equal to the min for the RangeSlider, and aria-valuemax equal to the value of the "Maximum" input. The "Maximum" input will have aria-valuemax equal to the max for the RangeSlider, and aria-valuemin equal to the value of the "Minimum" input.

I agree with @snowystinger that it having thumbs exposed for composing may be useful.

from react-spectrum.

chungwu avatar chungwu commented on May 22, 2024

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,

  • Do we need role="slider" on anything?
  • Do we need any aria labeling on the "thumb-knob" -- the visible graphical thumb that you drag with a cursor?
  • Would validation stuff (aria-required / invalid / errormessage etc) go on each range input?

Thanks!

from react-spectrum.

majornista avatar majornista commented on May 22, 2024

@chungwu,

  1. You shouldn't need role="slider" on anything, because the input[type="range"] has that role implicitly.
  2. Visible graphics or divs other than the input, like the thumb-knob, are presentational and shouldn't have any aria- labelling.
  3. Validation stuff, like aria-required or invalid, should go on the inputs, because that's what will receive focus, although I'm not sure there is a use case for aria-required on a Slider. For error messaging, we should be able to set aria-describedby on each of the inputs with an IDREF referencing the HTML element containing the error message text content.

from react-spectrum.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.