diff --git a/apps/pigment-css-next-app/src/app/material-ui/react-slider/page.tsx b/apps/pigment-css-next-app/src/app/material-ui/react-slider/page.tsx index 97fb8108c1cb09..19c93224b7a523 100644 --- a/apps/pigment-css-next-app/src/app/material-ui/react-slider/page.tsx +++ b/apps/pigment-css-next-app/src/app/material-ui/react-slider/page.tsx @@ -17,7 +17,6 @@ import RangeSlider from '../../../../../../docs/data/material/components/slider/ import SliderSizes from '../../../../../../docs/data/material/components/slider/SliderSizes'; import TrackFalseSlider from '../../../../../../docs/data/material/components/slider/TrackFalseSlider'; import TrackInvertedSlider from '../../../../../../docs/data/material/components/slider/TrackInvertedSlider'; -import VerticalAccessibleSlider from '../../../../../../docs/data/material/components/slider/VerticalAccessibleSlider'; import VerticalSlider from '../../../../../../docs/data/material/components/slider/VerticalSlider'; export default function Slider() { @@ -125,12 +124,6 @@ export default function Slider() { -
-

Vertical Accessible Slider

-
- -
-

Vertical Slider

diff --git a/apps/pigment-css-vite-app/src/pages/material-ui/react-slider.tsx b/apps/pigment-css-vite-app/src/pages/material-ui/react-slider.tsx index 963612339e2faf..a3bfac23c4c197 100644 --- a/apps/pigment-css-vite-app/src/pages/material-ui/react-slider.tsx +++ b/apps/pigment-css-vite-app/src/pages/material-ui/react-slider.tsx @@ -17,7 +17,6 @@ import RangeSlider from '../../../../../docs/data/material/components/slider/Ran import SliderSizes from '../../../../../docs/data/material/components/slider/SliderSizes.tsx'; import TrackFalseSlider from '../../../../../docs/data/material/components/slider/TrackFalseSlider.tsx'; import TrackInvertedSlider from '../../../../../docs/data/material/components/slider/TrackInvertedSlider.tsx'; -import VerticalAccessibleSlider from '../../../../../docs/data/material/components/slider/VerticalAccessibleSlider.tsx'; import VerticalSlider from '../../../../../docs/data/material/components/slider/VerticalSlider.tsx'; export default function Slider() { @@ -126,12 +125,6 @@ export default function Slider() {
-
-

Vertical Accessible Slider

-
- -
-

Vertical Slider

