Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/src/pages/components/slider/CustomizedSlider.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,21 @@ const AirbnbSlider = styled(Slider)({
});

function AirbnbThumbComponent(props) {
const { children, ...other } = props;
return (
<SliderThumb {...props}>
<SliderThumb {...other}>
{children}
<span className="bar" />
<span className="bar" />
<span className="bar" />
</SliderThumb>
);
}

AirbnbThumbComponent.propTypes = {
children: PropTypes.node,
};

export default function CustomizedSlider() {
return (
<Root>
Expand Down
8 changes: 6 additions & 2 deletions docs/src/pages/components/slider/CustomizedSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,13 @@ const AirbnbSlider = styled(Slider)({
},
});

function AirbnbThumbComponent(props: any) {
interface AirbnbThumbComponentProps extends React.HTMLAttributes<unknown> {}

function AirbnbThumbComponent(props: AirbnbThumbComponentProps) {
const { children, ...other } = props;
return (
<SliderThumb {...props}>
<SliderThumb {...other}>
{children}
<span className="bar" />
<span className="bar" />
<span className="bar" />
Expand Down
33 changes: 33 additions & 0 deletions docs/src/pages/components/slider/VerticalAccessibleSlider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as React from 'react';
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';
import Slider from '@material-ui/core/Slider';

export default function VerticalSlider() {
function preventHorizontalKeyboardNavigation(event) {
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.preventDefault();
}
}

return (
<React.Fragment>
<Typography id="vertical-accessible-slider" gutterBottom>
Temperature
</Typography>
<Box sx={{ height: 300 }}>
<Slider
sx={{
'& input[type="range"]': {
WebkitAppearance: 'slider-vertical',
},
}}
orientation="vertical"
defaultValue={30}
aria-labelledby="vertical-accessible-slider"
onKeyDown={preventHorizontalKeyboardNavigation}
/>
</Box>
</React.Fragment>
);
}
33 changes: 33 additions & 0 deletions docs/src/pages/components/slider/VerticalAccessibleSlider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as React from 'react';
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';
import Slider from '@material-ui/core/Slider';

export default function VerticalSlider() {
function preventHorizontalKeyboardNavigation(event: React.KeyboardEvent) {
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.preventDefault();
}
}

return (
<React.Fragment>
<Typography id="vertical-accessible-slider" gutterBottom>
Temperature
</Typography>
<Box sx={{ height: 300 }}>
<Slider
sx={{
'& input[type="range"]': {
WebkitAppearance: 'slider-vertical',
},
}}
orientation="vertical"
defaultValue={30}
aria-labelledby="vertical-accessible-slider"
onKeyDown={preventHorizontalKeyboardNavigation}
/>
</Box>
</React.Fragment>
);
}
10 changes: 10 additions & 0 deletions docs/src/pages/components/slider/slider.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ Here are some examples of customizing the component. You can learn more about th

{{"demo": "pages/components/slider/VerticalSlider.js"}}

**WARNING**: Chrome, Safari and newer Edge versions i.e. any browser based on WebKit exposes `<Slider orientation="vertical" />` as horizontal ([chromium issue #1158217](https://bugs.chromium.org/p/chromium/issues/detail?id=1158217)).
By applying `-webkit-appearance: slider-vertical;` the slider is exposed as vertical.

However, by applying `-webkit-appearance: slider-vertical;` keyboard navigation for horizontal keys (<kbd>Arrow Left</kbd>, <kbd>Arrow Right</kbd>) is reversed ([chromium issue #1162640](https://bugs.chromium.org/p/chromium/issues/detail?id=1162640)).
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.

{{"demo": "pages/components/slider/VerticalAccessibleSlider.js"}}

## Track

The track shows the range available for user selection.
Expand Down
181 changes: 85 additions & 96 deletions packages/material-ui-unstyled/src/SliderUnstyled/SliderUnstyled.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
unstable_useEventCallback as useEventCallback,
unstable_useForkRef as useForkRef,
unstable_useControlled as useControlled,
visuallyHidden,
} from '@material-ui/utils';
import isHostComponent from '../utils/isHostComponent';
import sliderUnstyledClasses from './sliderUnstyledClasses';
Expand All @@ -19,6 +20,9 @@ function asc(a, b) {
}

function clamp(value, min, max) {
if (value == null) {
return min;
}
return Math.min(Math.max(min, value), max);
}

Expand Down Expand Up @@ -102,7 +106,7 @@ function focusThumb({ sliderRef, activeIndex, setActive }) {
!sliderRef.current.contains(doc.activeElement) ||
Number(doc.activeElement.getAttribute('data-index')) !== activeIndex
) {
sliderRef.current.querySelector(`[role="slider"][data-index="${activeIndex}"]`).focus();
sliderRef.current.querySelector(`[type="range"][data-index="${activeIndex}"]`).focus();
}

if (setActive) {
Expand Down Expand Up @@ -209,7 +213,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) {
isRtl = false,
components = {},
componentsProps = {},
/* eslint-disable react/prop-types */
/* eslint-disable-next-line react/prop-types */
theme,
...other
} = props;
Expand All @@ -223,7 +227,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) {

const [valueDerived, setValueState] = useControlled({
controlled: valueProp,
default: defaultValue,
default: defaultValue ?? min,
name: 'Slider',
});

Expand Down Expand Up @@ -304,62 +308,30 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) {
setFocusVisible(-1);
}

const handleKeyDown = useEventCallback((event) => {
const handleHiddenInputChange = useEventCallback((event) => {
const index = Number(event.currentTarget.getAttribute('data-index'));
const value = values[index];
const tenPercents = (max - min) / 10;
const marksValues = marks.map((mark) => mark.value);
const marksIndex = marksValues.indexOf(value);
let newValue;
const increaseKey = isRtl ? 'ArrowLeft' : 'ArrowRight';
const decreaseKey = isRtl ? 'ArrowRight' : 'ArrowLeft';

switch (event.key) {
case 'Home':
newValue = min;
break;
case 'End':
newValue = max;
break;
case 'PageUp':
if (step) {
newValue = value + tenPercents;
}
break;
case 'PageDown':
if (step) {
newValue = value - tenPercents;
}
break;
case increaseKey:
case 'ArrowUp':
if (step) {
newValue = value + step;
} else {
newValue = marksValues[marksIndex + 1] || marksValues[marksValues.length - 1];
}
break;
case decreaseKey:
case 'ArrowDown':
if (step) {
newValue = value - step;
} else {
newValue = marksValues[marksIndex - 1] || marksValues[0];
}
break;
default:
return;
}

// Prevent scroll of the page
event.preventDefault();
let newValue = event.target.valueAsNumber;

if (step) {
newValue = roundValueToStep(newValue, step, min);
if (marks && step == null) {
newValue = newValue < value ? marksValues[marksIndex - 1] : marksValues[marksIndex + 1];
}

newValue = clamp(newValue, min, max);

if (marks && step == null) {
const markValues = marks.map((mark) => mark.value);
const currentMarkIndex = markValues.indexOf(values[index]);

newValue =
newValue < values[index]
? markValues[currentMarkIndex - 1]
: markValues[currentMarkIndex + 1];
}

if (range) {
const previousValue = newValue;
newValue = setValueIndex({
Expand All @@ -377,6 +349,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) {
if (handleChange) {
handleChange(event, newValue);
}

if (onChangeCommitted) {
onChangeCommitted(event, newValue);
}
Expand Down Expand Up @@ -650,7 +623,6 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) {
className={clsx(utilityClasses.track, trackProps.className)}
style={{ ...trackStyle, ...trackProps.style }}
/>
<input value={values.join(',')} name={name} type="hidden" />
{marks.map((mark, index) => {
const percent = valueToPercent(mark.value, min, max);
const style = axisProps[axis].offset(percent);
Expand Down Expand Up @@ -715,55 +687,72 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) {
const ValueLabelComponent = valueLabelDisplay === 'off' ? Forward : ValueLabel;

return (
<ValueLabelComponent
key={index}
valueLabelFormat={valueLabelFormat}
valueLabelDisplay={valueLabelDisplay}
value={
typeof valueLabelFormat === 'function'
? valueLabelFormat(scale(value), index)
: valueLabelFormat
}
index={index}
open={open === index || active === index || valueLabelDisplay === 'on'}
disabled={disabled}
{...valueLabelProps}
className={clsx(utilityClasses.valueLabel, valueLabelProps.className)}
{...(!isHostComponent(ValueLabel) && {
styleProps: { ...styleProps, ...valueLabelProps.styleProps },
theme,
})}
>
<Thumb
tabIndex={disabled ? null : 0}
role="slider"
data-index={index}
aria-label={getAriaLabel ? getAriaLabel(index) : ariaLabel}
aria-labelledby={ariaLabelledby}
aria-orientation={orientation}
aria-valuemax={scale(max)}
aria-valuemin={scale(min)}
aria-valuenow={scale(value)}
aria-valuetext={
getAriaValueText ? getAriaValueText(scale(value), index) : ariaValuetext
<React.Fragment key={index}>
<ValueLabelComponent
valueLabelFormat={valueLabelFormat}
valueLabelDisplay={valueLabelDisplay}
value={
typeof valueLabelFormat === 'function'
? valueLabelFormat(scale(value), index)
: valueLabelFormat
}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
onMouseOver={handleMouseOver}
onMouseLeave={handleMouseLeave}
{...thumbProps}
className={clsx(utilityClasses.thumb, thumbProps.className, {
[utilityClasses.active]: active === index,
[utilityClasses.focusVisible]: focusVisible === index,
})}
{...(!isHostComponent(Thumb) && {
styleProps: { ...styleProps, ...thumbProps.styleProps },
index={index}
open={open === index || active === index || valueLabelDisplay === 'on'}
disabled={disabled}
{...valueLabelProps}
className={clsx(utilityClasses.valueLabel, valueLabelProps.className)}
{...(!isHostComponent(ValueLabel) && {
styleProps: { ...styleProps, ...valueLabelProps.styleProps },
theme,
})}
style={{ ...style, ...thumbProps.style }}
/>
</ValueLabelComponent>
>
<Thumb
data-index={index}
onMouseOver={handleMouseOver}
onMouseLeave={handleMouseLeave}
{...thumbProps}
className={clsx(utilityClasses.thumb, thumbProps.className, {
[utilityClasses['active']]: active === index,
[utilityClasses['focusVisible']]: focusVisible === index,
})}
{...(!isHostComponent(Thumb) && {
styleProps: { ...styleProps, ...thumbProps.styleProps },
theme,
})}
style={{ ...style, ...thumbProps.style }}
>
<input
data-index={index}
aria-label={getAriaLabel ? getAriaLabel(index) : ariaLabel}
aria-labelledby={ariaLabelledby}
aria-orientation={orientation}
aria-valuemax={scale(max)}
aria-valuemin={scale(min)}
aria-valuenow={scale(value)}
aria-valuetext={
getAriaValueText ? getAriaValueText(scale(value), index) : ariaValuetext
}
onFocus={handleFocus}
onBlur={handleBlur}
name={name}
type="range"
min={props.min}
max={props.max}
step={props.step}
disabled={disabled}
value={values[index]}
onChange={handleHiddenInputChange}
style={{
...visuallyHidden,
direction: isRtl ? 'rtl' : 'ltr',
// So that VoiceOver's focus indicator matches the thumb's dimensions
width: '100%',
height: '100%',
}}
/>
</Thumb>
</ValueLabelComponent>
</React.Fragment>
);
})}
</Root>
Expand Down
Loading