Repository: mui/material-ui (TypeScript)
Date: 2019-08-30 16:22:21+00:00
Task type: Feature

Issue:
Slider: should be able to label each thumb with a different aria-label
<!-- Provide a general summary of the issue in the Title above -->
Slider's `aria-label` prop is a string, and that same string gets applied to all thumbs. Ideally it should also accept a string array, and apply each string to each thumb, similar to how `value` and `defaultValue` can be arrays of numbers.

<!--
  Thank you very much for contributing to Material-UI by creating an issue! ❤️
  To avoid duplicate issues we ask you to check off the following list.
-->

<!-- Checked checkbox should look like this: [x] -->

- [x] The issue is present in the latest release.
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Current Behavior 😯

`<Slider aria-label={["myFirstLabel", "mySecondLabel"]} value={[10, 20]} />`

renders markup that looks like

```
<span class="MuiSlider-thumb makeStyles-thumb-87 MuiSlider-active" tabindex="0" role="slider" data-index="0" aria-label="myFirstLabel,mySecondLabel" aria-orientation="horizontal" aria-valuemax="100" aria-valuemin="0" aria-valuenow="18" style="left: 18%;"></span>
<span class="MuiSlider-thumb makeStyles-thumb-87" tabindex="0" role="slider" data-index="1" aria-label="myFirstLabel,mySecondLabel" aria-orientation="horizontal" aria-valuemax="100" aria-valuemin="0" aria-valuenow="100" style="left: 100%;"></span>
```

## Expected Behavior 🤔

each string in the array should go to its corresponding thumb's `aria-label`

## Steps to Reproduce 🕹

https://codesandbox.io/s/create-react-app-qmlsu

Inspect the markup of this code sandbox

## Context 🔦

<!--
  What are you trying to accomplish? How has this issue affected you?
  Providing context helps us come up with a solution that is most useful in the real world.
-->

I am trying to improve accessibility of range sliders. Also, this would make it much easier to test range sliders using `@testing-library/react`

## Your Environment 🌎

<!--
  Include as many relevant details about the environment with which you experienced the bug.
  If you encounter issues with typescript please include version and tsconfig.
-->

| Tech        | Version |
| ----------- | ------- |
| Material-UI | v4.3.3  |
| React       |   16.9.0      |
| Browser     |  Chrome  76.0.3809.100 (Official Build) (64-bit)    |

## Extra

You can see an example of multi thumb sliders with different label per thumb here
https://www.w3.org/TR/wai-aria-practices/examples/slider/multithumb-slider.html

It looks like this would be pretty easy to fix. I am happy to send in a PR with the fix if that is desired.



Issue discussion:
@city41 Thank you for the link toward w3.org. It does make sense, I believe that we can replicate the same strategy that we use with `aria-valuetext` (`getAriaValueText`).
> each string in the array should go to its corresponding thumb's aria-label

We have decided not to override existing native types to avoid confusion: https://github.com/mui-org/material-ui/pull/15703#discussion_r289791687.
Sounds good. I will take a look this weekend and hopefully get a PR out. Thanks Olivier.

