From 1d01a824065cfd5fc784002e77dc7ec38b5db474 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Thu, 13 Jun 2019 00:03:45 +0200 Subject: [PATCH] [Slider] Major upgrade (#15703) * [Slider] Major upgrade * ThumbLabel.js => ValueLabel.js * increase spacing in the demo * fix vertical rtl * change default value of valueLabelDisplay * Update packages/material-ui-lab/src/Slider/Slider.js Co-Authored-By: Matt * Fix empty input slider * Sebastian review Co-Authored-By: Sebastian Silbermann * Improve focus-visible logic * Update docs/src/pages/components/slider/slider.md Co-Authored-By: Josh Wooding <12938082+joshwooding@users.noreply.github.com> --- .../components/slider/ContinuousSlider.js | 42 + .../components/slider/ContinuousSlider.tsx | 42 + .../components/slider/CustomIconSlider.js | 72 - .../slider/CustomValueReducerSlider.js | 77 - .../components/slider/CustomizedSlider.js | 219 ++- .../components/slider/CustomizedSlider.tsx | 230 ++- .../pages/components/slider/DisabledSlider.js | 31 - .../pages/components/slider/DiscreteSlider.js | 97 ++ .../components/slider/DiscreteSlider.tsx | 99 ++ .../pages/components/slider/InputSlider.js | 73 + .../pages/components/slider/InputSlider.tsx | 73 + .../pages/components/slider/RangeSlider.js | 38 + .../pages/components/slider/RangeSlider.tsx | 39 + .../pages/components/slider/SimpleSlider.js | 47 - .../src/pages/components/slider/StepSlider.js | 47 - .../pages/components/slider/VerticalSlider.js | 82 +- .../components/slider/VerticalSlider.tsx | 67 + docs/src/pages/components/slider/slider.md | 49 +- .../material-ui-lab/src/Slider/Slider.d.ts | 68 +- packages/material-ui-lab/src/Slider/Slider.js | 1233 +++++++++-------- .../src/Slider/Slider.spec.tsx | 15 - .../material-ui-lab/src/Slider/Slider.test.js | 272 ++-- .../material-ui-lab/src/Slider/ValueLabel.js | 86 ++ packages/material-ui-lab/src/Slider/index.js | 2 +- .../src/SpeedDial/SpeedDial.js | 11 +- packages/material-ui-lab/src/utils/clamp.d.ts | 1 - packages/material-ui-lab/src/utils/clamp.js | 3 - packages/material-ui-lab/src/utils/index.d.ts | 1 - packages/material-ui-lab/src/utils/index.js | 2 - .../src/ButtonBase/ButtonBase.test.js | 2 +- .../src/FilledInput/FilledInput.js | 2 +- packages/material-ui/src/Input/Input.js | 2 +- .../material-ui/src/InputBase/InputBase.js | 2 +- .../src/OutlinedInput/OutlinedInput.js | 2 +- .../material-ui/src/Popper/Popper.spec.tsx | 2 +- .../material-ui/src/RadioGroup/RadioGroup.js | 2 +- packages/material-ui/src/Tabs/Tabs.js | 2 +- .../material-ui/src/utils/focusVisible.js | 6 +- packages/material-ui/src/utils/index.js | 3 +- pages/api/filled-input.md | 2 +- pages/api/input-base.md | 2 +- pages/api/input.md | 2 +- pages/api/outlined-input.md | 2 +- pages/api/radio-group.md | 2 +- pages/api/slider.md | 56 +- pages/api/tabs.md | 2 +- scripts/sizeSnapshot/webpack.config.js | 6 + 47 files changed, 2029 insertions(+), 1188 deletions(-) create mode 100644 docs/src/pages/components/slider/ContinuousSlider.js create mode 100644 docs/src/pages/components/slider/ContinuousSlider.tsx delete mode 100644 docs/src/pages/components/slider/CustomIconSlider.js delete mode 100644 docs/src/pages/components/slider/CustomValueReducerSlider.js delete mode 100644 docs/src/pages/components/slider/DisabledSlider.js create mode 100644 docs/src/pages/components/slider/DiscreteSlider.js create mode 100644 docs/src/pages/components/slider/DiscreteSlider.tsx create mode 100644 docs/src/pages/components/slider/InputSlider.js create mode 100644 docs/src/pages/components/slider/InputSlider.tsx create mode 100644 docs/src/pages/components/slider/RangeSlider.js create mode 100644 docs/src/pages/components/slider/RangeSlider.tsx delete mode 100644 docs/src/pages/components/slider/SimpleSlider.js delete mode 100644 docs/src/pages/components/slider/StepSlider.js create mode 100644 docs/src/pages/components/slider/VerticalSlider.tsx delete mode 100644 packages/material-ui-lab/src/Slider/Slider.spec.tsx create mode 100644 packages/material-ui-lab/src/Slider/ValueLabel.js delete mode 100644 packages/material-ui-lab/src/utils/clamp.d.ts delete mode 100644 packages/material-ui-lab/src/utils/clamp.js delete mode 100644 packages/material-ui-lab/src/utils/index.d.ts delete mode 100644 packages/material-ui-lab/src/utils/index.js diff --git a/docs/src/pages/components/slider/ContinuousSlider.js b/docs/src/pages/components/slider/ContinuousSlider.js new file mode 100644 index 00000000000000..f3640971671d97 --- /dev/null +++ b/docs/src/pages/components/slider/ContinuousSlider.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import Slider from '@material-ui/lab/Slider'; +import VolumeDown from '@material-ui/icons/VolumeDown'; +import VolumeUp from '@material-ui/icons/VolumeUp'; + +const useStyles = makeStyles({ + root: { + width: 200, + }, +}); + +export default function ContinuousSlider() { + const classes = useStyles(); + const [value, setValue] = React.useState(30); + + const handleChange = (event, newValue) => { + setValue(newValue); + }; + + return ( +
+ + Volume + + + + + + + + + + + + + +
+ ); +} diff --git a/docs/src/pages/components/slider/ContinuousSlider.tsx b/docs/src/pages/components/slider/ContinuousSlider.tsx new file mode 100644 index 00000000000000..144e2a1836834c --- /dev/null +++ b/docs/src/pages/components/slider/ContinuousSlider.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import Slider from '@material-ui/lab/Slider'; +import VolumeDown from '@material-ui/icons/VolumeDown'; +import VolumeUp from '@material-ui/icons/VolumeUp'; + +const useStyles = makeStyles({ + root: { + width: 200, + }, +}); + +export default function ContinuousSlider() { + const classes = useStyles(); + const [value, setValue] = React.useState(30); + + const handleChange = (event: any, newValue: number | number[]) => { + setValue(newValue); + }; + + return ( +
+ + Volume + + + + + + + + + + + + + +
+ ); +} diff --git a/docs/src/pages/components/slider/CustomIconSlider.js b/docs/src/pages/components/slider/CustomIconSlider.js deleted file mode 100644 index 0451c15e23bba0..00000000000000 --- a/docs/src/pages/components/slider/CustomIconSlider.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import Slider from '@material-ui/lab/Slider'; -import LensIcon from '@material-ui/icons/LensOutlined'; - -const styles = { - root: { - width: 300, - }, - slider: { - padding: '22px 0px', - }, - thumbIcon: { - borderRadius: '50%', - }, - thumbIconWrapper: { - backgroundColor: '#fff', - }, -}; - -class CustomIconSlider extends React.Component { - state = { - value: 50, - }; - - handleChange = (event, value) => { - this.setState({ value }); - }; - - render() { - const { classes } = this.props; - const { value } = this.state; - - return ( -
- Image thumb - - } - /> - Icon thumb - } - /> -
- ); - } -} - -CustomIconSlider.propTypes = { - classes: PropTypes.object.isRequired, -}; - -export default withStyles(styles)(CustomIconSlider); diff --git a/docs/src/pages/components/slider/CustomValueReducerSlider.js b/docs/src/pages/components/slider/CustomValueReducerSlider.js deleted file mode 100644 index 78a975eadaca0c..00000000000000 --- a/docs/src/pages/components/slider/CustomValueReducerSlider.js +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Slider, { defaultValueReducer } from '@material-ui/lab/Slider'; - -const styles = { - root: { - width: 300, - }, -}; - -/** - * a value reducer that will snap to multiple of 10 but also to the edge value - * Useful here because the max=104 is not a multiple of 10 - */ -function valueReducer(rawValue, props, event) { - const { disabled, max, min, step } = props; - - function roundToStep(number) { - return Math.round(number / step) * step; - } - - if (!disabled && step) { - if (rawValue > min && rawValue < max) { - if (rawValue === max - step) { - // If moving the Slider using arrow keys and value is formerly an maximum edge value - return roundToStep(rawValue + step / 2); - } - if (rawValue === min + step) { - // Same for minimum edge value - return roundToStep(rawValue - step / 2); - } - return roundToStep(rawValue); - } - return rawValue; - } - - return defaultValueReducer(rawValue, props, event); -} - -/** - * this slider has a max that is not a multiple of its step. We use a custom - * `valueReducer` to adjust the given values - */ -class StepSlider extends React.Component { - state = { - value: 30, - }; - - handleChange = (event, value) => { - this.setState({ value }); - }; - - render() { - const { classes } = this.props; - const { value } = this.state; - - return ( -
- -
- ); - } -} - -StepSlider.propTypes = { - classes: PropTypes.object.isRequired, -}; - -export default withStyles(styles)(StepSlider); diff --git a/docs/src/pages/components/slider/CustomizedSlider.js b/docs/src/pages/components/slider/CustomizedSlider.js index 1bd2c5da5d6756..3b0e156fdabd56 100644 --- a/docs/src/pages/components/slider/CustomizedSlider.js +++ b/docs/src/pages/components/slider/CustomizedSlider.js @@ -1,54 +1,223 @@ import React from 'react'; -import { fade, withStyles, makeStyles } from '@material-ui/core/styles'; +import PropTypes from 'prop-types'; +import { withStyles, makeStyles } from '@material-ui/core/styles'; import Paper from '@material-ui/core/Paper'; import Slider from '@material-ui/lab/Slider'; +import Typography from '@material-ui/core/Typography'; +import Tooltip from '@material-ui/core/Tooltip'; -const useStyles = makeStyles({ +const useStyles = makeStyles(theme => ({ root: { - width: 300, + width: 300 + 24 * 2, padding: 24, }, -}); + margin: { + height: theme.spacing(3), + }, +})); + +function ValueLabelComponent(props) { + const { children, open, value } = props; + + const popperRef = React.useRef(null); + React.useEffect(() => { + if (popperRef.current) { + popperRef.current.update(); + } + }); + + return ( + + {children} + + ); +} + +ValueLabelComponent.propTypes = { + children: PropTypes.element.isRequired, + open: PropTypes.bool.isRequired, + value: PropTypes.number.isRequired, +}; + +const iOSBoxShadow = + '0 3px 1px rgba(0,0,0,0.1),0 4px 8px rgba(0,0,0,0.13),0 0 0 1px rgba(0,0,0,0.02)'; + +const marks = [ + { + value: 0, + }, + { + value: 20, + }, + { + value: 37, + }, + { + value: 100, + }, +]; -const StyledSlider = withStyles({ +const IOSSlider = withStyles({ + root: { + color: '#3880ff', + height: 2, + padding: '15px 0', + }, + thumb: { + height: 28, + width: 28, + backgroundColor: '#fff', + boxShadow: iOSBoxShadow, + marginTop: -14, + marginLeft: -14, + '&:focus,&:hover,&$active': { + boxShadow: '0 3px 1px rgba(0,0,0,0.1),0 4px 8px rgba(0,0,0,0.3),0 0 0 1px rgba(0,0,0,0.02)', + // Reset on touch devices, it doesn't add specificity + '@media (hover: none)': { + boxShadow: iOSBoxShadow, + }, + }, + }, + active: {}, + valueLabel: { + left: 'calc(-50% + 11px)', + top: -22, + '& *': { + background: 'transparent', + color: '#000', + }, + }, + track: { + height: 2, + }, + rail: { + height: 2, + opacity: 0.5, + backgroundColor: '#bfbfbf', + }, + mark: { + backgroundColor: '#bfbfbf', + height: 8, + width: 1, + marginTop: -3, + }, + markActive: { + backgroundColor: 'currentColor', + }, +})(Slider); + +const PrettoSlider = withStyles({ + root: { + color: '#52af77', + height: 8, + }, thumb: { height: 24, width: 24, backgroundColor: '#fff', - border: '2px solid #de235b', - '&$focused, &:hover': { - boxShadow: `0px 0px 0px ${8}px ${fade('#de235b', 0.16)}`, + border: '2px solid currentColor', + marginTop: -8, + marginLeft: -12, + '&:focus,&:hover,&$active': { + boxShadow: 'inherit', }, - '&$activated': { - boxShadow: `0px 0px 0px ${8 * 1.5}px ${fade('#de235b', 0.16)}`, + }, + active: {}, + valueLabel: { + left: 'calc(-50% + 4px)', + }, + track: { + height: 8, + borderRadius: 4, + }, + rail: { + height: 8, + borderRadius: 4, + }, +})(Slider); + +const AirbnbSlider = withStyles({ + root: { + color: '#3a8589', + height: 3, + padding: '13px 0', + }, + thumb: { + height: 27, + width: 27, + backgroundColor: '#fff', + border: '1px solid currentColor', + marginTop: -12, + marginLeft: -13, + boxShadow: '#ebebeb 0px 2px 2px', + '&:focus,&:hover,&$active': { + boxShadow: '#ccc 0px 2px 3px 1px', }, - '&$jumped': { - boxShadow: `0px 0px 0px ${8 * 1.5}px ${fade('#de235b', 0.16)}`, + '& .bar': { + // display: inline-block !important; + height: 9, + width: 1, + backgroundColor: 'currentColor', + marginLeft: 1, + marginRight: 1, }, }, + active: {}, + valueLabel: { + left: 'calc(-50% + 4px)', + }, track: { - backgroundColor: '#de235b', - height: 8, + height: 3, }, - trackAfter: { - backgroundColor: '#d0d7dc', + rail: { + color: '#d8d8d8', + opacity: 1, + height: 3, }, - focused: {}, - activated: {}, - jumped: {}, })(Slider); +function AirbnbThumbComponent(props) { + return ( + + + + + + ); +} + export default function CustomizedSlider() { const classes = useStyles(); - const [value, setValue] = React.useState(50); - - const handleChange = (event, newValue) => { - setValue(newValue); - }; return ( - + iOS + +
+ pretto.fr + +
+ Tooltip value label + +
+ Airbnb + ); } diff --git a/docs/src/pages/components/slider/CustomizedSlider.tsx b/docs/src/pages/components/slider/CustomizedSlider.tsx index 76fed16ef3f5c6..c339983e1a05c5 100644 --- a/docs/src/pages/components/slider/CustomizedSlider.tsx +++ b/docs/src/pages/components/slider/CustomizedSlider.tsx @@ -1,54 +1,232 @@ import React from 'react'; -import { fade, withStyles, makeStyles } from '@material-ui/core/styles'; +import PropTypes from 'prop-types'; +import { fade, withStyles, makeStyles, Theme, createStyles } from '@material-ui/core/styles'; import Paper from '@material-ui/core/Paper'; import Slider from '@material-ui/lab/Slider'; +import Typography from '@material-ui/core/Typography'; +import Tooltip from '@material-ui/core/Tooltip'; +import PopperJs from 'popper.js'; -const useStyles = makeStyles({ +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + width: 300 + 24 * 2, + padding: 24, + }, + margin: { + height: theme.spacing(3), + }, + }), +); + +interface Props { + children: React.ReactElement; + open: boolean; + value: number; +} + +function ValueLabelComponent(props: Props) { + const { children, open, value } = props; + + const popperRef = React.useRef(null); + React.useEffect(() => { + if (popperRef.current) { + popperRef.current.update(); + } + }); + + return ( + + {children} + + ); +} + +ValueLabelComponent.propTypes = { + children: PropTypes.element.isRequired, + open: PropTypes.bool.isRequired, + value: PropTypes.number.isRequired, +}; + +const iOSBoxShadow = + '0 3px 1px rgba(0,0,0,0.1),0 4px 8px rgba(0,0,0,0.13),0 0 0 1px rgba(0,0,0,0.02)'; + +const marks = [ + { + value: 0, + }, + { + value: 20, + }, + { + value: 37, + }, + { + value: 100, + }, +]; + +const IOSSlider = withStyles({ root: { - width: 300, - padding: 24, + color: '#3880ff', + height: 2, + padding: '15px 0', + }, + thumb: { + height: 28, + width: 28, + backgroundColor: '#fff', + boxShadow: iOSBoxShadow, + marginTop: -14, + marginLeft: -14, + '&:focus,&:hover,&$active': { + boxShadow: '0 3px 1px rgba(0,0,0,0.1),0 4px 8px rgba(0,0,0,0.3),0 0 0 1px rgba(0,0,0,0.02)', + // Reset on touch devices, it doesn't add specificity + '@media (hover: none)': { + boxShadow: iOSBoxShadow, + }, + }, + }, + active: {}, + valueLabel: { + left: 'calc(-50% + 11px)', + top: -22, + '& *': { + background: 'transparent', + color: '#000', + }, + }, + track: { + height: 2, + }, + rail: { + height: 2, + opacity: 0.5, + backgroundColor: '#bfbfbf', }, -}); + mark: { + backgroundColor: '#bfbfbf', + height: 8, + width: 1, + marginTop: -3, + }, + markActive: { + backgroundColor: 'currentColor', + }, +})(Slider); -const StyledSlider = withStyles({ +const PrettoSlider = withStyles({ + root: { + color: '#52af77', + height: 8, + }, thumb: { height: 24, width: 24, backgroundColor: '#fff', - border: '2px solid #de235b', - '&$focused, &:hover': { - boxShadow: `0px 0px 0px ${8}px ${fade('#de235b', 0.16)}`, + border: '2px solid currentColor', + marginTop: -8, + marginLeft: -12, + '&:focus,&:hover,&$active': { + boxShadow: 'inherit', }, - '&$activated': { - boxShadow: `0px 0px 0px ${8 * 1.5}px ${fade('#de235b', 0.16)}`, + }, + active: {}, + valueLabel: { + left: 'calc(-50% + 4px)', + }, + track: { + height: 8, + borderRadius: 4, + }, + rail: { + height: 8, + borderRadius: 4, + }, +})(Slider); + +const AirbnbSlider = withStyles({ + root: { + color: '#3a8589', + height: 3, + padding: '13px 0', + }, + thumb: { + height: 27, + width: 27, + backgroundColor: '#fff', + border: '1px solid currentColor', + marginTop: -12, + marginLeft: -13, + boxShadow: '#ebebeb 0px 2px 2px', + '&:focus,&:hover,&$active': { + boxShadow: '#ccc 0px 2px 3px 1px', }, - '&$jumped': { - boxShadow: `0px 0px 0px ${8 * 1.5}px ${fade('#de235b', 0.16)}`, + '& .bar': { + // display: inline-block !important; + height: 9, + width: 1, + backgroundColor: 'currentColor', + marginLeft: 1, + marginRight: 1, }, }, + active: {}, + valueLabel: { + left: 'calc(-50% + 4px)', + }, track: { - backgroundColor: '#de235b', - height: 8, + height: 3, }, - trackAfter: { - backgroundColor: '#d0d7dc', + rail: { + color: '#d8d8d8', + opacity: 1, + height: 3, }, - focused: {}, - activated: {}, - jumped: {}, })(Slider); +function AirbnbThumbComponent(props: any) { + return ( + + + + + + ); +} + export default function CustomizedSlider() { const classes = useStyles(); - const [value, setValue] = React.useState(50); - - const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { - setValue(newValue); - }; return ( - + iOS + +
+ pretto.fr + +
+ Tooltip value label + +
+ Airbnb + ); } diff --git a/docs/src/pages/components/slider/DisabledSlider.js b/docs/src/pages/components/slider/DisabledSlider.js deleted file mode 100644 index 56747d7bc46806..00000000000000 --- a/docs/src/pages/components/slider/DisabledSlider.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Slider from '@material-ui/lab/Slider'; - -const styles = { - root: { - width: 300, - }, - slider: { - padding: '8px 0px', - }, -}; - -function DisabledSlider(props) { - const { classes } = props; - - return ( -
- - - -
- ); -} - -DisabledSlider.propTypes = { - classes: PropTypes.object.isRequired, -}; - -export default withStyles(styles)(DisabledSlider); diff --git a/docs/src/pages/components/slider/DiscreteSlider.js b/docs/src/pages/components/slider/DiscreteSlider.js new file mode 100644 index 00000000000000..645416bc25edd2 --- /dev/null +++ b/docs/src/pages/components/slider/DiscreteSlider.js @@ -0,0 +1,97 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import Slider from '@material-ui/lab/Slider'; + +const useStyles = makeStyles(theme => ({ + root: { + width: 300, + }, + margin: { + height: theme.spacing(3), + }, +})); + +const marks = [ + { + value: 0, + label: '0°C', + }, + { + value: 20, + label: '20°C', + }, + { + value: 37, + label: '37°C', + }, + { + value: 100, + label: '100°C', + }, +]; + +function valuetext(value) { + return `${value}°C`; +} + +function valueLabelFormat(value) { + return marks.findIndex(mark => mark.value === value) + 1; +} + +export default function DiscreteSlider() { + const classes = useStyles(); + + return ( +
+ + Temperature + + +
+ + Custom marks + + +
+ + Restricted values + + +
+ + Always visible + + +
+ ); +} diff --git a/docs/src/pages/components/slider/DiscreteSlider.tsx b/docs/src/pages/components/slider/DiscreteSlider.tsx new file mode 100644 index 00000000000000..566549cc115af8 --- /dev/null +++ b/docs/src/pages/components/slider/DiscreteSlider.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import Slider from '@material-ui/lab/Slider'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + width: 300, + }, + margin: { + height: theme.spacing(3), + }, + }), +); + +const marks = [ + { + value: 0, + label: '0°C', + }, + { + value: 20, + label: '20°C', + }, + { + value: 37, + label: '37°C', + }, + { + value: 100, + label: '100°C', + }, +]; + +function valuetext(value: number) { + return `${value}°C`; +} + +function valueLabelFormat(value: number) { + return marks.findIndex(mark => mark.value === value) + 1; +} + +export default function DiscreteSlider() { + const classes = useStyles(); + + return ( +
+ + Temperature + + +
+ + Custom marks + + +
+ + Restricted values + + +
+ + Always visible + + +
+ ); +} diff --git a/docs/src/pages/components/slider/InputSlider.js b/docs/src/pages/components/slider/InputSlider.js new file mode 100644 index 00000000000000..24e5159a335f70 --- /dev/null +++ b/docs/src/pages/components/slider/InputSlider.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import Slider from '@material-ui/lab/Slider'; +import Input from '@material-ui/core/Input'; +import VolumeUp from '@material-ui/icons/VolumeUp'; + +const useStyles = makeStyles({ + root: { + width: 250, + }, + input: { + width: 42, + }, +}); + +export default function InputSlider() { + const classes = useStyles(); + const [value, setValue] = React.useState(30); + + const handleSliderChange = (event, newValue) => { + setValue(newValue); + }; + + const handleInputChange = event => { + setValue(event.target.value === '' ? '' : Number(event.target.value)); + }; + + const handleBlur = () => { + if (value < 0) { + setValue(0); + } else if (value > 100) { + setValue(100); + } + }; + + return ( +
+ + Volume + + + + + + + + + + + + +
+ ); +} diff --git a/docs/src/pages/components/slider/InputSlider.tsx b/docs/src/pages/components/slider/InputSlider.tsx new file mode 100644 index 00000000000000..2f0b20fc3bb812 --- /dev/null +++ b/docs/src/pages/components/slider/InputSlider.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import Slider from '@material-ui/lab/Slider'; +import Input from '@material-ui/core/Input'; +import VolumeUp from '@material-ui/icons/VolumeUp'; + +const useStyles = makeStyles({ + root: { + width: 250, + }, + input: { + width: 42, + }, +}); + +export default function InputSlider() { + const classes = useStyles(); + const [value, setValue] = React.useState>(30); + + const handleSliderChange = (event: any, newValue: number | number[]) => { + setValue(newValue); + }; + + const handleInputChange = (event: React.ChangeEvent) => { + setValue(event.target.value === '' ? '' : Number(event.target.value)); + }; + + const handleBlur = () => { + if (value < 0) { + setValue(0); + } else if (value > 100) { + setValue(100); + } + }; + + return ( +
+ + Volume + + + + + + + + + + + + +
+ ); +} diff --git a/docs/src/pages/components/slider/RangeSlider.js b/docs/src/pages/components/slider/RangeSlider.js new file mode 100644 index 00000000000000..8ef7cb23c305ba --- /dev/null +++ b/docs/src/pages/components/slider/RangeSlider.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import Slider from '@material-ui/lab/Slider'; + +const useStyles = makeStyles({ + root: { + width: 300, + }, +}); + +function valuetext(value) { + return `${value}°C`; +} + +export default function RangeSlider() { + const classes = useStyles(); + const [value, setValue] = React.useState([20, 37]); + + const handleChange = (event, newValue) => { + setValue(newValue); + }; + + return ( +
+ + Temperature range + + +
+ ); +} diff --git a/docs/src/pages/components/slider/RangeSlider.tsx b/docs/src/pages/components/slider/RangeSlider.tsx new file mode 100644 index 00000000000000..5d8859b7356ed7 --- /dev/null +++ b/docs/src/pages/components/slider/RangeSlider.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import Slider from '@material-ui/lab/Slider'; +import Tooltip from '@material-ui/core/Tooltip'; + +const useStyles = makeStyles({ + root: { + width: 300, + }, +}); + +function valuetext(value: number) { + return `${value}°C`; +} + +export default function RangeSlider() { + const classes = useStyles(); + const [value, setValue] = React.useState([20, 37]); + + const handleChange = (event: any, newValue: number | number[]) => { + setValue(newValue); + }; + + return ( +
+ + Temperature range + + +
+ ); +} diff --git a/docs/src/pages/components/slider/SimpleSlider.js b/docs/src/pages/components/slider/SimpleSlider.js deleted file mode 100644 index a95faf14812786..00000000000000 --- a/docs/src/pages/components/slider/SimpleSlider.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import Slider from '@material-ui/lab/Slider'; - -const styles = { - root: { - width: 300, - }, - slider: { - padding: '22px 0px', - }, -}; - -class SimpleSlider extends React.Component { - state = { - value: 50, - }; - - handleChange = (event, value) => { - this.setState({ value }); - }; - - render() { - const { classes } = this.props; - const { value } = this.state; - - return ( -
- Slider label - -
- ); - } -} - -SimpleSlider.propTypes = { - classes: PropTypes.object.isRequired, -}; - -export default withStyles(styles)(SimpleSlider); diff --git a/docs/src/pages/components/slider/StepSlider.js b/docs/src/pages/components/slider/StepSlider.js deleted file mode 100644 index be2c2faec09213..00000000000000 --- a/docs/src/pages/components/slider/StepSlider.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Slider from '@material-ui/lab/Slider'; - -const styles = { - root: { - width: 300, - }, - slider: { - padding: '22px 0px', - }, -}; - -class StepSlider extends React.Component { - state = { - value: 3, - }; - - handleChange = (event, value) => { - this.setState({ value }); - }; - - render() { - const { classes } = this.props; - const { value } = this.state; - - return ( -
- -
- ); - } -} - -StepSlider.propTypes = { - classes: PropTypes.object.isRequired, -}; - -export default withStyles(styles)(StepSlider); diff --git a/docs/src/pages/components/slider/VerticalSlider.js b/docs/src/pages/components/slider/VerticalSlider.js index 3b6f878e8e4814..f582e33dc540fe 100644 --- a/docs/src/pages/components/slider/VerticalSlider.js +++ b/docs/src/pages/components/slider/VerticalSlider.js @@ -1,41 +1,67 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; +import { makeStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; import Slider from '@material-ui/lab/Slider'; -const styles = { +const useStyles = makeStyles({ root: { - display: 'flex', height: 300, }, - slider: { - padding: '0px 22px', - }, -}; +}); -class VerticalSlider extends React.Component { - state = { - value: 50, - }; +function valuetext(value) { + return `${value}°C`; +} - handleChange = (event, value) => { - this.setState({ value }); - }; +const marks = [ + { + value: 0, + label: '0°C', + }, + { + value: 20, + label: '20°C', + }, + { + value: 37, + label: '37°C', + }, + { + value: 100, + label: '100°C', + }, +]; - render() { - const { classes } = this.props; - const { value } = this.state; +export default function VerticalSlider() { + const classes = useStyles(); - return ( + return ( + + + Temperature +
- + + +
- ); - } +
+ ); } - -VerticalSlider.propTypes = { - classes: PropTypes.object.isRequired, -}; - -export default withStyles(styles)(VerticalSlider); diff --git a/docs/src/pages/components/slider/VerticalSlider.tsx b/docs/src/pages/components/slider/VerticalSlider.tsx new file mode 100644 index 00000000000000..79289f5a9eb1d0 --- /dev/null +++ b/docs/src/pages/components/slider/VerticalSlider.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import Slider from '@material-ui/lab/Slider'; + +const useStyles = makeStyles({ + root: { + height: 300, + }, +}); + +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() { + const classes = useStyles(); + + return ( + + + Temperature + +
+ + + +
+
+ ); +} diff --git a/docs/src/pages/components/slider/slider.md b/docs/src/pages/components/slider/slider.md index 5b615c2b5d5691..a7f94b9613c4ad 100644 --- a/docs/src/pages/components/slider/slider.md +++ b/docs/src/pages/components/slider/slider.md @@ -9,43 +9,50 @@ components: Slider [Sliders](https://material.io/design/components/sliders.html) reflect a range of values along a bar, from which users may select a single value. They are ideal for adjusting settings such as volume, brightness, or applying image filters. -Sliders can have icons on both ends of the bar that reflect a range of values. +- 📦 [21 kB gzipped](/size-snapshot) (but only 6 kB without @material-ui/styles). -#### Immediate effects +## Continuous sliders -Changes made with sliders are immediate, allowing a user to make slider adjustments until finding their preference. They shouldn’t be paired with settings that have even minor delays in providing user feedback. +Continuous sliders allow users to select a value along a subjective range. -#### Current state +{{"demo": "pages/components/slider/ContinuousSlider.js"}} -Sliders reflect the current state of the settings they control. +## Discrete sliders -## Simple slider +Discrete sliders can be adjusted to a specific value by referencing its value indicator. +By order of demos: -{{"demo": "pages/components/slider/SimpleSlider.js"}} +1. You can generate a mark for each step with `marks={true}`. +2. You can have custom marks by providing a rich array to the `marks` prop. +3. You can restrict the selectable values to those provided with the `marks` prop with `step={null}`. +4. You can force the thumb label to be always visible with `valueLabelDisplay="on"`. -## Slider with steps +{{"demo": "pages/components/slider/DiscreteSlider.js"}} -{{"demo": "pages/components/slider/StepSlider.js"}} +## Range sliders -## Disabled slider +{{"demo": "pages/components/slider/RangeSlider.js"}} -{{"demo": "pages/components/slider/DisabledSlider.js"}} +## Customized sliders -## Vertical slider +Here are some examples of customizing the component. You can learn more about this in the [overrides documentation page](/customization/components/). -{{"demo": "pages/components/slider/VerticalSlider.js"}} +{{"demo": "pages/components/slider/CustomizedSlider.js"}} -## Customized sliders +## With input field -Here is an example of customizing the component. You can learn more about this in the -[overrides documentation page](/customization/components/). +{{"demo": "pages/components/slider/InputSlider.js"}} -{{"demo": "pages/components/slider/CustomizedSlider.js"}} +## Vertical sliders -## Custom thumb +{{"demo": "pages/components/slider/VerticalSlider.js"}} -{{"demo": "pages/components/slider/CustomIconSlider.js"}} +## Accessibility -## Custom value reducer +The component handles most of the work necessary to make it accessible. +However, you need to make sure that: -{{"demo": "pages/components/slider/CustomValueReducerSlider.js"}} +- 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. +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-lab/src/Slider/Slider.d.ts b/packages/material-ui-lab/src/Slider/Slider.d.ts index fcf46738ce4b29..f2396f33f27d48 100644 --- a/packages/material-ui-lab/src/Slider/Slider.d.ts +++ b/packages/material-ui-lab/src/Slider/Slider.d.ts @@ -1,46 +1,58 @@ import * as React from 'react'; import { StandardProps } from '@material-ui/core'; -/** - * @param rawValue - the value inferred from the event in [min, max] - */ -export type ValueReducer = ( - rawValue: number, - props: SliderProps, - event: React.SyntheticEvent, -) => number; +export interface Mark { + value: number; + label?: React.ReactNode; +} -export const defaultValueReducer: ValueReducer; +export interface ValueLabelProps extends React.HTMLAttributes { + value: number; + open: boolean; + children: React.ReactElement; +} export interface SliderProps - extends StandardProps, SliderClassKey, 'onChange', false> { + extends StandardProps< + React.HTMLAttributes, + SliderClassKey, + 'defaultValue' | 'onChange' + > { + 'aria-label'?: string; + 'aria-labelledby'?: string; + 'aria-valuetext'?: string; + defaultValue?: number | number[]; disabled?: boolean; - vertical?: boolean; + getAriaValueText?: (value: number, index: number) => string; + marks?: boolean | Mark[]; max?: number; min?: number; - step?: number; - value?: number; - valueReducer?: ValueReducer; - thumb?: React.ReactElement; - onChange?: (event: React.ChangeEvent<{}>, value: number) => void; - onDragEnd?: (event: React.ChangeEvent<{}>) => void; - onDragStart?: (event: React.ChangeEvent<{}>) => void; + name?: string; + onChange?: (event: React.ChangeEvent<{}>, value: number | number[]) => void; + onChangeCommitted?: (event: React.ChangeEvent<{}>, value: number | number[]) => void; + orientation?: 'horizontal' | 'vertical'; + step?: number | null; + ThumbComponent?: React.ElementType>; + value?: number | number[]; + ValueLabelComponent?: React.ElementType; + valueLabelDisplay?: 'on' | 'auto' | 'off'; + valueLabelFormat?: string | ((value: number, index: number) => React.ReactNode); } export type SliderClassKey = | 'root' - | 'container' + | 'marked' + | 'vertical' + | 'rtl' + | 'disabled' + | 'rail' | 'track' - | 'trackBefore' - | 'trackAfter' | 'thumb' - | 'thumbIconWrapper' - | 'thumbIcon' - | 'focused' - | 'activated' - | 'disabled' - | 'vertical' - | 'jumped'; + | 'valueLabel' + | 'mark' + | 'markActive' + | 'markLabel' + | 'markLabelActive'; declare const Slider: React.ComponentType; diff --git a/packages/material-ui-lab/src/Slider/Slider.js b/packages/material-ui-lab/src/Slider/Slider.js index 15ae09f3e94233..9fde8f2827da67 100644 --- a/packages/material-ui-lab/src/Slider/Slider.js +++ b/packages/material-ui-lab/src/Slider/Slider.js @@ -1,208 +1,56 @@ +/* eslint-disable no-use-before-define */ + import React from 'react'; -import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import { fade, withStyles } from '@material-ui/core/styles'; -import ButtonBase from '@material-ui/core/ButtonBase'; -import { setRef, withForwardedRef } from '@material-ui/core/utils'; -import { clamp } from '@material-ui/lab/utils'; - -export const styles = theme => { - const commonTransitionsOptions = { - duration: theme.transitions.duration.shortest, - easing: theme.transitions.easing.easeOut, - }; - - const trackTransitions = theme.transitions.create( - ['width', 'height', 'transform'], - commonTransitionsOptions, - ); - const thumbTransitions = theme.transitions.create( - ['transform', 'box-shadow'], - commonTransitionsOptions, - ); - - const colors = { - primary: theme.palette.primary.main, - disabled: theme.palette.grey[400], - thumbOutline: fade(theme.palette.primary.main, 0.16), - }; - - /** - * radius of the box-shadow when pressed - * hover should have a diameter equal to the pressed radius - */ - const pressedOutlineRadius = 9; - - /** - * We need to give some overflow so that the button and tap - * highlight can be shown with overlay hidden. - */ - const overflowSize = 24; - - return { - /* Styles applied to the root element. */ - root: { - position: 'relative', - width: '100%', - cursor: 'pointer', - WebkitTapHighlightColor: 'transparent', - '&$disabled': { - cursor: 'no-drop', - }, - '&$vertical': { - height: '100%', - }, - }, - /* Styles applied to the container element. */ - container: { - width: `calc(100% + ${overflowSize * 2}px)`, - overflow: 'hidden', - padding: overflowSize, - margin: -overflowSize, - boxSizing: 'border-box', - '&$vertical': { - height: `calc(100% + ${overflowSize * 2}px)`, - }, - }, - /* Styles applied to the track elements. */ - track: { - position: 'absolute', - transform: 'translate(0, -50%)', - top: '50%', - width: '100%', - height: 2, - backgroundColor: colors.primary, - transition: trackTransitions, - '&$activated': { - transition: 'none', - }, - '&$disabled': { - backgroundColor: colors.disabled, - boxShadow: 'none', - }, - '&$vertical': { - transform: 'translate(-50%, 0)', - left: '50%', - top: 'initial', - bottom: 0, - width: 2, - height: '100%', - }, - }, - /* Styles applied to the track element before the thumb. */ - trackBefore: { - zIndex: 1, - left: 0, - transformOrigin: 'left bottom', - }, - /* Styles applied to the track element after the thumb. */ - trackAfter: { - right: 0, - opacity: 0.24, - transformOrigin: 'right top', - '&$vertical': { - top: 0, - }, - }, - /* Styles applied to the thumb wrapper element. */ - thumbWrapper: { - position: 'relative', - zIndex: 2, - transition: thumbTransitions, - '&$activated': { - transition: 'none', - }, - '&$vertical': { - bottom: 0, - height: '100%', - }, - }, - /* Styles applied to the thumb element. */ - thumb: { - // Opt out of rtl flip as positioning here only is for centering - flip: false, - position: 'absolute', - left: 0, - transform: 'translate(-50%, -50%)', - width: 12, - height: 12, - borderRadius: '50%', - backgroundColor: colors.primary, - transition: thumbTransitions, - '&$focused, &:hover': { - boxShadow: `0px 0px 0px ${pressedOutlineRadius}px ${colors.thumbOutline}`, - }, - '&$activated': { - boxShadow: `0px 0px 0px ${pressedOutlineRadius * 2}px ${colors.thumbOutline}`, - }, - '&$disabled': { - cursor: 'no-drop', - width: 9, - height: 9, - backgroundColor: colors.disabled, - }, - '&$jumped': { - boxShadow: `0px 0px 0px ${pressedOutlineRadius * 2}px ${colors.thumbOutline}`, - }, - }, - /* Class applied to the thumb element if custom thumb icon provided. */ - thumbIconWrapper: { - backgroundColor: 'transparent', - }, - thumbIcon: { - height: 'inherit', - width: 'inherit', - }, - /* Class applied to the track and thumb elements to trigger JSS nested styles if `disabled`. */ - disabled: {}, - /* Class applied to the track and thumb elements to trigger JSS nested styles if `jumped`. */ - jumped: {}, - /* Class applied to the track and thumb elements to trigger JSS nested styles if `focused`. */ - focused: {}, - /* Class applied to the track and thumb elements to trigger JSS nested styles if `activated`. */ - activated: {}, - /* Class applied to the root, track and container to trigger JSS nested styles if `vertical`. */ - vertical: {}, - }; -}; +import { useTheme, withStyles, fade, lighten } from '@material-ui/core/styles'; +import { useForkRef, ownerWindow, useIsFocusVisible } from '@material-ui/core/utils'; +import { chainPropTypes } from '@material-ui/utils'; +import ValueLabel from './ValueLabel'; -function percentToValue(percent, min, max) { - return ((max - min) * percent) / 100 + min; +function asc(a, b) { + return a - b; } -function roundToStep(number, step) { - return Math.round(number / step) * step; +function clamp(value, min, max) { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; } -function getOffset(node) { - const { pageYOffset, pageXOffset } = global; - const { left, bottom } = node.getBoundingClientRect(); +function findClosest(values, currentValue) { + const { index: closestIndex } = values.reduce((acc, value, index) => { + const distance = Math.abs(currentValue - value); - return { - bottom: bottom + pageYOffset, - left: left + pageXOffset, - }; + if (acc === null || distance < acc.distance || distance === acc.distance) { + return { + distance, + index, + }; + } + + return acc; + }, null); + return closestIndex; } -function getMousePosition(event, touchId) { - if (event.changedTouches) { - // event.changedTouches.findIndex(touch => touch.identifier === touchId) - let touchIndex = 0; +function trackFinger(event, touchId) { + if (touchId.current !== undefined && event.changedTouches) { for (let i = 0; i < event.changedTouches.length; i += 1) { const touch = event.changedTouches[i]; - if (touch.identifier === touchId) { - touchIndex = i; - break; + if (touch.identifier === touchId.current) { + return { + x: event.changedTouches[i].pageX, + y: event.changedTouches[i].pageY, + }; } } - if (event.changedTouches[touchIndex]) { - return { - x: event.changedTouches[touchIndex].pageX, - y: event.changedTouches[touchIndex].pageY, - }; - } + return false; } return { @@ -211,95 +59,337 @@ function getMousePosition(event, touchId) { }; } -function calculatePercent(node, event, isVertical, isRtl, touchId) { - const { width, height } = node.getBoundingClientRect(); - const { bottom, left } = getOffset(node); - const { x, y } = getMousePosition(event, touchId); - - const value = isVertical ? bottom - y : x - left; - const onePercent = (isVertical ? height : width) / 100; - - return isRtl && !isVertical ? 100 - clamp(value / onePercent) : clamp(value / onePercent); +function valueToPercent(value, min, max) { + return ((value - min) * 100) / (max - min); } -function preventPageScrolling(event) { - event.preventDefault(); +function percentToValue(percent, min, max) { + return (max - min) * percent + min; } -/** - * @param {number} rawValue - * @param {object} props - */ -export function defaultValueReducer(rawValue, props) { - const { disabled, step } = props; +function roundValueToStep(value, step) { + return Math.round(value / step) * step; +} - if (disabled) { - return null; +function setValueIndex({ values, source, newValue, index }) { + // Performance shortcut + if (values[index] === newValue) { + return source; } - if (step) { - return roundToStep(rawValue, step); + const output = [...values]; + output[index] = newValue; + return output; +} + +function focusThumb({ sliderRef, activeIndex, setActive }) { + if ( + !sliderRef.current.contains(document.activeElement) || + Number(document.activeElement.getAttribute('data-index')) !== activeIndex + ) { + sliderRef.current.querySelector(`[data-index="${activeIndex}"]`).focus(); } - return Number(rawValue.toFixed(3)); + if (setActive) { + setActive(activeIndex); + } } -class Slider extends React.Component { - state = { - currentState: 'initial', - }; +const axisProps = { + horizontal: { + offset: value => ({ left: `${value}%` }), + leap: value => ({ width: `${value}%` }), + }, + 'horizontal-reverse': { + offset: value => ({ right: `${value}%` }), + leap: value => ({ width: `${value}%` }), + }, + vertical: { + offset: value => ({ bottom: `${value}%` }), + leap: value => ({ height: `${value}%` }), + }, + 'vertical-reverse': { + offset: value => ({ top: `${value}%` }), + leap: value => ({ height: `${value}%` }), + }, +}; - jumpAnimationTimeoutId = -1; +const defaultMarks = []; +const Identity = x => x; - touchId = undefined; +const useEnhancedEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; +/** + * https://github.com/facebook/react/issues/14099#issuecomment-440013892 + * + * @param {function} fn + */ +function useEventCallback(fn) { + const ref = React.useRef(fn); + useEnhancedEffect(() => { + ref.current = fn; + }); + return React.useCallback(event => (0, ref.current)(event), []); +} - componentWillUnmount() { - document.body.removeEventListener('mouseenter', this.handleMouseEnter); - document.body.removeEventListener('mouseleave', this.handleMouseLeave); - document.body.removeEventListener('mousemove', this.handleMouseMove); - document.body.removeEventListener('mouseup', this.handleMouseUp); - clearTimeout(this.jumpAnimationTimeoutId); - } +export const styles = theme => ({ + /* Styles applied to the root element. */ + root: { + height: 2, + width: '100%', + boxSizing: 'content-box', + padding: '11px 0', + display: 'inline-block', + position: 'relative', + cursor: 'pointer', + touchAction: 'none', + color: theme.palette.primary.main, + // Remove grey highlight + WebkitTapHighlightColor: 'transparent', + '&$disabled': { + cursor: 'default', + color: theme.palette.grey[400], + }, + '&$vertical': { + width: 2, + height: '100%', + padding: '0 11px', + }, + }, + /* Styles applied to the root element if `marks` is provided with at least one label. */ + marked: { + marginBottom: 20, + '&$vertical': { + marginBottom: 'auto', + marginRight: 20, + }, + }, + /* Pseudo-class applied to the root element if `orientation="vertical"`. */ + vertical: {}, + /* Pseudo-class applied to the root element if `disabled={true}`. */ + disabled: {}, + /* Styles applied to the rail element. */ + rail: { + position: 'absolute', + width: '100%', + height: 2, + borderRadius: 1, + backgroundColor: 'currentColor', + opacity: 0.38, + '$vertical &': { + height: '100%', + width: 2, + }, + }, + /* Styles applied to the track element. */ + track: { + position: 'absolute', + height: 2, + borderRadius: 1, + backgroundColor: 'currentColor', + '$vertical &': { + width: 2, + }, + }, + /* Styles applied to the thumb element. */ + thumb: { + position: 'absolute', + width: 12, + height: 12, + marginLeft: -6, + marginTop: -5, + boxSizing: 'border-box', + borderRadius: '50%', + outline: 'none', + backgroundColor: 'currentColor', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + transition: theme.transitions.create(['box-shadow'], { + duration: theme.transitions.duration.shortest, + }), + '&$focusVisible,&:hover': { + boxShadow: `0px 0px 0px 8px ${fade(theme.palette.primary.main, 0.16)}`, + '@media (hover: none)': { + boxShadow: 'none', + }, + }, + '&$active': { + boxShadow: `0px 0px 0px 14px ${fade(theme.palette.primary.main, 0.16)}`, + }, + '$disabled &': { + pointerEvents: 'none', + width: 8, + height: 8, + marginLeft: -4, + marginTop: -3, + '&:hover': { + boxShadow: 'none', + }, + }, + '$vertical &': { + marginLeft: -5, + marginBottom: -6, + }, + '$vertical$disabled &': { + marginLeft: -3, + marginBottom: -4, + }, + }, + /* Pseudo-class applied to the thumb element if it's active. */ + active: {}, + /* Pseudo-class applied to the thumb element if keyboard focused. */ + focusVisible: {}, + /* Styles applied to the thumb label element. */ + valueLabel: {}, + /* Styles applied to the mark element. */ + mark: { + position: 'absolute', + width: 2, + height: 2, + borderRadius: 1, + backgroundColor: 'currentColor', + }, + /* Styles applied to the mark element if active (depending on the value). */ + markActive: { + backgroundColor: lighten(theme.palette.primary.main, 0.76), + }, + /* Styles applied to the mark label element. */ + markLabel: { + ...theme.typography.body2, + color: theme.palette.text.secondary, + position: 'absolute', + top: 22, + transform: 'translateX(-50%)', + whiteSpace: 'nowrap', + '$vertical &': { + top: 'auto', + left: 22, + transform: 'translateY(50%)', + }, + }, + /* Styles applied to the mark label element if active (depending on the value). */ + markLabelActive: { + color: theme.palette.text.primary, + }, +}); + +const Slider = React.forwardRef(function Slider(props, ref) { + const { + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledby, + 'aria-valuetext': ariaValuetext, + classes, + className, + component: Component = 'span', + defaultValue, + disabled = false, + getAriaValueText, + marks: marksProp = defaultMarks, + max = 100, + min = 0, + name, + onChange, + onChangeCommitted, + onMouseDown, + orientation = 'horizontal', + step = 1, + ThumbComponent = 'span', + value: valueProp, + ValueLabelComponent = ValueLabel, + valueLabelDisplay = 'off', + valueLabelFormat = Identity, + ...other + } = props; + const theme = useTheme(); + const { current: isControlled } = React.useRef(valueProp != null); + const touchId = React.useRef(); + // We can't use the :active browser pseudo-classes. + // - The active state isn't triggered when clicking on the rail. + // - The active state isn't transfered when inversing a range slider. + const [active, setActive] = React.useState(-1); + const [open, setOpen] = React.useState(-1); + const [valueState, setValueState] = React.useState(defaultValue); + const valueDerived = isControlled ? valueProp : valueState; + const range = Array.isArray(valueDerived); + const instanceRef = React.useRef(); + let values = range ? valueDerived.sort(asc) : [valueDerived]; + values = values.map(value => clamp(value, min, max)); + const marks = + marksProp === true && step !== null + ? [...Array(Math.floor(max / step) + 1)].map((_, index) => ({ + value: step * index, + })) + : marksProp; + + instanceRef.current = { + source: valueDerived, // Keep track of the input value to leverage immutable state comparison. + }; - static getDerivedStateFromProps(nextProps, prevState) { - if (nextProps.disabled) { - return { currentState: 'disabled' }; - } + const { isFocusVisible, onBlurVisible, ref: focusVisibleRef } = useIsFocusVisible(); + const [focusVisible, setFocusVisible] = React.useState(-1); - if (!nextProps.disabled && prevState.currentState === 'disabled') { - return { currentState: 'normal' }; + const handleFocus = useEventCallback(event => { + const index = Number(event.currentTarget.getAttribute('data-index')); + if (isFocusVisible(event)) { + setFocusVisible(index); } - - return null; - } - - handleKeyDown = event => { - const { min, max, value: currentValue } = this.props; - - const onePercent = Math.abs((max - min) / 100); - const step = this.props.step || onePercent; - let value; + setOpen(index); + }); + const handleBlur = useEventCallback(() => { + if (focusVisible !== -1) { + setFocusVisible(-1); + onBlurVisible(); + } + setOpen(-1); + }); + const handleMouseOver = useEventCallback(event => { + const index = Number(event.currentTarget.getAttribute('data-index')); + setOpen(index); + }); + const handleMouseLeave = useEventCallback(() => { + setOpen(-1); + }); + + const handleKeyDown = 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; switch (event.key) { case 'Home': - value = min; + newValue = min; break; case 'End': - value = max; + newValue = max; break; case 'PageUp': - value = currentValue + onePercent * 10; + if (step) { + newValue = value + tenPercents; + } break; case 'PageDown': - value = currentValue - onePercent * 10; + if (step) { + newValue = value - tenPercents; + } break; case 'ArrowRight': case 'ArrowUp': - value = currentValue + step; + if (step) { + newValue = value + step; + } else { + newValue = marksValues[marksIndex + 1] || marksValues[marksValues.length - 1]; + } break; case 'ArrowLeft': case 'ArrowDown': - value = currentValue - step; + if (step) { + newValue = value - step; + } else { + newValue = marksValues[marksIndex - 1] || marksValues[0]; + } break; default: return; @@ -307,330 +397,340 @@ class Slider extends React.Component { event.preventDefault(); - value = clamp(value, min, max); - - this.emitChange(event, value); - }; - - handleFocus = () => { - this.setState({ currentState: 'focused' }); - }; - - handleBlur = () => { - this.setState({ currentState: 'normal' }); - }; - - handleClick = event => { - const value = this.calculateValueFromPercent(event); - - this.emitChange(event, value, () => { - this.playJumpAnimation(); - }); - }; - - handleMouseEnter = event => { - // If the slider was being interacted with but the mouse went off the window - // and then re-entered while unclicked then end the interaction. - if (event.buttons === 0) { - this.handleDragEnd(event); + if (step) { + newValue = roundValueToStep(newValue, step); } - }; - handleMouseLeave = event => { - // The mouse will have moved between the last mouse move event - // this mouse leave event - this.handleMouseMove(event); - }; - - handleTouchStart = event => { - event.preventDefault(); - const touch = event.changedTouches.item(0); - if (touch != null) { - this.touchId = touch.identifier; + newValue = clamp(newValue, min, max); + + if (range) { + const previousValue = newValue; + newValue = setValueIndex({ + values, + source: valueDerived, + newValue, + index, + }).sort(asc); + focusThumb({ sliderRef, activeIndex: newValue.indexOf(previousValue) }); } - this.setState({ currentState: 'activated' }); - - const { onDragStart, valueReducer } = this.props; - - const value = this.calculateValueFromPercent(event); - const newValue = valueReducer(value, this.props, event); - this.emitChange(event, value); - document.body.addEventListener('touchend', this.handleTouchEnd); - - if (typeof onDragStart === 'function') { - onDragStart(event, newValue); + if (!isControlled) { + setValueState(newValue); } - }; - - handleMouseDown = event => { - this.setState({ currentState: 'activated' }); - const { onDragStart, valueReducer } = this.props; + setFocusVisible(index); - const value = this.calculateValueFromPercent(event); - const newValue = valueReducer(value, this.props, event); - - document.body.addEventListener('mouseenter', this.handleMouseEnter); - document.body.addEventListener('mouseleave', this.handleMouseLeave); - document.body.addEventListener('mousemove', this.handleMouseMove); - document.body.addEventListener('mouseup', this.handleMouseUp); - - if (typeof onDragStart === 'function') { - onDragStart(event, newValue); + if (onChange) { + onChange(event, newValue); } - }; - - handleTouchEnd = event => { - if (this.touchId === undefined) { - this.handleMouseUp(event); + if (onChangeCommitted) { + onChangeCommitted(event, newValue); } + }); + + const sliderRef = React.useRef(); + const handleFocusRef = useForkRef(focusVisibleRef, sliderRef); + const handleRef = useForkRef(ref, handleFocusRef); + const previousIndex = React.useRef(); + let axis = orientation; + if (theme.direction === 'rtl' && orientation === 'horizontal') { + axis += '-reverse'; + } - for (let i = 0; i < event.changedTouches.length; i += 1) { - const touch = event.changedTouches.item(i); - if (touch.identifier === this.touchId) { - this.handleMouseUp(event); - break; + const getNewValue = React.useCallback( + ({ finger, move = false, values: values2, source }) => { + const { current: slider } = sliderRef; + const { width, height, bottom, left } = slider.getBoundingClientRect(); + let percent; + + if (axis.indexOf('vertical') === 0) { + percent = (bottom + ownerWindow(slider).pageYOffset - finger.y) / height; + } else { + percent = (finger.x - left - ownerWindow(slider).pageXOffset) / width; } - } - }; - handleMouseUp = event => { - this.handleDragEnd(event); - }; + if (axis.indexOf('-reverse') !== -1) { + percent = 1 - percent; + } - handleTouchMove = event => { - if (this.touchId === undefined) { - this.handleMouseMove(event); - } + let newValue; + newValue = percentToValue(percent, min, max); + if (step) { + newValue = roundValueToStep(newValue, step); + } else { + const marksValues = marks.map(mark => mark.value); + const closestIndex = findClosest(marksValues, newValue); + newValue = marksValues[closestIndex]; + } - for (let i = 0; i < event.changedTouches.length; i += 1) { - const touch = event.changedTouches.item(i); - if (touch.identifier === this.touchId) { - this.handleMouseMove(event); - break; + newValue = clamp(newValue, min, max); + let activeIndex = 0; + + if (range) { + if (!move) { + activeIndex = findClosest(values2, newValue); + } else { + activeIndex = previousIndex.current; + } + + const previousValue = newValue; + newValue = setValueIndex({ + values: values2, + source, + newValue, + index: activeIndex, + }).sort(asc); + activeIndex = newValue.indexOf(previousValue); + previousIndex.current = activeIndex; } - } - }; - handleMouseMove = event => { - const value = this.calculateValueFromPercent(event); + return { newValue, activeIndex }; + }, + [max, min, axis, range, step, marks], + ); - this.emitChange(event, value); - }; + const handleTouchMove = useEventCallback(event => { + const finger = trackFinger(event, touchId); - handleRef = ref => { - setRef(this.props.innerRef, ref); + if (!finger) { + return; + } - // #StrictMode ready - const nextContainer = ReactDOM.findDOMNode(ref); - const prevContainer = this.container; + const { newValue, activeIndex } = getNewValue({ + finger, + move: true, + values, + source: valueDerived, + }); - if (prevContainer !== nextContainer) { - if (prevContainer) { - prevContainer.removeEventListener('touchstart', preventPageScrolling, { - passive: false, - }); - } - if (nextContainer) { - nextContainer.addEventListener('touchstart', preventPageScrolling, { passive: false }); - } + focusThumb({ sliderRef, activeIndex, setActive }); + if (!isControlled) { + setValueState(newValue); + } + if (onChange) { + onChange(event, newValue); } + }); - this.container = nextContainer; - }; + const handleTouchEnd = useEventCallback(event => { + const finger = trackFinger(event, touchId); - handleDragEnd(event) { - const { onDragEnd, valueReducer } = this.props; + if (!finger) { + return; + } - const value = this.calculateValueFromPercent(event); - const newValue = valueReducer(value, this.props, event); + const { newValue } = getNewValue({ finger, values, source: valueDerived }); - this.setState({ currentState: 'normal' }); + setActive(-1); + if (event.type === 'touchend') { + setOpen(-1); + } - document.body.removeEventListener('mouseenter', this.handleMouseEnter); - document.body.removeEventListener('mouseleave', this.handleMouseLeave); - document.body.removeEventListener('mousemove', this.handleMouseMove); - document.body.removeEventListener('mouseup', this.handleMouseUp); - document.body.removeEventListener('touchend', this.handleTouchEnd); + if (onChangeCommitted) { + onChangeCommitted(event, newValue); + } - if (typeof onDragEnd === 'function') { - onDragEnd(event, newValue); + touchId.current = undefined; + document.body.removeEventListener('mousemove', handleTouchMove); + document.body.removeEventListener('mouseup', handleTouchEnd); + document.body.removeEventListener('touchmove', handleTouchMove); + document.body.removeEventListener('touchend', handleTouchEnd); + }); + + const handleMouseEnter = useEventCallback(event => { + // If the slider was being interacted with but the mouse went off the window + // and then re-entered while unclicked then end the interaction. + if (event.buttons === 0) { + handleTouchEnd(event); } - } + }); - emitChange(event, rawValue, callback) { - const { onChange, value: previousValue, valueReducer } = this.props; - const newValue = valueReducer(rawValue, this.props, event); + const handleTouchStart = useEventCallback(event => { + // Workaround as Safari has partial support for touchAction: 'none'. + event.preventDefault(); + const touch = event.changedTouches.item(0); + if (touch != null) { + // A number that uniquely identifies the current finger in the touch session. + touchId.current = touch.identifier; + } + const finger = trackFinger(event, touchId); + const { newValue, activeIndex } = getNewValue({ finger, values, source: valueDerived }); + focusThumb({ sliderRef, activeIndex, setActive }); - if (newValue !== null && newValue !== previousValue && typeof onChange === 'function') { + if (!isControlled) { + setValueState(newValue); + } + if (onChange) { onChange(event, newValue); - - if (typeof callback === 'function') { - callback(); - } } - } - calculateTrackPartStyles(percent) { - const { theme, vertical } = this.props; - const { currentState } = this.state; + document.body.addEventListener('touchmove', handleTouchMove); + document.body.addEventListener('touchend', handleTouchEnd); + }); - switch (currentState) { - case 'disabled': - return { - [vertical ? 'height' : 'width']: `calc(${percent}% - 6px)`, - }; - default: - return { - transform: `${ - vertical - ? `translateX(${theme.direction === 'rtl' ? '' : '-'}50%) scaleY` - : 'translateY(-50%) scaleX' - }(${percent / 100})`, - }; + React.useEffect(() => { + if (disabled) { + return () => {}; } - } - calculateValueFromPercent(event) { - const { min, max, vertical } = this.props; - const percent = calculatePercent( - this.container, - event, - vertical, - this.isReverted(), - this.touchId, - ); - return percentToValue(percent, min, max); - } + const { current: slider } = sliderRef; + slider.addEventListener('touchstart', handleTouchStart); - playJumpAnimation() { - this.setState({ currentState: 'jumped' }, () => { - clearTimeout(this.jumpAnimationTimeoutId); - this.jumpAnimationTimeoutId = setTimeout(() => { - this.setState({ currentState: 'normal' }); - }, this.props.theme.transitions.duration.complex); - }); - } + return () => { + slider.removeEventListener('touchstart', handleTouchStart); + document.body.removeEventListener('mousemove', handleTouchMove); + document.body.removeEventListener('mouseup', handleTouchEnd); + document.body.removeEventListener('mouseenter', handleMouseEnter); + document.body.removeEventListener('touchmove', handleTouchMove); + document.body.removeEventListener('touchend', handleTouchEnd); + }; + }, [disabled, handleMouseEnter, handleTouchEnd, handleTouchMove, handleTouchStart]); - isReverted() { - return this.props.theme.direction === 'rtl'; - } + const handleMouseDown = useEventCallback(event => { + if (onMouseDown) { + onMouseDown(event); + } - render() { - const { currentState } = this.state; - const { - className: classNameProp, - classes, - component: Component, - thumb: thumbIcon, - disabled, - innerRef, - max, - min, - onChange, - onDragEnd, - onDragStart, - step, - theme, - value, - valueReducer, - vertical, - ...other - } = this.props; - - const percent = clamp(((value - min) * 100) / (max - min)); - - const commonClasses = clsx({ - [classes.disabled]: disabled, - [classes.jumped]: !disabled && currentState === 'jumped', - [classes.focused]: !disabled && currentState === 'focused', - [classes.activated]: !disabled && currentState === 'activated', - [classes.vertical]: vertical, - [classes.rtl]: theme.direction === 'rtl', - }); + if (disabled) { + return; + } - const className = clsx( - classes.root, - { - [classes.vertical]: vertical, - [classes.disabled]: disabled, - }, - classNameProp, - ); + event.preventDefault(); + const finger = trackFinger(event, touchId); + const { newValue, activeIndex } = getNewValue({ finger, values, source: valueDerived }); + focusThumb({ sliderRef, activeIndex, setActive }); - const containerClasses = clsx(classes.container, { - [classes.vertical]: vertical, - }); + if (!isControlled) { + setValueState(newValue); + } + if (onChange) { + onChange(event, newValue); + } - const trackBeforeClasses = clsx(classes.track, classes.trackBefore, commonClasses); - const trackAfterClasses = clsx(classes.track, classes.trackAfter, commonClasses); + document.body.addEventListener('mousemove', handleTouchMove); + document.body.addEventListener('mouseenter', handleMouseEnter); + document.body.addEventListener('mouseup', handleTouchEnd); + }); - const thumbTransformFunction = vertical ? 'translateY' : 'translateX'; - const thumbDirectionInverted = vertical || theme.direction === 'rtl'; - const inlineTrackBeforeStyles = this.calculateTrackPartStyles(percent); - const inlineTrackAfterStyles = this.calculateTrackPartStyles(100 - percent); - const inlineThumbStyles = { - transform: `${thumbTransformFunction}(${thumbDirectionInverted ? 100 - percent : percent}%)`, - }; + const offset = range ? valueToPercent(values[0], min, max) : 0; + const leap = valueToPercent(values[values.length - 1], min, max) - offset; + const trackStyle = { + ...axisProps[axis].offset(offset), + ...axisProps[axis].leap(leap), + }; - /** Start Thumb Icon Logic Here */ - const ThumbIcon = thumbIcon - ? React.cloneElement(thumbIcon, { - ...thumbIcon.props, - className: clsx(thumbIcon.props.className, classes.thumbIcon), - }) - : null; - /** End Thumb Icon Logic Here */ - - const thumbWrapperClasses = clsx(classes.thumbWrapper, commonClasses); - const thumbClasses = clsx( - classes.thumb, - { - [classes.thumbIconWrapper]: thumbIcon, - }, - commonClasses, - ); - - return ( - -
-
-
- 0 && marks.some(mark => mark.label), + [classes.vertical]: orientation === 'vertical', + }, + className, + )} + onMouseDown={handleMouseDown} + {...other} + > + + + + {marks.map(mark => { + const percent = valueToPercent(mark.value, min, max); + const style = axisProps[axis].offset(percent); + const markActive = range + ? percent >= values[0] && percent <= values[values.length - 1] + : percent <= values[0]; + + return ( + + + - {ThumbIcon} - -
-
-
- - ); - } -} + {mark.label} + + + ); + })} + {values.map((value, index) => { + const percent = valueToPercent(value, min, max); + const style = axisProps[axis].offset(percent); + + return ( + + + + ); + })} + + ); +}); Slider.propTypes = { + /** + * The label of the slider. + */ + 'aria-label': PropTypes.string, + /** + * The id of the element containing a label for the slider. + */ + 'aria-labelledby': PropTypes.string, + /** + * A string value that provides a user-friendly name for the current value of the slider. + */ + 'aria-valuetext': chainPropTypes(PropTypes.string, props => { + const range = Array.isArray(props.value || props.defaultValue); + + if (range && props['aria-valuetext']) { + return new Error( + 'Material-UI: you need to use the `getAriaValueText` prop instead of `aria-valuetext` when using a range input.', + ); + } + + return null; + }), + /** * Override or extend the styles applied to the component. * See [CSS API](#css) below for more details. @@ -645,15 +745,27 @@ Slider.propTypes = { * Either a string to use a DOM element or a component. */ component: PropTypes.elementType, + /** + * The default element value. Use when the component is not controlled. + */ + defaultValue: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]), /** * If `true`, the slider will be disabled. */ disabled: PropTypes.bool, /** - * @ignore - * from `withForwardRef` + * 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 + */ + getAriaValueText: PropTypes.func, + /** + * 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. */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + marks: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]), /** * The maximum allowed value of the slider. * Should not be equal to min. @@ -664,54 +776,67 @@ Slider.propTypes = { * Should not be equal to max. */ min: PropTypes.number, + /** + * Name attribute of the hidden `input` element. + */ + name: PropTypes.string, /** * Callback function that is fired when the slider's value changed. + * + * @param {object} event The event source of the callback + * @param {any} value The new value */ onChange: PropTypes.func, /** - * Callback function that is fired when the slide has stopped moving. + * Callback function that is fired when the `mouseup` is triggered. + * + * @param {object} event The event source of the callback + * @param {any} value The new value + */ + onChangeCommitted: PropTypes.func, + /** + * @ignore */ - onDragEnd: PropTypes.func, + onMouseDown: PropTypes.func, /** - * Callback function that is fired when the slider has begun to move. + * The slider orientation. */ - onDragStart: PropTypes.func, + orientation: PropTypes.oneOf(['horizontal', 'vertical']), /** - * The granularity the slider can step through values. + * The granularity with which the slider can step through values. (A "discrete" slider.) + * When step is `null`, the thumb can only be slid onto marks provided with the `marks` prop. */ step: PropTypes.number, /** - * @ignore + * The component used to display the value label. */ - theme: PropTypes.object.isRequired, + ThumbComponent: PropTypes.elementType, /** - * The component used for the slider icon. - * This is optional, if provided should be a react element. + * The value of the slider. + * For ranged sliders, provide an array with two values. */ - thumb: PropTypes.element, + value: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]), /** - * The value of the slider. + * The value label componnet. */ - value: PropTypes.number.isRequired, + ValueLabelComponent: PropTypes.elementType, /** - * the reducer used to process the value emitted from the slider. If `null` or - * the same value is returned no change is emitted. - * @param {number} rawValue - value in [min, max] - * @param {SliderProps} props - current props of the Slider - * @param {Event} event - the event the change was triggered from + * Controls when the value label is displayed: + * + * - `auto` the value label will display when the thumb is hovered or focused. + * - `on` will display persistently. + * - `off` will never display. */ - valueReducer: PropTypes.func, + valueLabelDisplay: PropTypes.oneOf(['on', 'auto', 'off']), /** - * If `true`, the slider will be vertical. + * The format function the value label's value. + * + * When a function is provided, it should have the following signature: + * + * - {number} value The value label's value to format + * - {number} index The value label's index to format */ - vertical: PropTypes.bool, -}; - -Slider.defaultProps = { - min: 0, - max: 100, - component: 'div', - valueReducer: defaultValueReducer, + valueLabelFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), }; -export default withStyles(styles, { name: 'MuiSlider', withTheme: true })(withForwardedRef(Slider)); +export default withStyles(styles, { name: 'MuiSlider' })(Slider); diff --git a/packages/material-ui-lab/src/Slider/Slider.spec.tsx b/packages/material-ui-lab/src/Slider/Slider.spec.tsx deleted file mode 100644 index a3f31ccb231c2a..00000000000000 --- a/packages/material-ui-lab/src/Slider/Slider.spec.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import Slider, { defaultValueReducer, ValueReducer } from '@material-ui/lab/Slider'; -import * as React from 'react'; - -{ - // round to 1 digit instead of 3 - const valueReducer: ValueReducer = (rawValue, props, event) => { - const { disabled, step } = props; - if (!disabled && !step) { - return +rawValue.toFixed(1); - } - return defaultValueReducer(rawValue, props, event); - }; - - ; -} diff --git a/packages/material-ui-lab/src/Slider/Slider.test.js b/packages/material-ui-lab/src/Slider/Slider.test.js index da0f08df540013..75b9ae43d9c7bc 100644 --- a/packages/material-ui-lab/src/Slider/Slider.test.js +++ b/packages/material-ui-lab/src/Slider/Slider.test.js @@ -7,20 +7,30 @@ import { findOutermostIntrinsic, wrapsIntrinsicElement, } from '@material-ui/core/test-utils'; -import Slider, { defaultValueReducer } from './Slider'; +import describeConformance from '@material-ui/core/test-utils/describeConformance'; +import Slider from './Slider'; function touchList(touchArray) { touchArray.item = idx => touchArray[idx]; return touchArray; } +function fireBodyMouseEvent(name, properties = {}) { + const event = document.createEvent('MouseEvents'); + event.initEvent(name, true, true); + Object.keys(properties).forEach(key => { + event[key] = properties[key]; + }); + document.body.dispatchEvent(event); + return event; +} + describe('', () => { let mount; let classes; before(() => { classes = getClasses(); - // StrictMode violation: uses ButtonBase mount = createMount({ strict: false }); }); @@ -28,23 +38,21 @@ describe('', () => { mount.cleanUp(); }); - function findHandle(wrapper) { + describeConformance(, () => ({ + classes, + inheritComponent: 'span', + mount, + refInstanceof: window.HTMLSpanElement, + testComponentPropWith: 'span', + })); + + function findThumb(wrapper) { // Will also match any other react component if not filtered. They won't appear in the DOM // and are therefore an implementation detail. We're interested in what the user // interacts with. return wrapper.find('[role="slider"]').filterWhere(wrapsIntrinsicElement); } - it('should render a div', () => { - const wrapper = mount(); - assert.strictEqual(findOutermostIntrinsic(wrapper).type(), 'div'); - }); - - it('should render with the default classes', () => { - const wrapper = mount(); - assert.strictEqual(findOutermostIntrinsic(wrapper).hasClass(classes.root), true); - }); - it('should render with the default and user classes', () => { const wrapper = mount(); assert.strictEqual( @@ -58,16 +66,10 @@ describe('', () => { it('should call handlers', () => { const handleChange = spy(); - const handleDragStart = spy(); - const handleDragEnd = spy(); + const handleChangeCommitted = spy(); const wrapper = mount( - , + , ); wrapper.simulate('click'); @@ -75,66 +77,46 @@ describe('', () => { // document.simulate('mouseup') document.body.dispatchEvent(new window.MouseEvent('mouseup')); - assert.strictEqual(handleChange.callCount, 1, 'should have called the handleChange cb'); - assert.strictEqual(handleDragStart.callCount, 1, 'should have called the handleDragStart cb'); - assert.strictEqual(handleDragEnd.callCount, 1, 'should have called the handleDragEnd cb'); + assert.strictEqual(handleChange.callCount, 1); + assert.strictEqual(handleChangeCommitted.callCount, 1); - assert.strictEqual( - handleChange.args[0].length, - 2, - 'should have called the handleDragEnd cb with 2 arguments', - ); - assert.strictEqual( - handleDragStart.args[0].length, - 2, - 'should have called the handleDragEnd cb with 2 argument', - ); - assert.strictEqual( - handleDragEnd.args[0].length, - 2, - 'should have called the handleDragEnd cb with 2 arguments', - ); + assert.strictEqual(handleChange.args[0].length, 2); + assert.strictEqual(handleChangeCommitted.args[0].length, 2); }); it('should only listen to changes from the same touchpoint', () => { const handleChange = spy(); - const handleDragStart = spy(); - const handleDragEnd = spy(); - let touchEvent; - + const handleChangeCommitted = spy(); + const touches = [{ pageX: 0, pageY: 0 }]; const wrapper = mount( - , + , ); - wrapper.simulate('touchstart', { + const event = fireBodyMouseEvent('touchstart', { changedTouches: touchList([{ identifier: 1 }]), + touches, }); - wrapper.simulate('touchmove', { + wrapper.getDOMNode().dispatchEvent(event); + assert.strictEqual(handleChange.callCount, 1); + assert.strictEqual(handleChangeCommitted.callCount, 0); + fireBodyMouseEvent('touchend', { changedTouches: touchList([{ identifier: 2 }]), + touches, }); - touchEvent = new window.MouseEvent('touchend'); - touchEvent.changedTouches = touchList([{ identifier: 2 }]); - document.body.dispatchEvent(touchEvent); - - assert.strictEqual(handleChange.callCount, 1, 'should have called the handleChange cb'); - assert.strictEqual(handleDragStart.callCount, 1, 'should have called the handleDragStart cb'); - assert.strictEqual(handleDragEnd.callCount, 0, 'should not have called the handleDragEnd cb'); - - wrapper.simulate('touchmove', { + assert.strictEqual(handleChange.callCount, 1); + assert.strictEqual(handleChangeCommitted.callCount, 0); + fireBodyMouseEvent('touchmove', { changedTouches: touchList([{ identifier: 1 }]), + touches, }); - touchEvent = new window.MouseEvent('touchend'); - touchEvent.changedTouches = touchList([{ identifier: 1 }]); - document.body.dispatchEvent(touchEvent); - - assert.strictEqual(handleChange.callCount, 2, 'should have called the handleChange cb'); - assert.strictEqual(handleDragStart.callCount, 1, 'should have called the handleDragStart cb'); - assert.strictEqual(handleDragEnd.callCount, 1, 'should have called the handleDragEnd cb'); + assert.strictEqual(handleChange.callCount, 2); + assert.strictEqual(handleChangeCommitted.callCount, 0); + fireBodyMouseEvent('touchend', { + changedTouches: touchList([{ identifier: 1 }]), + touches, + }); + assert.strictEqual(handleChange.callCount, 2); + assert.strictEqual(handleChangeCommitted.callCount, 1); }); describe('when mouse leaves window', () => { @@ -146,7 +128,7 @@ describe('', () => { wrapper.simulate('mousedown'); document.body.dispatchEvent(new window.MouseEvent('mouseleave')); - assert.strictEqual(handleChange.callCount, 1, 'should have called the handleChange cb'); + assert.strictEqual(handleChange.callCount, 1); }); }); @@ -164,7 +146,7 @@ describe('', () => { document.body.dispatchEvent(mouseEnter); document.body.dispatchEvent(new window.MouseEvent('mousemove')); - assert.strictEqual(handleChange.callCount, 2, 'should have called the handleChange cb'); + assert.strictEqual(handleChange.callCount, 2); }); it('should not update if mouse is not clicked', () => { @@ -180,16 +162,18 @@ describe('', () => { document.body.dispatchEvent(mouseEnter); document.body.dispatchEvent(new window.MouseEvent('mousemove')); - assert.strictEqual(handleChange.callCount, 1, 'should have called the handleChange cb'); + assert.strictEqual(handleChange.callCount, 1); }); }); describe('unmount', () => { it('should not have global event listeners registered after unmount', () => { const handleChange = spy(); - const handleDragEnd = spy(); + const handleChangeCommitted = spy(); - const wrapper = mount(); + const wrapper = mount( + , + ); const callGlobalListeners = () => { document.body.dispatchEvent(new window.MouseEvent('mousemove')); @@ -199,24 +183,22 @@ describe('', () => { wrapper.simulate('mousedown'); callGlobalListeners(); // pre condition: the dispatched event actually did something when mounted - assert.strictEqual(handleChange.callCount, 1); - assert.strictEqual(handleDragEnd.callCount, 1); - + assert.strictEqual(handleChange.callCount, 2); + assert.strictEqual(handleChangeCommitted.callCount, 1); wrapper.unmount(); - // After unmounting global listeners should not be registered anymore since that would // break component encapsulation. If they are still mounted either react will throw warnings // or other component logic throws. // post condition: the dispatched events dont cause errors/warnings callGlobalListeners(); - assert.strictEqual(handleChange.callCount, 1); - assert.strictEqual(handleDragEnd.callCount, 1); + assert.strictEqual(handleChange.callCount, 2); + assert.strictEqual(handleChangeCommitted.callCount, 1); }); }); - describe('prop: vertical', () => { + describe('prop: orientation', () => { it('should render with the default and vertical classes', () => { - const wrapper = mount(); + const wrapper = mount(); assert.strictEqual( wrapper .find(`.${classes.root}`) @@ -228,119 +210,77 @@ describe('', () => { }); describe('prop: disabled', () => { - const handleChange = spy(); - let wrapper; - - before(() => { - wrapper = mount(); - }); - - it('should render thumb with the disabled classes', () => { - const handle = findHandle(wrapper); - - assert.strictEqual(handle.hasClass(classes.thumb), true); - assert.strictEqual(handle.hasClass(classes.disabled), true); - }); - - it('should render tracks with the disabled classes', () => { - const tracks = wrapper.find('div').filterWhere(n => n.hasClass(classes.track)); - - assert.strictEqual(tracks.everyWhere(n => n.hasClass(classes.disabled)), true); - }); - - it("should not call 'onChange' handler", () => { - wrapper.simulate('click'); - - assert.strictEqual(handleChange.callCount, 0); - }); - - it('should signal that it is disabled', () => { - assert.ok(findHandle(wrapper).props().disabled); + it('should render the disabled classes', () => { + const wrapper = mount(); + assert.strictEqual(findOutermostIntrinsic(wrapper).hasClass(classes.disabled), true); }); }); - describe('prop: slider', () => { + describe('keyboard', () => { let wrapper; - const moveLeftEvent = new window.KeyboardEvent('keydown', { + const moveLeftEvent = { key: 'ArrowLeft', - }); - const moveRightEvent = new window.KeyboardEvent('keydown', { + }; + const moveRightEvent = { key: 'ArrowRight', - }); + }; before(() => { - function valueReducer(rawValue, props, event) { - const { disabled, max, min, step } = props; - - function roundToStep(number) { - return Math.round(number / step) * step; - } - - if (!disabled && step) { - if (rawValue > min && rawValue < max) { - if (rawValue === max - step) { - // If moving the Slider using arrow keys and value is formerly an maximum edge value - return roundToStep(rawValue + step / 2); - } - if (rawValue === min + step) { - // Same for minimum edge value - return roundToStep(rawValue - step / 2); - } - return roundToStep(rawValue); - } - return rawValue; - } - - return defaultValueReducer(rawValue, props, event); - } - const onChange = (_, value) => { wrapper.setProps({ value }); }; - wrapper = mount( - , - ); + wrapper = mount(); }); it('should reach right edge value', () => { wrapper.setProps({ value: 90 }); - const handle = findHandle(wrapper); + const thumb = findThumb(wrapper); - handle.prop('onKeyDown')(moveRightEvent); - assert.strictEqual(wrapper.prop('value'), 100); + thumb.simulate('keydown', moveRightEvent); + assert.strictEqual(wrapper.props().value, 100); - handle.prop('onKeyDown')(moveRightEvent); - assert.strictEqual(wrapper.prop('value'), 108); + thumb.simulate('keydown', moveRightEvent); + assert.strictEqual(wrapper.props().value, 108); - handle.prop('onKeyDown')(moveLeftEvent); - assert.strictEqual(wrapper.prop('value'), 100); + thumb.simulate('keydown', moveLeftEvent); + assert.strictEqual(wrapper.props().value, 100); - handle.prop('onKeyDown')(moveLeftEvent); - assert.strictEqual(wrapper.prop('value'), 90); + thumb.simulate('keydown', moveLeftEvent); + assert.strictEqual(wrapper.props().value, 90); }); it('should reach left edge value', () => { wrapper.setProps({ value: 20 }); - const handle = findHandle(wrapper); - handle.prop('onKeyDown')(moveLeftEvent); - assert.strictEqual(wrapper.prop('value'), 10); + const thumb = findThumb(wrapper); + thumb.simulate('keydown', moveLeftEvent); + assert.strictEqual(wrapper.props().value, 10); - handle.prop('onKeyDown')(moveLeftEvent); - assert.strictEqual(wrapper.prop('value'), 6); + thumb.simulate('keydown', moveLeftEvent); + assert.strictEqual(wrapper.props().value, 6); + + thumb.simulate('keydown', moveRightEvent); + assert.strictEqual(wrapper.props().value, 20); + + thumb.simulate('keydown', moveRightEvent); + assert.strictEqual(wrapper.props().value, 30); + }); + }); + + describe('markActive state', () => { + it('should set the mark active', () => { + function getActives(wrapper) { + return wrapper + .find(`.${classes.markLabel}`) + .map(node => node.hasClass(classes.markLabelActive)); + } + const marks = [{ value: 5 }, { value: 10 }, { value: 15 }]; - handle.prop('onKeyDown')(moveRightEvent); - assert.strictEqual(wrapper.prop('value'), 10); + const wrapper1 = mount(); + assert.deepEqual(getActives(wrapper1), [true, true, false]); - handle.prop('onKeyDown')(moveRightEvent); - assert.strictEqual(wrapper.prop('value'), 20); + const wrapper2 = mount(); + assert.deepEqual(getActives(wrapper2), [false, true, false]); }); }); }); diff --git a/packages/material-ui-lab/src/Slider/ValueLabel.js b/packages/material-ui-lab/src/Slider/ValueLabel.js new file mode 100644 index 00000000000000..66efb68a97d7cd --- /dev/null +++ b/packages/material-ui-lab/src/Slider/ValueLabel.js @@ -0,0 +1,86 @@ +import React from 'react'; +import { withStyles } from '@material-ui/core/styles'; +import clsx from 'clsx'; + +const styles = theme => ({ + thumb: { + '&$open': { + '& $offset': { + transform: 'scale(1) translateY(-10px)', + }, + }, + }, + open: {}, + offset: { + zIndex: 1, + ...theme.typography.body2, + fontSize: theme.typography.pxToRem(12), + lineHeight: 1.2, + transition: theme.transitions.create(['transform'], { + duration: theme.transitions.duration.shortest, + }), + top: -34, + left: 'calc(-50% + -4px)', + transformOrigin: 'bottom center', + transform: 'scale(0)', + position: 'absolute', + }, + circle: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 32, + height: 32, + borderRadius: '50% 50% 50% 0', + backgroundColor: 'currentColor', + transform: 'rotate(-45deg)', + }, + label: { + color: 'white', + transform: 'rotate(45deg)', + }, +}); + +/** + * @ignore - internal component. + */ +function ValueLabel(props) { + const { + children, + classes, + className, + index, + open, + value, + valueLabelDisplay, + valueLabelFormat, + } = props; + + if (valueLabelDisplay === 'off') { + return children; + } + + return React.cloneElement( + children, + { + className: clsx( + children.props.className, + { + [classes.open]: open || valueLabelDisplay === 'on', + }, + classes.thumb, + ), + }, + + + + {typeof valueLabelFormat === 'function' + ? valueLabelFormat(value, index) + : valueLabelFormat} + + + , + ); +} + +export default withStyles(styles, { name: 'PrivateValueLabel' })(ValueLabel); diff --git a/packages/material-ui-lab/src/Slider/index.js b/packages/material-ui-lab/src/Slider/index.js index 922d80e49ceea2..9898d6a85d1d01 100644 --- a/packages/material-ui-lab/src/Slider/index.js +++ b/packages/material-ui-lab/src/Slider/index.js @@ -1 +1 @@ -export { default, defaultValueReducer } from './Slider'; +export { default } from './Slider'; diff --git a/packages/material-ui-lab/src/SpeedDial/SpeedDial.js b/packages/material-ui-lab/src/SpeedDial/SpeedDial.js index 6899c4846a54b9..b92cf40814fe2b 100644 --- a/packages/material-ui-lab/src/SpeedDial/SpeedDial.js +++ b/packages/material-ui-lab/src/SpeedDial/SpeedDial.js @@ -8,7 +8,16 @@ import Zoom from '@material-ui/core/Zoom'; import Fab from '@material-ui/core/Fab'; import { isMuiElement, useForkRef } from '@material-ui/core/utils'; import * as utils from './utils'; -import { clamp } from '../utils'; + +function clamp(value, min, max) { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; +} const dialRadius = 32; const spacingActions = 16; diff --git a/packages/material-ui-lab/src/utils/clamp.d.ts b/packages/material-ui-lab/src/utils/clamp.d.ts deleted file mode 100644 index ebee452b697125..00000000000000 --- a/packages/material-ui-lab/src/utils/clamp.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default function clamp(value: number, min?: number, max?: number): number; diff --git a/packages/material-ui-lab/src/utils/clamp.js b/packages/material-ui-lab/src/utils/clamp.js deleted file mode 100644 index c0aa7667a1d1e6..00000000000000 --- a/packages/material-ui-lab/src/utils/clamp.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function clamp(value, min = 0, max = 100) { - return Math.min(Math.max(value, min), max); -} diff --git a/packages/material-ui-lab/src/utils/index.d.ts b/packages/material-ui-lab/src/utils/index.d.ts deleted file mode 100644 index b16ff7bb005842..00000000000000 --- a/packages/material-ui-lab/src/utils/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as clamp } from './clamp'; diff --git a/packages/material-ui-lab/src/utils/index.js b/packages/material-ui-lab/src/utils/index.js deleted file mode 100644 index ba965424fc1233..00000000000000 --- a/packages/material-ui-lab/src/utils/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/prefer-default-export -export { default as clamp } from './clamp'; diff --git a/packages/material-ui/src/ButtonBase/ButtonBase.test.js b/packages/material-ui/src/ButtonBase/ButtonBase.test.js index fd7cf9067851a1..c96be8c2ee77df 100644 --- a/packages/material-ui/src/ButtonBase/ButtonBase.test.js +++ b/packages/material-ui/src/ButtonBase/ButtonBase.test.js @@ -127,7 +127,7 @@ describe('', () => { events.forEach(n => { const event = n.charAt(2).toLowerCase() + n.slice(3); - wrapper.simulate(event, { persist: () => {} }); + wrapper.simulate(event, { target: {}, persist: () => {} }); assert.strictEqual(handlers[n].callCount, 1, `should have called the ${n} handler`); }); }); diff --git a/packages/material-ui/src/FilledInput/FilledInput.js b/packages/material-ui/src/FilledInput/FilledInput.js index 2b8eb2ae5aeb66..c7ca09e01ee8ca 100644 --- a/packages/material-ui/src/FilledInput/FilledInput.js +++ b/packages/material-ui/src/FilledInput/FilledInput.js @@ -177,7 +177,7 @@ FilledInput.propTypes = { */ className: PropTypes.string, /** - * The default `input` element value, useful when not controlling the component. + * The default `input` element value. Use when the component is not controlled. */ defaultValue: PropTypes.any, /** diff --git a/packages/material-ui/src/Input/Input.js b/packages/material-ui/src/Input/Input.js index 80b090b78c8248..2fa38996a8ff83 100644 --- a/packages/material-ui/src/Input/Input.js +++ b/packages/material-ui/src/Input/Input.js @@ -139,7 +139,7 @@ Input.propTypes = { */ className: PropTypes.string, /** - * The default `input` element value, useful when not controlling the component. + * The default `input` element value. Use when the component is not controlled. */ defaultValue: PropTypes.any, /** diff --git a/packages/material-ui/src/InputBase/InputBase.js b/packages/material-ui/src/InputBase/InputBase.js index 2d5ac0ec2f2603..ae2d3bed9804e6 100644 --- a/packages/material-ui/src/InputBase/InputBase.js +++ b/packages/material-ui/src/InputBase/InputBase.js @@ -433,7 +433,7 @@ InputBase.propTypes = { */ className: PropTypes.string, /** - * The default `input` element value, useful when not controlling the component. + * The default `input` element value. Use when the component is not controlled. */ defaultValue: PropTypes.any, /** diff --git a/packages/material-ui/src/OutlinedInput/OutlinedInput.js b/packages/material-ui/src/OutlinedInput/OutlinedInput.js index e15bbea9ae1e30..63f57c1270e25e 100644 --- a/packages/material-ui/src/OutlinedInput/OutlinedInput.js +++ b/packages/material-ui/src/OutlinedInput/OutlinedInput.js @@ -145,7 +145,7 @@ OutlinedInput.propTypes = { */ className: PropTypes.string, /** - * The default `input` element value, useful when not controlling the component. + * The default `input` element value. Use when the component is not controlled. */ defaultValue: PropTypes.any, /** diff --git a/packages/material-ui/src/Popper/Popper.spec.tsx b/packages/material-ui/src/Popper/Popper.spec.tsx index 08aa01b00ac077..ce734544e6cf40 100644 --- a/packages/material-ui/src/Popper/Popper.spec.tsx +++ b/packages/material-ui/src/Popper/Popper.spec.tsx @@ -7,7 +7,7 @@ interface Props { value: number; } -export default function ThumbLabelComponent(props: Props) { +export default function ValueLabelComponent(props: Props) { const { children, value } = props; const popperRef = React.useRef(null); diff --git a/packages/material-ui/src/RadioGroup/RadioGroup.js b/packages/material-ui/src/RadioGroup/RadioGroup.js index 731260b37d9655..cdc77ade46decd 100644 --- a/packages/material-ui/src/RadioGroup/RadioGroup.js +++ b/packages/material-ui/src/RadioGroup/RadioGroup.js @@ -81,7 +81,7 @@ RadioGroup.propTypes = { */ children: PropTypes.node, /** - * The default `input` element value, useful when not controlling the component. + * The default `input` element value. Use when the component is not controlled. */ defaultValue: PropTypes.any, /** diff --git a/packages/material-ui/src/Tabs/Tabs.js b/packages/material-ui/src/Tabs/Tabs.js index 9dfd56a7155765..ba7bbc038f70e1 100644 --- a/packages/material-ui/src/Tabs/Tabs.js +++ b/packages/material-ui/src/Tabs/Tabs.js @@ -463,7 +463,7 @@ Tabs.propTypes = { */ ScrollButtonComponent: PropTypes.elementType, /** - * Determine behavior of scroll buttons when tabs are set to scroll + * Determine behavior of scroll buttons when tabs are set to scroll: * * - `auto` will only present them when not all the items are visible. * - `desktop` will only present them on medium and larger viewports. diff --git a/packages/material-ui/src/utils/focusVisible.js b/packages/material-ui/src/utils/focusVisible.js index 1a1e59a4480152..f20ddb884fcabc 100644 --- a/packages/material-ui/src/utils/focusVisible.js +++ b/packages/material-ui/src/utils/focusVisible.js @@ -75,7 +75,7 @@ function handleVisibilityChange() { } } -export function prepare(ownerDocument) { +function prepare(ownerDocument) { ownerDocument.addEventListener('keydown', handleKeyDown, true); ownerDocument.addEventListener('mousedown', handlePointerDown, true); ownerDocument.addEventListener('pointerdown', handlePointerDown, true); @@ -91,7 +91,7 @@ export function teardown(ownerDocument) { ownerDocument.removeEventListener('visibilitychange', handleVisibilityChange, true); } -export function isFocusVisible(event) { +function isFocusVisible(event) { const { target } = event; try { return target.matches(':focus-visible'); @@ -110,7 +110,7 @@ export function isFocusVisible(event) { /** * Should be called if a blur event is fired on a focus-visible element */ -export function handleBlurVisible() { +function handleBlurVisible() { // To detect a tab/window switch, we look for a blur event followed // rapidly by a visibility change. // If we don't see a visibility change within 100ms, it's probably a diff --git a/packages/material-ui/src/utils/index.js b/packages/material-ui/src/utils/index.js index 98bba77882065d..fde589ccd4f444 100644 --- a/packages/material-ui/src/utils/index.js +++ b/packages/material-ui/src/utils/index.js @@ -1,8 +1,9 @@ export { default as deprecatedPropType } from './deprecatedPropType'; export * from './helpers'; export * from './reactHelpers'; -export { default as requirePropFactory } from './requirePropFactory'; +export { useIsFocusVisible } from './focusVisible'; export { default as ownerDocument } from './ownerDocument'; export { default as ownerWindow } from './ownerWindow'; +export { default as requirePropFactory } from './requirePropFactory'; export { default as unsupportedProp } from './unsupportedProp'; export { default as withForwardedRef } from './withForwardedRef'; diff --git a/pages/api/filled-input.md b/pages/api/filled-input.md index 95e7ca66914820..a893115333595f 100644 --- a/pages/api/filled-input.md +++ b/pages/api/filled-input.md @@ -22,7 +22,7 @@ import FilledInput from '@material-ui/core/FilledInput'; | autoFocus | bool | | If `true`, the `input` element will be focused during the first mount. | | classes | object | | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. | | className | string | | The CSS class name of the wrapper element. | -| defaultValue | any | | The default `input` element value, useful when not controlling the component. | +| defaultValue | any | | The default `input` element value. Use when the component is not controlled. | | disabled | bool | | If `true`, the `input` element will be disabled. | | disableUnderline | bool | | If `true`, the input will not have an underline. | | endAdornment | node | | End `InputAdornment` for this component. | diff --git a/pages/api/input-base.md b/pages/api/input-base.md index fb29675c77548c..d078839bbc91cd 100644 --- a/pages/api/input-base.md +++ b/pages/api/input-base.md @@ -24,7 +24,7 @@ It contains a load of style reset and some state logic. | autoFocus | bool | | If `true`, the `input` element will be focused during the first mount. | | classes | object | | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. | | className | string | | The CSS class name of the wrapper element. | -| defaultValue | any | | The default `input` element value, useful when not controlling the component. | +| defaultValue | any | | The default `input` element value. Use when the component is not controlled. | | disabled | bool | | If `true`, the `input` element will be disabled. | | endAdornment | node | | End `InputAdornment` for this component. | | error | bool | | If `true`, the input will indicate an error. This is normally obtained via context from FormControl. | diff --git a/pages/api/input.md b/pages/api/input.md index 214b31c3adebf6..4fabfbca8d43e4 100644 --- a/pages/api/input.md +++ b/pages/api/input.md @@ -22,7 +22,7 @@ import Input from '@material-ui/core/Input'; | autoFocus | bool | | If `true`, the `input` element will be focused during the first mount. | | classes | object | | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. | | className | string | | The CSS class name of the wrapper element. | -| defaultValue | any | | The default `input` element value, useful when not controlling the component. | +| defaultValue | any | | The default `input` element value. Use when the component is not controlled. | | disabled | bool | | If `true`, the `input` element will be disabled. | | disableUnderline | bool | | If `true`, the input will not have an underline. | | endAdornment | node | | End `InputAdornment` for this component. | diff --git a/pages/api/outlined-input.md b/pages/api/outlined-input.md index 2c3fea414ffd02..4d78f0d9063afb 100644 --- a/pages/api/outlined-input.md +++ b/pages/api/outlined-input.md @@ -22,7 +22,7 @@ import OutlinedInput from '@material-ui/core/OutlinedInput'; | autoFocus | bool | | If `true`, the `input` element will be focused during the first mount. | | classes | object | | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. | | className | string | | The CSS class name of the wrapper element. | -| defaultValue | any | | The default `input` element value, useful when not controlling the component. | +| defaultValue | any | | The default `input` element value. Use when the component is not controlled. | | disabled | bool | | If `true`, the `input` element will be disabled. | | endAdornment | node | | End `InputAdornment` for this component. | | error | bool | | If `true`, the input will indicate an error. This is normally obtained via context from FormControl. | diff --git a/pages/api/radio-group.md b/pages/api/radio-group.md index 430648bcc9b892..bcd48d94f2dfd3 100644 --- a/pages/api/radio-group.md +++ b/pages/api/radio-group.md @@ -19,7 +19,7 @@ import RadioGroup from '@material-ui/core/RadioGroup'; | Name | Type | Default | Description | |:-----|:-----|:--------|:------------| | children | node | | The content of the component. | -| defaultValue | any | | The default `input` element value, useful when not controlling the component. | +| defaultValue | any | | The default `input` element value. Use when the component is not controlled. | | name | string | | The name used to reference the value of the control. | | onChange | func | | Callback fired when a radio button is selected.

**Signature:**
`function(event: object, value: string) => void`
*event:* The event source of the callback. You can pull out the new value by accessing `event.target.value`.
*value:* The `value` of the selected radio button | | value | string | | Value of the selected radio button. | diff --git a/pages/api/slider.md b/pages/api/slider.md index be02f2e8c52e0a..d5ad85b9472bb3 100644 --- a/pages/api/slider.md +++ b/pages/api/slider.md @@ -18,21 +18,29 @@ import Slider from '@material-ui/lab/Slider'; | Name | Type | Default | Description | |:-----|:-----|:--------|:------------| +| aria-label | string | | The label of the slider. | +| aria-labelledby | string | | The id of the element containing a label for the slider. | +| aria-valuetext | string | | A string value that provides a user-friendly name for the current value of the slider. | | classes | object | | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. | -| component | elementType | 'div' | The component used for the root node. Either a string to use a DOM element or a component. | -| disabled | bool | | If `true`, the slider will be disabled. | +| component | elementType | 'span' | The component used for the root node. Either a string to use a DOM element or a component. | +| defaultValue | union: number |
 arrayOf
| | The default element value. Use when the component is not controlled. | +| disabled | bool | false | If `true`, the slider will be disabled. | +| getAriaValueText | func | | Accepts a function which returns a string value that provides a user-friendly name for the current value of the slider.

**Signature:**
`function(value: number, index: number) => void`
*value:* The thumb label's value to format
*index:* The thumb label's index to format | +| marks | union: bool |
 array
| [] | 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. | | max | number | 100 | The maximum allowed value of the slider. Should not be equal to min. | | min | number | 0 | The minimum allowed value of the slider. Should not be equal to max. | -| onChange | func | | Callback function that is fired when the slider's value changed. | -| onDragEnd | func | | Callback function that is fired when the slide has stopped moving. | -| onDragStart | func | | Callback function that is fired when the slider has begun to move. | -| step | number | | The granularity the slider can step through values. | -| thumb | element | | The component used for the slider icon. This is optional, if provided should be a react element. | -| value * | number | | The value of the slider. | -| valueReducer | func | function defaultValueReducer(rawValue, props) { const { disabled, step } = props; if (disabled) { return null; } if (step) { return roundToStep(rawValue, step); } return Number(rawValue.toFixed(3));} | the reducer used to process the value emitted from the slider. If `null` or the same value is returned no change is emitted.

**Signature:**
`function(rawValue: number, props: SliderProps, event: Event) => void`
*rawValue:* value in [min, max]
*props:* current props of the Slider
*event:* the event the change was triggered from | -| vertical | bool | | If `true`, the slider will be vertical. | - -The component cannot hold a ref. +| name | string | | Name attribute of the hidden `input` element. | +| onChange | func | | Callback function that is fired when the slider's value changed.

**Signature:**
`function(event: object, value: any) => void`
*event:* The event source of the callback
*value:* The new value | +| onChangeCommitted | func | | Callback function that is fired when the `mouseup` is triggered.

**Signature:**
`function(event: object, value: any) => void`
*event:* The event source of the callback
*value:* The new value | +| orientation | enum: 'horizontal' |
 'vertical'
| 'horizontal' | The slider orientation. | +| step | number | 1 | The granularity with which the slider can step through values. (A "discrete" slider.) When step is `null`, the thumb can only be slid onto marks provided with the `marks` prop. | +| ThumbComponent | elementType | 'span' | The component used to display the value label. | +| value | union: number |
 arrayOf
| | The value of the slider. For ranged sliders, provide an array with two values. | +| ValueLabelComponent | elementType | ValueLabel | The value label componnet. | +| valueLabelDisplay | enum: 'on' |
 'auto' |
 'off'
| 'off' | Controls when the value label is displayed:
- `auto` the value label will display when the thumb is hovered or focused. - `on` will display persistently. - `off` will never display. | +| valueLabelFormat | union: string |
 func
| x => x | The format function the value label's value.
When a function is provided, it should have the following signature:
- {number} value The value label's value to format - {number} index The value label's index to format | + +The `ref` is forwarded to the root element. Any other properties supplied will be provided to the root element (native element). @@ -45,19 +53,19 @@ This property accepts the following keys: | Name | Description | |:-----|:------------| | root | Styles applied to the root element. -| container | Styles applied to the container element. -| track | Styles applied to the track elements. -| trackBefore | Styles applied to the track element before the thumb. -| trackAfter | Styles applied to the track element after the thumb. -| thumbWrapper | Styles applied to the thumb wrapper element. +| marked | Styles applied to the root element if `marks` is provided with at least one label. +| vertical | Pseudo-class applied to the root element if `orientation="vertical"`. +| disabled | Pseudo-class applied to the root element if `disabled={true}`. +| rail | Styles applied to the rail element. +| track | Styles applied to the track element. | thumb | Styles applied to the thumb element. -| thumbIconWrapper | Class applied to the thumb element if custom thumb icon provided. -| thumbIcon | -| disabled | Class applied to the track and thumb elements to trigger JSS nested styles if `disabled`. -| jumped | Class applied to the track and thumb elements to trigger JSS nested styles if `jumped`. -| focused | Class applied to the track and thumb elements to trigger JSS nested styles if `focused`. -| activated | Class applied to the track and thumb elements to trigger JSS nested styles if `activated`. -| vertical | Class applied to the root, track and container to trigger JSS nested styles if `vertical`. +| active | Pseudo-class applied to the thumb element if it's active. +| focusVisible | Pseudo-class applied to the thumb element if keyboard focused. +| valueLabel | Styles applied to the thumb label element. +| mark | Styles applied to the mark element. +| markActive | Styles applied to the mark element if active (depending on the value). +| markLabel | Styles applied to the mark label element. +| markLabelActive | Styles applied to the mark label element if active (depending on the value). Have a look at the [overriding styles with classes](/customization/components/#overriding-styles-with-classes) section and the [implementation of the component](https://github.com/mui-org/material-ui/blob/master/packages/material-ui-lab/src/Slider/Slider.js) diff --git a/pages/api/tabs.md b/pages/api/tabs.md index dfe437d8258ca1..5eebe61939d772 100644 --- a/pages/api/tabs.md +++ b/pages/api/tabs.md @@ -26,7 +26,7 @@ import Tabs from '@material-ui/core/Tabs'; | indicatorColor | enum: 'secondary' |
 'primary'
| 'secondary' | Determines the color of the indicator. | | onChange | func | | Callback fired when the value changes.

**Signature:**
`function(event: object, value: any) => void`
*event:* The event source of the callback
*value:* We default to the index of the child (number) | | ScrollButtonComponent | elementType | TabScrollButton | The component used to render the scroll buttons. | -| scrollButtons | enum: 'auto' |
 'desktop' |
 'on' |
 'off'
| 'auto' | Determine behavior of scroll buttons when tabs are set to scroll
- `auto` will only present them when not all the items are visible. - `desktop` will only present them on medium and larger viewports. - `on` will always present them. - `off` will never present them. | +| scrollButtons | enum: 'auto' |
 'desktop' |
 'on' |
 'off'
| 'auto' | Determine behavior of scroll buttons when tabs are set to scroll:
- `auto` will only present them when not all the items are visible. - `desktop` will only present them on medium and larger viewports. - `on` will always present them. - `off` will never present them. | | TabIndicatorProps | object | | Properties applied to the `TabIndicator` element. | | textColor | enum: 'secondary' |
 'primary' |
 'inherit'
| 'inherit' | Determines the color of the `Tab`. | | value | any | | The value of the currently selected `Tab`. If you don't want any selected `Tab`, you can set this property to `false`. | diff --git a/scripts/sizeSnapshot/webpack.config.js b/scripts/sizeSnapshot/webpack.config.js index 69d34b87cc075c..25a23da5618b24 100644 --- a/scripts/sizeSnapshot/webpack.config.js +++ b/scripts/sizeSnapshot/webpack.config.js @@ -96,6 +96,12 @@ async function getSizeLimitBundles() { webpack: true, path: 'packages/material-ui/build/esm/InputBase/Textarea.js', }, + { + // vs https://bundlephobia.com/result?p=rc-slider + name: 'Slider', + webpack: true, + path: 'packages/material-ui-lab/build/esm/Slider/index.js', + }, { name: 'docs.main', webpack: false,