diff --git a/docs/data/material/components/slider/VerticalAccessibleSlider.js b/docs/data/material/components/slider/VerticalAccessibleSlider.js deleted file mode 100644 index e9892bcc838cdd..00000000000000 --- a/docs/data/material/components/slider/VerticalAccessibleSlider.js +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import Slider from '@mui/material/Slider'; - -export default function VerticalAccessibleSlider() { - function preventHorizontalKeyboardNavigation(event) { - if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { - event.preventDefault(); - } - } - - return ( - - - - ); -} diff --git a/docs/data/material/components/slider/VerticalAccessibleSlider.tsx b/docs/data/material/components/slider/VerticalAccessibleSlider.tsx deleted file mode 100644 index bc66892e7f20b3..00000000000000 --- a/docs/data/material/components/slider/VerticalAccessibleSlider.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import Slider from '@mui/material/Slider'; - -export default function VerticalAccessibleSlider() { - function preventHorizontalKeyboardNavigation(event: React.KeyboardEvent) { - if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { - event.preventDefault(); - } - } - - return ( - - - - ); -} diff --git a/docs/data/material/components/slider/VerticalAccessibleSlider.tsx.preview b/docs/data/material/components/slider/VerticalAccessibleSlider.tsx.preview deleted file mode 100644 index 2f23a0596d7eee..00000000000000 --- a/docs/data/material/components/slider/VerticalAccessibleSlider.tsx.preview +++ /dev/null @@ -1,12 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/material/components/slider/VerticalSlider.js b/docs/data/material/components/slider/VerticalSlider.js index 502b71bdb8bb65..5ae94100d85d93 100644 --- a/docs/data/material/components/slider/VerticalSlider.js +++ b/docs/data/material/components/slider/VerticalSlider.js @@ -2,36 +2,13 @@ import * as React from 'react'; import Stack from '@mui/material/Stack'; import Slider from '@mui/material/Slider'; -function valuetext(value) { - return `${value}°C`; -} - -const marks = [ - { - value: 0, - label: '0°C', - }, - { - value: 20, - label: '20°C', - }, - { - value: 37, - label: '37°C', - }, - { - value: 100, - label: '100°C', - }, -]; - export default function VerticalSlider() { return ( @@ -45,7 +22,7 @@ export default function VerticalSlider() { 'Temperature'} orientation="vertical" - getAriaValueText={valuetext} + getAriaValueText={getAriaValueText} defaultValue={[20, 37]} valueLabelDisplay="auto" marks={marks} @@ -53,3 +30,26 @@ export default function VerticalSlider() { ); } + +function getAriaValueText(value) { + return `${value}°C`; +} + +const marks = [ + { + value: 0, + label: '0°C', + }, + { + value: 20, + label: '20°C', + }, + { + value: 37, + label: '37°C', + }, + { + value: 100, + label: '100°C', + }, +]; diff --git a/docs/data/material/components/slider/VerticalSlider.tsx b/docs/data/material/components/slider/VerticalSlider.tsx index 2f4e4ffa167ca8..743b7103b07703 100644 --- a/docs/data/material/components/slider/VerticalSlider.tsx +++ b/docs/data/material/components/slider/VerticalSlider.tsx @@ -2,36 +2,13 @@ import * as React from 'react'; import Stack from '@mui/material/Stack'; import Slider from '@mui/material/Slider'; -function valuetext(value: number) { - return `${value}°C`; -} - -const marks = [ - { - value: 0, - label: '0°C', - }, - { - value: 20, - label: '20°C', - }, - { - value: 37, - label: '37°C', - }, - { - value: 100, - label: '100°C', - }, -]; - export default function VerticalSlider() { return ( @@ -45,7 +22,7 @@ export default function VerticalSlider() { 'Temperature'} orientation="vertical" - getAriaValueText={valuetext} + getAriaValueText={getAriaValueText} defaultValue={[20, 37]} valueLabelDisplay="auto" marks={marks} @@ -53,3 +30,26 @@ export default function VerticalSlider() { ); } + +function getAriaValueText(value: number) { + return `${value}°C`; +} + +const marks = [ + { + value: 0, + label: '0°C', + }, + { + value: 20, + label: '20°C', + }, + { + value: 37, + label: '37°C', + }, + { + value: 100, + label: '100°C', + }, +]; diff --git a/docs/data/material/components/slider/slider.md b/docs/data/material/components/slider/slider.md index aa6afc620b1e17..e473d6375eb37b 100644 --- a/docs/data/material/components/slider/slider.md +++ b/docs/data/material/components/slider/slider.md @@ -98,17 +98,23 @@ You can learn more about this in the [overrides documentation page](/material-ui ## Vertical sliders +Set the `orientation` prop to `"vertical"` to create vertical sliders. The thumb will track vertical movement instead of horizontal movement. + {{"demo": "VerticalSlider.js"}} -**WARNING**: Chrome, Safari and newer Edge versions that is any browser based on WebKit exposes `` as horizontal ([chromium issue #40736841](https://issues.chromium.org/issues/40736841)). -By applying `-webkit-appearance: slider-vertical;` the slider is exposed as vertical. +:::warning +Chrome versions below 124 implement `aria-orientation` incorrectly for vertical sliders and exposes them as `'horizontal'` in the accessibility tree. ([Chromium issue #40736841](https://issues.chromium.org/issues/40736841)) + +The `-webkit-appearance: slider-vertical` CSS property can be used to correct this for these older versions, with the trade-off of causing a console warning in newer Chrome versions: -However, by applying `-webkit-appearance: slider-vertical;` keyboard navigation for horizontal keys (Arrow Left, Arrow Right) is reversed ([chromium issue #40739626](https://issues.chromium.org/issues/40739626)). -Usually, up and right should increase and left and down should decrease the value. -If you apply `-webkit-appearance` you could prevent keyboard navigation for horizontal arrow keys for a truly vertical slider. -This might be less confusing to users compared to a change in direction. +```css +.MuiSlider-thumb input { + -webkit-appearance: slider-vertical; +} +``` -{{"demo": "VerticalAccessibleSlider.js"}} +For Chrome 124 and newer, the slider includes the CSS [`writing-mode`](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_writing_modes/Vertical_controls#range_sliders_meters_and_progress_bars) property that fixes this bug. +::: ## Marks placement diff --git a/packages/mui-material/src/Slider/Slider.test.js b/packages/mui-material/src/Slider/Slider.test.js index 8b8d868434dbbd..8498815e1ff923 100644 --- a/packages/mui-material/src/Slider/Slider.test.js +++ b/packages/mui-material/src/Slider/Slider.test.js @@ -254,6 +254,50 @@ describe('', () => { expect(handleChange.args[0][1]).to.equal(80); expect(handleChange.args[1][1]).to.equal(78); }); + + describe('vertical orientation', () => { + describe('keyboard interactions', () => { + it('ArrowLeft and ArrowDown decrements the value', () => { + const { getByRole } = render(); + + const slider = getByRole('slider'); + + expect(slider).to.have.attribute('aria-valuenow', '50'); + + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, { key: 'ArrowLeft' }); + + expect(slider).to.have.attribute('aria-valuenow', '49'); + + fireEvent.keyDown(slider, { key: 'ArrowDown' }); + + expect(slider).to.have.attribute('aria-valuenow', '48'); + }); + + it('ArrowRight and ArrowUp increments the value', () => { + const { getByRole } = render(); + + const slider = getByRole('slider'); + + expect(slider).to.have.attribute('aria-valuenow', '50'); + + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, { key: 'ArrowRight' }); + + expect(slider).to.have.attribute('aria-valuenow', '51'); + + fireEvent.keyDown(slider, { key: 'ArrowUp' }); + + expect(slider).to.have.attribute('aria-valuenow', '52'); + }); + }); + }); }); describe('range', () => { @@ -421,41 +465,157 @@ describe('', () => { }); describe('prop: step', () => { - it('should handle a null step', () => { - const { getByRole, container } = render( - , - ); - stub(container.firstChild, 'getBoundingClientRect').callsFake(() => ({ - width: 100, - height: 10, - bottom: 10, - left: 0, - })); - const slider = getByRole('slider'); + describe('when step is `null`', () => { + it('values are defined by mark values', () => { + const { getByRole, container } = render( + , + ); + stub(container.firstChild, 'getBoundingClientRect').callsFake(() => ({ + width: 100, + height: 10, + bottom: 10, + left: 0, + })); + const slider = getByRole('slider'); + + fireEvent.touchStart( + container.firstChild, + createTouches([{ identifier: 1, clientX: 21, clientY: 0 }]), + ); + expect(slider).to.have.attribute('aria-valuenow', '20'); - fireEvent.touchStart( - container.firstChild, - createTouches([{ identifier: 1, clientX: 21, clientY: 0 }]), - ); - expect(slider).to.have.attribute('aria-valuenow', '20'); + fireEvent.change(slider, { + target: { + value: 21, + }, + }); + expect(slider).to.have.attribute('aria-valuenow', '30'); - fireEvent.change(slider, { - target: { - value: 21, - }, + fireEvent.change(slider, { + target: { + value: 29, + }, + }); + expect(slider).to.have.attribute('aria-valuenow', '20'); }); - expect(slider).to.have.attribute('aria-valuenow', '30'); - fireEvent.change(slider, { - target: { - value: 29, - }, + describe('keyboard interactions', () => { + describe('horizontal orientation', () => { + it('ArrowLeft and ArrowDown decrements the value', () => { + const { getByRole } = render( + , + ); + + const slider = getByRole('slider'); + + expect(slider).to.have.attribute('aria-valuenow', '79'); + + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, { key: 'ArrowLeft' }); + + expect(slider).to.have.attribute('aria-valuenow', '29'); + + fireEvent.keyDown(slider, { key: 'ArrowDown' }); + + expect(slider).to.have.attribute('aria-valuenow', '19'); + }); + + it('ArrowRight and ArrowUp increments the value', () => { + const { getByRole } = render( + , + ); + + const slider = getByRole('slider'); + + expect(slider).to.have.attribute('aria-valuenow', '9'); + + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, { key: 'ArrowRight' }); + + expect(slider).to.have.attribute('aria-valuenow', '19'); + + fireEvent.keyDown(slider, { key: 'ArrowUp' }); + + expect(slider).to.have.attribute('aria-valuenow', '29'); + }); + }); + + describe('vertical orientation', () => { + it('ArrowLeft and ArrowDown decrements the value', () => { + const { getByRole } = render( + , + ); + + const slider = getByRole('slider'); + + expect(slider).to.have.attribute('aria-valuenow', '79'); + + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, { key: 'ArrowLeft' }); + + expect(slider).to.have.attribute('aria-valuenow', '29'); + + fireEvent.keyDown(slider, { key: 'ArrowDown' }); + + expect(slider).to.have.attribute('aria-valuenow', '19'); + }); + + it('ArrowRight and ArrowUp increments the value', () => { + const { getByRole } = render( + , + ); + + const slider = getByRole('slider'); + + expect(slider).to.have.attribute('aria-valuenow', '9'); + + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, { key: 'ArrowRight' }); + + expect(slider).to.have.attribute('aria-valuenow', '19'); + + fireEvent.keyDown(slider, { key: 'ArrowUp' }); + + expect(slider).to.have.attribute('aria-valuenow', '29'); + }); + }); }); - expect(slider).to.have.attribute('aria-valuenow', '20'); }); it('change events with non integer numbers should work', () => { @@ -986,6 +1146,112 @@ describe('', () => { expect(handleChange.args[0][1]).to.equal(80); expect(handleChange.args[1][1]).to.equal(78); }); + + describe('keyboard interactions', () => { + it('ArrowRight and ArrowDown decrements the value', () => { + const { getByRole } = render( + + + , + ); + + const slider = getByRole('slider'); + + expect(slider).to.have.attribute('aria-valuenow', '50'); + + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, { key: 'ArrowRight' }); + + expect(slider).to.have.attribute('aria-valuenow', '49'); + + fireEvent.keyDown(slider, { key: 'ArrowDown' }); + + expect(slider).to.have.attribute('aria-valuenow', '48'); + }); + + it('ArrowLeft and ArrowUp increments the value', () => { + const { getByRole } = render( + + + , + ); + + const slider = getByRole('slider'); + + expect(slider).to.have.attribute('aria-valuenow', '50'); + + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, { key: 'ArrowLeft' }); + + expect(slider).to.have.attribute('aria-valuenow', '51'); + + fireEvent.keyDown(slider, { key: 'ArrowUp' }); + + expect(slider).to.have.attribute('aria-valuenow', '52'); + }); + + it('End key sets the value to max', () => { + const { getByRole } = render( + + + , + ); + + const slider = getByRole('slider'); + + expect(slider).to.have.attribute('aria-valuenow', '50'); + + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, { key: 'End' }); + + expect(slider).to.have.attribute('aria-valuenow', '99'); + }); + + it('Home key sets the value to min', () => { + const { getByRole } = render( + + + , + ); + + const slider = getByRole('slider'); + + expect(slider).to.have.attribute('aria-valuenow', '50'); + + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, { key: 'Home' }); + + expect(slider).to.have.attribute('aria-valuenow', '1'); + }); + }); }); describe('warnings', () => { diff --git a/packages/mui-material/src/Slider/useSlider.ts b/packages/mui-material/src/Slider/useSlider.ts index 0cd7ba016c3387..7dc3b3b2b54b99 100644 --- a/packages/mui-material/src/Slider/useSlider.ts +++ b/packages/mui-material/src/Slider/useSlider.ts @@ -687,6 +687,11 @@ export function useSlider(parameters: UseSliderParameters): UseSliderReturnValue }; }; + let cssWritingMode: 'vertical-rl' | 'vertical-lr' | undefined; + if (orientation === 'vertical') { + cssWritingMode = isRtl ? 'vertical-rl' : 'vertical-lr'; + } + const getHiddenInputProps = = {}>( externalProps: ExternalProps = {} as ExternalProps, ): UseSliderHiddenInputProps => { @@ -724,6 +729,7 @@ export function useSlider(parameters: UseSliderParameters): UseSliderReturnValue // So that VoiceOver's focus indicator matches the thumb's dimensions width: '100%', height: '100%', + writingMode: cssWritingMode, }, }; }; diff --git a/test/regressions/index.js b/test/regressions/index.js index 8d377681dfb032..11618772af4aba 100644 --- a/test/regressions/index.js +++ b/test/regressions/index.js @@ -178,7 +178,6 @@ const blacklist = [ 'docs-components-skeleton/Facebook.png', // Flaky image loading 'docs-components-skeleton/SkeletonChildren.png', // flaky image loading 'docs-components-skeleton/YouTube.png', // Flaky image loading - 'docs-components-slider/VerticalAccessibleSlider.png', // Redundant 'docs-components-snackbars/ConsecutiveSnackbars.png', // Needs interaction 'docs-components-snackbars/CustomizedSnackbars.png', // Redundant 'docs-components-snackbars/DirectionSnackbar.png', // Needs interaction