Solution patch:
diff --git a/docs/pages/api/slider.md b/docs/pages/api/slider.md
--- a/docs/pages/api/slider.md
+++ b/docs/pages/api/slider.md
@@ -31,7 +31,8 @@ You can learn more about the difference by [reading our guide](/guides/minimizin
 | <span class="prop-name">component</span> | <span class="prop-type">elementType</span> | <span class="prop-default">'span'</span> | The component used for the root node. Either a string to use a DOM element or a component. |
 | <span class="prop-name">defaultValue</span> | <span class="prop-type">number<br>&#124;&nbsp;Array<number></span> |  | The default element value. Use when the component is not controlled. |
 | <span class="prop-name">disabled</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the slider will be disabled. |
-| <span class="prop-name">getAriaValueText</span> | <span class="prop-type">func</span> |  | Accepts a function which returns a string value that provides a user-friendly name for the current value of the slider.<br><br>**Signature:**<br>`function(value: number, index: number) => void`<br>*value:* The thumb label's value to format.<br>*index:* The thumb label's index to format. |
+| <span class="prop-name">getAriaLabel</span> | <span class="prop-type">func</span> |  | Accepts a function which returns a string value that provides a user-friendly name for the thumb labels of the slider.<br><br>**Signature:**<br>`function(index: number) => string`<br>*index:* The thumb label's index to format. |
+| <span class="prop-name">getAriaValueText</span> | <span class="prop-type">func</span> |  | Accepts a function which returns a string value that provides a user-friendly name for the current value of the slider.<br><br>**Signature:**<br>`function(value: number, index: number) => string`<br>*value:* The thumb label's value to format.<br>*index:* The thumb label's index to format. |
 | <span class="prop-name">marks</span> | <span class="prop-type">bool<br>&#124;&nbsp;array</span> | <span class="prop-default">[]</span> | Marks indicate predetermined values to which the user can move the slider. If `true` the marks will be spaced according the value of the `step` prop. If an array, it should contain objects with `value` and an optional `label` keys. |
 | <span class="prop-name">max</span> | <span class="prop-type">number</span> | <span class="prop-default">100</span> | The maximum allowed value of the slider. Should not be equal to min. |
 | <span class="prop-name">min</span> | <span class="prop-type">number</span> | <span class="prop-default">0</span> | The minimum allowed value of the slider. Should not be equal to max. |
diff --git a/docs/src/pages/components/slider/slider.md b/docs/src/pages/components/slider/slider.md
--- a/docs/src/pages/components/slider/slider.md
+++ b/docs/src/pages/components/slider/slider.md
@@ -53,7 +53,7 @@ Continuous sliders allow users to select a value along a subjective range.
 The component handles most of the work necessary to make it accessible.
 However, you need to make sure that:
 
-- The slider, as a whole, has a label (`aria-label` or `aria-labelledby` prop).
-- Each thumb has a user-friendly name for its current value.
+- Each thumb has a user-friendly label (`aria-label`, `aria-labelledby` or `getAriaLabel` prop).
+- Each thumb has a user-friendly text for its current value.
 This is not required if the value matches the semantics of the label.
 You can change the name with the `getAriaValueText` or `aria-valuetext` prop.
diff --git a/packages/material-ui/src/Slider/Slider.d.ts b/packages/material-ui/src/Slider/Slider.d.ts
--- a/packages/material-ui/src/Slider/Slider.d.ts
+++ b/packages/material-ui/src/Slider/Slider.d.ts
@@ -24,6 +24,7 @@ export interface SliderProps
   component?: React.ElementType<React.HTMLAttributes<HTMLSpanElement>>;
   defaultValue?: number | number[];
   disabled?: boolean;
+  getAriaLabel?: (index: number) => string;
   getAriaValueText?: (value: number, index: number) => string;
   marks?: boolean | Mark[];
   max?: number;
diff --git a/packages/material-ui/src/Slider/Slider.js b/packages/material-ui/src/Slider/Slider.js
--- a/packages/material-ui/src/Slider/Slider.js
+++ b/packages/material-ui/src/Slider/Slider.js
@@ -284,6 +284,7 @@ const Slider = React.forwardRef(function Slider(props, ref) {
     component: Component = 'span',
     defaultValue,
     disabled = false,
+    getAriaLabel,
     getAriaValueText,
     marks: marksProp = defaultMarks,
     max = 100,
@@ -696,7 +697,7 @@ const Slider = React.forwardRef(function Slider(props, ref) {
               role="slider"
               style={style}
               data-index={index}
-              aria-label={ariaLabel}
+              aria-label={getAriaLabel ? getAriaLabel(index) : ariaLabel}
               aria-labelledby={ariaLabelledby}
               aria-orientation={orientation}
               aria-valuemax={max}
@@ -720,7 +721,17 @@ Slider.propTypes = {
   /**
    * The label of the slider.
    */
-  'aria-label': PropTypes.string,
+  'aria-label': chainPropTypes(PropTypes.string, props => {
+    const range = Array.isArray(props.value || props.defaultValue);
+
+    if (range && props['aria-label'] != null) {
+      return new Error(
+        'Material-UI: you need to use the `getAriaLabel` prop instead of `aria-label` when using a range slider.',
+      );
+    }
+
+    return null;
+  }),
   /**
    * The id of the element containing a label for the slider.
    */
@@ -731,9 +742,9 @@ Slider.propTypes = {
   'aria-valuetext': chainPropTypes(PropTypes.string, props => {
     const range = Array.isArray(props.value || props.defaultValue);
 
-    if (range && props['aria-valuetext']) {
+    if (range && props['aria-valuetext'] != null) {
       return new Error(
-        'Material-UI: you need to use the `getAriaValueText` prop instead of `aria-valuetext` when using a range input.',
+        'Material-UI: you need to use the `getAriaValueText` prop instead of `aria-valuetext` when using a range slider.',
       );
     }
 
@@ -761,11 +772,19 @@ Slider.propTypes = {
    * If `true`, the slider will be disabled.
    */
   disabled: PropTypes.bool,
+  /**
+   * Accepts a function which returns a string value that provides a user-friendly name for the thumb labels of the slider.
+   *
+   * @param {number} index The thumb label's index to format.
+   * @returns {string}
+   */
+  getAriaLabel: PropTypes.func,
   /**
    * Accepts a function which returns a string value that provides a user-friendly name for the current value of the slider.
    *
    * @param {number} value The thumb label's value to format.
    * @param {number} index The thumb label's index to format.
+   * @returns {string}
    */
   getAriaValueText: PropTypes.func,
   /**


Test patch:
diff --git a/packages/material-ui/src/Slider/Slider.test.js b/packages/material-ui/src/Slider/Slider.test.js
--- a/packages/material-ui/src/Slider/Slider.test.js
+++ b/packages/material-ui/src/Slider/Slider.test.js
@@ -454,11 +454,36 @@ describe('<Slider />', () => {
       PropTypes.resetWarningCache();
     });
 
-    it('should warn if aria-valuetext is a string', () => {
+    it('should warn if aria-valuetext is provided', () => {
       render(<Slider value={[20, 50]} aria-valuetext="hot" />);
       expect(consoleErrorMock.args()[0][0]).to.include(
         'you need to use the `getAriaValueText` prop instead of',
       );
     });
+
+    it('should warn if aria-label is provided', () => {
+      render(<Slider value={[20, 50]} aria-label="hot" />);
+      expect(consoleErrorMock.args()[0][0]).to.include(
+        'you need to use the `getAriaLabel` prop instead of',
+      );
+    });
+  });
+
+  it('should support getAriaValueText', () => {
+    const getAriaValueText = value => `${value}°C`;
+    const { getAllByRole } = render(
+      <Slider value={[20, 50]} getAriaValueText={getAriaValueText} />,
+    );
+
+    expect(getAllByRole('slider')[0]).to.have.attribute('aria-valuetext', '20°C');
+    expect(getAllByRole('slider')[1]).to.have.attribute('aria-valuetext', '50°C');
+  });
+
+  it('should support getAriaLabel', () => {
+    const getAriaLabel = index => `Label ${index}`;
+    const { getAllByRole } = render(<Slider value={[20, 50]} getAriaLabel={getAriaLabel} />);
+
+    expect(getAllByRole('slider')[0]).to.have.attribute('aria-label', 'Label 0');
+    expect(getAllByRole('slider')[1]).to.have.attribute('aria-label', 'Label 1');
   });
 });