From 6814667056245b2d614e48c1622faa28814f2a86 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Tue, 7 Dec 2021 11:39:16 +0100 Subject: [PATCH 01/18] [SliderUnstyled] Update components & componentsProps --- .../src/SliderUnstyled/SliderUnstyled.d.ts | 12 +-- .../src/SliderUnstyled/SliderUnstyled.js | 88 ++++++++----------- .../src/SliderUnstyled/SliderUnstyled.test.js | 25 ++++-- .../mui-base/src/utils/appendOwnerState.ts | 6 +- packages/mui-material/src/Slider/Slider.js | 22 ++--- test/utils/describeConformanceUnstyled.tsx | 14 +-- 6 files changed, 74 insertions(+), 93 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.d.ts b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.d.ts index 165c295b77ee6b..f127ad3139bd7a 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.d.ts +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.d.ts @@ -55,12 +55,12 @@ export interface SliderUnstyledTypeMap

& SliderUnstyledComponentsPropsOverrides; + track?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; + rail?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; + thumb?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; + mark?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; + markLabel?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; valueLabel?: React.ComponentPropsWithRef & SliderUnstyledComponentsPropsOverrides; }; diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index 6448594ac9d330..eaa8b0babf4b3d 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -11,6 +11,7 @@ import { unstable_useControlled as useControlled, visuallyHidden, } from '@mui/utils'; +import appendOwnerState from '../utils/appendOwnerState'; import isHostComponent from '../utils/isHostComponent'; import composeClasses from '../composeClasses'; import { getSliderUtilityClass } from './sliderUnstyledClasses'; @@ -188,7 +189,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { 'aria-labelledby': ariaLabelledby, 'aria-valuetext': ariaValuetext, className, - component = 'span', + component, classes: classesProp, defaultValue, disableSwap = false, @@ -586,27 +587,6 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { ...axisProps[axis].leap(trackLeap), }; - const Root = components.Root || component; - const rootProps = componentsProps.root || {}; - - const Rail = components.Rail || 'span'; - const railProps = componentsProps.rail || {}; - - const Track = components.Track || 'span'; - const trackProps = componentsProps.track || {}; - - const Thumb = components.Thumb || 'span'; - const thumbProps = componentsProps.thumb || {}; - - const ValueLabel = components.ValueLabel || SliderValueLabelUnstyled; - const valueLabelProps = componentsProps.valueLabel || {}; - - const Mark = components.Mark || 'span'; - const markProps = componentsProps.mark || {}; - - const MarkLabel = components.MarkLabel || 'span'; - const markLabelProps = componentsProps.markLabel || {}; - // all props with defaults // consider extracting to hook an reusing the lint rule for the varints const ownerState = { @@ -626,6 +606,27 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { valueLabelFormat, }; + const Root = component ?? components.Root ?? 'span'; + const rootProps = appendOwnerState(Root, { ...other, ...componentsProps.root }, ownerState); + + const Rail = components.Rail ?? 'span'; + const railProps = appendOwnerState(Rail, componentsProps.rail, ownerState); + + const Track = components.Track ?? 'span'; + const trackProps = appendOwnerState(Track, componentsProps.track, ownerState); + + const Thumb = components.Thumb ?? 'span'; + const thumbProps = componentsProps.thumb || {}; + + const ValueLabel = components.ValueLabel ?? SliderValueLabelUnstyled; + const valueLabelProps = appendOwnerState(ValueLabel, componentsProps.valueLabel, ownerState); + + const Mark = components.Mark ?? 'span'; + const markProps = appendOwnerState(Mark, componentsProps.mark, ownerState); + + const MarkLabel = components.MarkLabel ?? 'span'; + const markLabelProps = appendOwnerState(MarkLabel, componentsProps.markLabel, ownerState); + const classes = useUtilityClasses(ownerState); return ( @@ -635,23 +636,13 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { {...rootProps} {...(!isHostComponent(Root) && { as: component, - ownerState: { ...ownerState, ...rootProps.ownerState }, })} {...other} className={clsx(classes.root, rootProps.className, className)} > - + @@ -680,7 +671,6 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { data-index={index} {...markProps} {...(!isHostComponent(Mark) && { - ownerState: { ...ownerState, ...markProps.ownerState }, markActive, })} style={{ ...style, ...markProps.style }} @@ -694,12 +684,8 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { data-index={index} {...markLabelProps} {...(!isHostComponent(MarkLabel) && { - ownerState: { - ...ownerState, - ...markLabelProps.ownerState, - }, + markLabelActive: markActive, })} - markLabelActive={markActive} style={{ ...style, ...markLabelProps.style }} className={clsx(classes.markLabel, markLabelProps.className, { [classes.markLabelActive]: markActive, @@ -720,21 +706,19 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { return ( ', () => { before(function beforeHook() { @@ -14,18 +20,19 @@ describe('', () => { const mount = createMount(); const { render } = createRenderer(); - describeConformance(, () => ({ + describeConformanceUnstyled(, () => ({ classes, inheritComponent: 'span', - mount, render, + mount, refInstanceof: window.HTMLSpanElement, - testComponentPropWith: 'span', - skip: [ - 'themeDefaultProps', // unstyled - 'themeStyleOverrides', // unstyled - 'themeVariants', // unstyled - ], + testComponentPropWith: 'div', + muiName: 'MuiSlider', + slots: { + root: { + expectedClassName: classes.root, + }, + }, })); it('forwards style props on the Root component', () => { diff --git a/packages/mui-base/src/utils/appendOwnerState.ts b/packages/mui-base/src/utils/appendOwnerState.ts index c1255ffa62eb7e..dd74ee4214bfc5 100644 --- a/packages/mui-base/src/utils/appendOwnerState.ts +++ b/packages/mui-base/src/utils/appendOwnerState.ts @@ -12,12 +12,14 @@ export default function appendOwnerState( existingProps: Record = {}, ownerState: object, ) { + const { ownerState: existingPropsOwnerState, ...otherExistingProps } = existingProps; + if (isHostComponent(elementType)) { - return existingProps; + return otherExistingProps; } return { ...existingProps, - ownerState: { ...existingProps.ownerState, ...ownerState }, + ownerState: { ...existingPropsOwnerState, ...ownerState }, }; } diff --git a/packages/mui-material/src/Slider/Slider.js b/packages/mui-material/src/Slider/Slider.js index c64c45ee31d01e..6830d0f2d8f3d4 100644 --- a/packages/mui-material/src/Slider/Slider.js +++ b/packages/mui-material/src/Slider/Slider.js @@ -2,7 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { chainPropTypes } from '@mui/utils'; -import { generateUtilityClasses, isHostComponent } from '@mui/base'; +import { generateUtilityClasses } from '@mui/base'; import SliderUnstyled, { SliderValueLabelUnstyled, sliderUnstyledClasses, @@ -417,10 +417,6 @@ const extendUtilityClasses = (ownerState) => { }; }; -const shouldSpreadOwnerState = (Component) => { - return !Component || !isHostComponent(Component); -}; - const Slider = React.forwardRef(function Slider(inputProps, ref) { const props = useThemeProps({ props: inputProps, name: 'MuiSlider' }); @@ -457,27 +453,19 @@ const Slider = React.forwardRef(function Slider(inputProps, ref) { ...componentsProps, root: { ...componentsProps.root, - ...(shouldSpreadOwnerState(components.Root) && { - ownerState: { ...componentsProps.root?.ownerState, color, size }, - }), + ownerState: { ...componentsProps.root?.ownerState, color, size }, }, thumb: { ...componentsProps.thumb, - ...(shouldSpreadOwnerState(components.Thumb) && { - ownerState: { ...componentsProps.thumb?.ownerState, color, size }, - }), + ownerState: { ...componentsProps.thumb?.ownerState, color, size }, }, track: { ...componentsProps.track, - ...(shouldSpreadOwnerState(components.Track) && { - ownerState: { ...componentsProps.track?.ownerState, color, size }, - }), + ownerState: { ...componentsProps.track?.ownerState, color, size }, }, valueLabel: { ...componentsProps.valueLabel, - ...(shouldSpreadOwnerState(components.ValueLabel) && { - ownerState: { ...componentsProps.valueLabel?.ownerState, color, size }, - }), + ownerState: { ...componentsProps.valueLabel?.ownerState, color, size }, }, }} classes={classes} diff --git a/test/utils/describeConformanceUnstyled.tsx b/test/utils/describeConformanceUnstyled.tsx index 53ebfd6b8ddb12..d61033bcf2c68c 100644 --- a/test/utils/describeConformanceUnstyled.tsx +++ b/test/utils/describeConformanceUnstyled.tsx @@ -42,7 +42,7 @@ interface WithClassName { interface WithCustomProp { fooBar: string; - tabIndex: number; + role: number; } interface WithOwnerState { @@ -75,14 +75,14 @@ function testPropForwarding( it('forwards custom props to the root element if a component is provided', () => { const CustomRoot = React.forwardRef( - ({ fooBar, tabIndex }: WithCustomProp, ref: React.ForwardedRef) => { + ({ fooBar, role }: WithCustomProp, ref: React.ForwardedRef) => { // @ts-ignore - return ; + return ; }, ); const otherProps = { - tabIndex: '0', + role: 'button', fooBar: randomStringValue(), }; @@ -90,13 +90,13 @@ function testPropForwarding( React.cloneElement(element, { components: { Root: CustomRoot }, ...otherProps }), ); - expect(container.firstChild).to.have.attribute('tabindex', otherProps.tabIndex.toString()); + expect(container.firstChild).to.have.attribute('role', otherProps.role.toString()); expect(container.firstChild).to.have.attribute('data-foobar', otherProps.fooBar); }); it('does forward standard props to the root element if an intrinsic element is provided', () => { const otherProps = { - tabIndex: '0', + role: 'button', 'data-foobar': randomStringValue(), }; @@ -104,7 +104,7 @@ function testPropForwarding( React.cloneElement(element, { components: { Root: Element }, ...otherProps }), ); - expect(container.firstChild).to.have.attribute('tabindex', otherProps.tabIndex); + expect(container.firstChild).to.have.attribute('role', otherProps.role); expect(container.firstChild).to.have.attribute('data-foobar', otherProps['data-foobar']); }); } From 96c3dff6409cd75254e5eab95c4d4a83246dcf74 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Tue, 7 Dec 2021 13:16:00 +0100 Subject: [PATCH 02/18] [useSlider] Add hook (WIP) --- .../src/SliderUnstyled/SliderUnstyled.js | 569 +-------------- .../mui-base/src/SliderUnstyled/index.d.ts | 3 + packages/mui-base/src/SliderUnstyled/index.js | 1 + .../mui-base/src/SliderUnstyled/useSlider.tsx | 657 ++++++++++++++++++ 4 files changed, 687 insertions(+), 543 deletions(-) create mode 100644 packages/mui-base/src/SliderUnstyled/useSlider.tsx diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index eaa8b0babf4b3d..df01e1841b5295 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -1,156 +1,13 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import { - chainPropTypes, - unstable_useIsFocusVisible as useIsFocusVisible, - unstable_useEnhancedEffect as useEnhancedEffect, - unstable_ownerDocument as ownerDocument, - unstable_useEventCallback as useEventCallback, - unstable_useForkRef as useForkRef, - unstable_useControlled as useControlled, - visuallyHidden, -} from '@mui/utils'; +import { chainPropTypes, visuallyHidden } from '@mui/utils'; import appendOwnerState from '../utils/appendOwnerState'; import isHostComponent from '../utils/isHostComponent'; import composeClasses from '../composeClasses'; import { getSliderUtilityClass } from './sliderUnstyledClasses'; import SliderValueLabelUnstyled from './SliderValueLabelUnstyled'; - -const INTENTIONAL_DRAG_COUNT_THRESHOLD = 2; - -function asc(a, b) { - return a - b; -} - -function clamp(value, min, max) { - if (value == null) { - return min; - } - return Math.min(Math.max(min, value), max); -} - -function findClosest(values, currentValue) { - const { index: closestIndex } = values.reduce((acc, value, index) => { - const distance = Math.abs(currentValue - value); - - if (acc === null || distance < acc.distance || distance === acc.distance) { - return { - distance, - index, - }; - } - - return acc; - }, null); - return closestIndex; -} - -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.current) { - return { - x: touch.clientX, - y: touch.clientY, - }; - } - } - - return false; - } - - return { - x: event.clientX, - y: event.clientY, - }; -} - -function valueToPercent(value, min, max) { - return ((value - min) * 100) / (max - min); -} - -function percentToValue(percent, min, max) { - return (max - min) * percent + min; -} - -function getDecimalPrecision(num) { - // This handles the case when num is very small (0.00000001), js will turn this into 1e-8. - // When num is bigger than 1 or less than -1 it won't get converted to this notation so it's fine. - if (Math.abs(num) < 1) { - const parts = num.toExponential().split('e-'); - const matissaDecimalPart = parts[0].split('.')[1]; - return (matissaDecimalPart ? matissaDecimalPart.length : 0) + parseInt(parts[1], 10); - } - - const decimalPart = num.toString().split('.')[1]; - return decimalPart ? decimalPart.length : 0; -} - -function roundValueToStep(value, step, min) { - const nearest = Math.round((value - min) / step) * step + min; - return Number(nearest.toFixed(getDecimalPrecision(step))); -} - -function setValueIndex({ values, newValue, index }) { - const output = values.slice(); - output[index] = newValue; - return output.sort(asc); -} - -function focusThumb({ sliderRef, activeIndex, setActive }) { - const doc = ownerDocument(sliderRef.current); - if ( - !sliderRef.current.contains(doc.activeElement) || - Number(doc.activeElement.getAttribute('data-index')) !== activeIndex - ) { - sliderRef.current.querySelector(`[type="range"][data-index="${activeIndex}"]`).focus(); - } - - if (setActive) { - setActive(activeIndex); - } -} - -const axisProps = { - horizontal: { - offset: (percent) => ({ left: `${percent}%` }), - leap: (percent) => ({ width: `${percent}%` }), - }, - 'horizontal-reverse': { - offset: (percent) => ({ right: `${percent}%` }), - leap: (percent) => ({ width: `${percent}%` }), - }, - vertical: { - offset: (percent) => ({ bottom: `${percent}%` }), - leap: (percent) => ({ height: `${percent}%` }), - }, -}; - -const Identity = (x) => x; - -// TODO: remove support for Safari < 13. -// https://caniuse.com/#search=touch-action -// -// Safari, on iOS, supports touch action since v13. -// Over 80% of the iOS phones are compatible -// in August 2020. -// Utilizing the CSS.supports method to check if touch-action is supported. -// Since CSS.supports is supported on all but Edge@12 and IE and touch-action -// is supported on both Edge@12 and IE if CSS.supports is not available that means that -// touch-action will be supported -let cachedSupportsTouchActionNone; -function doesSupportTouchActionNone() { - if (cachedSupportsTouchActionNone === undefined) { - if (typeof CSS !== 'undefined' && typeof CSS.supports === 'function') { - cachedSupportsTouchActionNone = CSS.supports('touch-action', 'none'); - } else { - cachedSupportsTouchActionNone = true; - } - } - return cachedSupportsTouchActionNone; -} +import useSlider, { Identity, valueToPercent } from './useSlider'; const useUtilityClasses = (ownerState) => { const { disabled, dragging, marked, orientation, track, classes } = ownerState; @@ -186,12 +43,10 @@ const Forward = ({ children }) => children; const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { const { 'aria-label': ariaLabel, - 'aria-labelledby': ariaLabelledby, 'aria-valuetext': ariaValuetext, className, component, classes: classesProp, - defaultValue, disableSwap = false, disabled = false, getAriaLabel, @@ -217,385 +72,13 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { ...other } = props; - 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 transferred when inversing a range slider. - const [active, setActive] = React.useState(-1); - const [open, setOpen] = React.useState(-1); - const [dragging, setDragging] = React.useState(false); - const moveCount = React.useRef(0); - - const [valueDerived, setValueState] = useControlled({ - controlled: valueProp, - default: defaultValue ?? min, - name: 'Slider', - }); - - const handleChange = - onChange && - ((event, value, thumbIndex) => { - // Redefine target to allow name and value to be read. - // This allows seamless integration with the most popular form libraries. - // https://github.com/mui-org/material-ui/issues/13485#issuecomment-676048492 - // Clone the event to not override `target` of the original event. - const nativeEvent = event.nativeEvent || event; - const clonedEvent = new nativeEvent.constructor(nativeEvent.type, nativeEvent); - - Object.defineProperty(clonedEvent, 'target', { - writable: true, - value: { value, name }, - }); - - onChange(clonedEvent, value, thumbIndex); - }); - - const range = Array.isArray(valueDerived); - let values = range ? valueDerived.slice().sort(asc) : [valueDerived]; - values = values.map((value) => clamp(value, min, max)); - const marks = - marksProp === true && step !== null - ? [...Array(Math.floor((max - min) / step) + 1)].map((_, index) => ({ - value: min + step * index, - })) - : marksProp || []; - - const { - isFocusVisibleRef, - onBlur: handleBlurVisible, - onFocus: handleFocusVisible, - ref: focusVisibleRef, - } = useIsFocusVisible(); - const [focusVisible, setFocusVisible] = React.useState(-1); - - const sliderRef = React.useRef(); - const handleFocusRef = useForkRef(focusVisibleRef, sliderRef); - const handleRef = useForkRef(ref, handleFocusRef); - - const handleFocus = (event) => { - const index = Number(event.currentTarget.getAttribute('data-index')); - handleFocusVisible(event); - if (isFocusVisibleRef.current === true) { - setFocusVisible(index); - } - setOpen(index); - }; - const handleBlur = (event) => { - handleBlurVisible(event); - if (isFocusVisibleRef.current === false) { - setFocusVisible(-1); - } - setOpen(-1); - }; - const handleMouseOver = useEventCallback((event) => { - const index = Number(event.currentTarget.getAttribute('data-index')); - setOpen(index); - }); - const handleMouseLeave = useEventCallback(() => { - setOpen(-1); - }); - - useEnhancedEffect(() => { - if (disabled && sliderRef.current.contains(document.activeElement)) { - // This is necessary because Firefox and Safari will keep focus - // on a disabled element: - // https://codesandbox.io/s/mui-pr-22247-forked-h151h?file=/src/App.js - document.activeElement.blur(); - } - }, [disabled]); - - if (disabled && active !== -1) { - setActive(-1); - } - if (disabled && focusVisible !== -1) { - setFocusVisible(-1); - } - - const handleHiddenInputChange = (event) => { - const index = Number(event.currentTarget.getAttribute('data-index')); - const value = values[index]; - const marksValues = marks.map((mark) => mark.value); - const marksIndex = marksValues.indexOf(value); - - let newValue = event.target.valueAsNumber; - - if (marks && step == null) { - newValue = newValue < value ? marksValues[marksIndex - 1] : marksValues[marksIndex + 1]; - } - - newValue = clamp(newValue, min, max); - - if (marks && step == null) { - const markValues = marks.map((mark) => mark.value); - const currentMarkIndex = markValues.indexOf(values[index]); - - newValue = - newValue < values[index] - ? markValues[currentMarkIndex - 1] - : markValues[currentMarkIndex + 1]; - } - - if (range) { - // Bound the new value to the thumb's neighbours. - if (disableSwap) { - newValue = clamp(newValue, values[index - 1] || -Infinity, values[index + 1] || Infinity); - } - - const previousValue = newValue; - newValue = setValueIndex({ - values, - newValue, - index, - }); - - let activeIndex = index; - - // Potentially swap the index if needed. - if (!disableSwap) { - activeIndex = newValue.indexOf(previousValue); - } - - focusThumb({ sliderRef, activeIndex }); - } - - setValueState(newValue); - setFocusVisible(index); - - if (handleChange) { - handleChange(event, newValue, index); - } - - if (onChangeCommitted) { - onChangeCommitted(event, newValue); - } - }; - - const previousIndex = React.useRef(); - let axis = orientation; - if (isRtl && orientation === 'horizontal') { - axis += '-reverse'; - } - - const getFingerNewValue = ({ finger, move = false, values: values2 }) => { - const { current: slider } = sliderRef; - const { width, height, bottom, left } = slider.getBoundingClientRect(); - let percent; - - if (axis.indexOf('vertical') === 0) { - percent = (bottom - finger.y) / height; - } else { - percent = (finger.x - left) / width; - } - - if (axis.indexOf('-reverse') !== -1) { - percent = 1 - percent; - } - - let newValue; - newValue = percentToValue(percent, min, max); - if (step) { - newValue = roundValueToStep(newValue, step, min); - } else { - const marksValues = marks.map((mark) => mark.value); - const closestIndex = findClosest(marksValues, newValue); - newValue = marksValues[closestIndex]; - } - - newValue = clamp(newValue, min, max); - let activeIndex = 0; - - if (range) { - if (!move) { - activeIndex = findClosest(values2, newValue); - } else { - activeIndex = previousIndex.current; - } - - // Bound the new value to the thumb's neighbours. - if (disableSwap) { - newValue = clamp( - newValue, - values2[activeIndex - 1] || -Infinity, - values2[activeIndex + 1] || Infinity, - ); - } - - const previousValue = newValue; - newValue = setValueIndex({ - values: values2, - newValue, - index: activeIndex, - }); - - // Potentially swap the index if needed. - if (!(disableSwap && move)) { - activeIndex = newValue.indexOf(previousValue); - previousIndex.current = activeIndex; - } - } - - return { newValue, activeIndex }; - }; - - const handleTouchMove = useEventCallback((nativeEvent) => { - const finger = trackFinger(nativeEvent, touchId); - - if (!finger) { - return; - } - - moveCount.current += 1; - - // Cancel move in case some other element consumed a mouseup event and it was not fired. - if (nativeEvent.type === 'mousemove' && nativeEvent.buttons === 0) { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - handleTouchEnd(nativeEvent); - return; - } - - const { newValue, activeIndex } = getFingerNewValue({ - finger, - move: true, - values, - }); - - focusThumb({ sliderRef, activeIndex, setActive }); - setValueState(newValue); - - if (!dragging && moveCount.current > INTENTIONAL_DRAG_COUNT_THRESHOLD) { - setDragging(true); - } - - if (handleChange) { - handleChange(nativeEvent, newValue, activeIndex); - } - }); - - const handleTouchEnd = useEventCallback((nativeEvent) => { - const finger = trackFinger(nativeEvent, touchId); - setDragging(false); - - if (!finger) { - return; - } - - const { newValue } = getFingerNewValue({ finger, values }); - - setActive(-1); - if (nativeEvent.type === 'touchend') { - setOpen(-1); - } - - if (onChangeCommitted) { - onChangeCommitted(nativeEvent, newValue); - } - - touchId.current = undefined; - - // eslint-disable-next-line @typescript-eslint/no-use-before-define - stopListening(); - }); - - const handleTouchStart = useEventCallback((nativeEvent) => { - // If touch-action: none; is not supported we need to prevent the scroll manually. - if (!doesSupportTouchActionNone()) { - nativeEvent.preventDefault(); - } - - const touch = nativeEvent.changedTouches[0]; - if (touch != null) { - // A number that uniquely identifies the current finger in the touch session. - touchId.current = touch.identifier; - } - const finger = trackFinger(nativeEvent, touchId); - const { newValue, activeIndex } = getFingerNewValue({ finger, values }); - focusThumb({ sliderRef, activeIndex, setActive }); - - setValueState(newValue); - - if (handleChange) { - handleChange(nativeEvent, newValue, activeIndex); - } - - moveCount.current = 0; - const doc = ownerDocument(sliderRef.current); - doc.addEventListener('touchmove', handleTouchMove); - doc.addEventListener('touchend', handleTouchEnd); - }); - - const stopListening = React.useCallback(() => { - const doc = ownerDocument(sliderRef.current); - doc.removeEventListener('mousemove', handleTouchMove); - doc.removeEventListener('mouseup', handleTouchEnd); - doc.removeEventListener('touchmove', handleTouchMove); - doc.removeEventListener('touchend', handleTouchEnd); - }, [handleTouchEnd, handleTouchMove]); - - React.useEffect(() => { - const { current: slider } = sliderRef; - slider.addEventListener('touchstart', handleTouchStart, { - passive: doesSupportTouchActionNone(), - }); - - return () => { - slider.removeEventListener('touchstart', handleTouchStart, { - passive: doesSupportTouchActionNone(), - }); - - stopListening(); - }; - }, [stopListening, handleTouchStart]); - - React.useEffect(() => { - if (disabled) { - stopListening(); - } - }, [disabled, stopListening]); - - const handleMouseDown = useEventCallback((event) => { - if (onMouseDown) { - onMouseDown(event); - } - - // Only handle left clicks - if (event.button !== 0) { - return; - } - - // Avoid text selection - event.preventDefault(); - const finger = trackFinger(event, touchId); - const { newValue, activeIndex } = getFingerNewValue({ finger, values }); - focusThumb({ sliderRef, activeIndex, setActive }); - - setValueState(newValue); - - if (handleChange) { - handleChange(event, newValue, activeIndex); - } - - moveCount.current = 0; - const doc = ownerDocument(sliderRef.current); - doc.addEventListener('mousemove', handleTouchMove); - doc.addEventListener('mouseup', handleTouchEnd); - }); - - const trackOffset = valueToPercent(range ? values[0] : min, min, max); - const trackLeap = valueToPercent(values[values.length - 1], min, max) - trackOffset; - const trackStyle = { - ...axisProps[axis].offset(trackOffset), - ...axisProps[axis].leap(trackLeap), - }; - // all props with defaults // consider extracting to hook an reusing the lint rule for the varints const ownerState = { ...props, classes: classesProp, disabled, - dragging, isRtl, - marked: marks.length > 0 && marks.some((mark) => mark.label), max, min, orientation, @@ -606,6 +89,26 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { valueLabelFormat, }; + const { + axisProps, + getRootProps, + getTrackProps, + getHiddenInputProps, + open, + active, + axis, + range, + focusVisible, + handleMouseOver, + handleMouseLeave, + dragging, + marks, + values, + } = useSlider({ ...ownerState, ref }); + + ownerState.marked = marks.length > 0 && marks.some((mark) => mark.label); + ownerState.dragging = dragging; + const Root = component ?? components.Root ?? 'span'; const rootProps = appendOwnerState(Root, { ...other, ...componentsProps.root }, ownerState); @@ -631,8 +134,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { return ( {marks.map((mark, index) => { const percent = valueToPercent(mark.value, min, max); @@ -739,34 +242,14 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { }} > diff --git a/packages/mui-base/src/SliderUnstyled/index.d.ts b/packages/mui-base/src/SliderUnstyled/index.d.ts index f7141e5cf29460..3040453cedd4f5 100644 --- a/packages/mui-base/src/SliderUnstyled/index.d.ts +++ b/packages/mui-base/src/SliderUnstyled/index.d.ts @@ -4,5 +4,8 @@ export * from './SliderUnstyled'; export { default as SliderValueLabelUnstyled } from './SliderValueLabelUnstyled'; export * from './SliderValueLabelUnstyled'; +export { default as useSlider } from './useSlider'; +export * from './useSlider'; + export { default as sliderUnstyledClasses } from './sliderUnstyledClasses'; export * from './sliderUnstyledClasses'; diff --git a/packages/mui-base/src/SliderUnstyled/index.js b/packages/mui-base/src/SliderUnstyled/index.js index fd8c7f0ef356d2..8a215cc62ada6f 100644 --- a/packages/mui-base/src/SliderUnstyled/index.js +++ b/packages/mui-base/src/SliderUnstyled/index.js @@ -1,4 +1,5 @@ export { default } from './SliderUnstyled'; export { default as SliderValueLabelUnstyled } from './SliderValueLabelUnstyled'; export { default as sliderUnstyledClasses } from './sliderUnstyledClasses'; +export { default as useSlider } from './useSlider'; export * from './sliderUnstyledClasses'; diff --git a/packages/mui-base/src/SliderUnstyled/useSlider.tsx b/packages/mui-base/src/SliderUnstyled/useSlider.tsx new file mode 100644 index 00000000000000..10fe1ba08d3436 --- /dev/null +++ b/packages/mui-base/src/SliderUnstyled/useSlider.tsx @@ -0,0 +1,657 @@ +import * as React from 'react'; +import { + unstable_useIsFocusVisible as useIsFocusVisible, + unstable_useEnhancedEffect as useEnhancedEffect, + unstable_ownerDocument as ownerDocument, + unstable_useEventCallback as useEventCallback, + unstable_useForkRef as useForkRef, + unstable_useControlled as useControlled, + visuallyHidden, +} from '@mui/utils'; +import { SliderUnstyledProps, Mark } from './SliderUnstyled'; + +interface UseSliderProps extends SliderUnstyledProps {} + +const INTENTIONAL_DRAG_COUNT_THRESHOLD = 2; + +function asc(a: number, b: number) { + return a - b; +} + +function clamp(value: number, min: number, max: number) { + if (value == null) { + return min; + } + return Math.min(Math.max(min, value), max); +} + +function findClosest(values: number[], currentValue: number) { + const { index: closestIndex } = + values.reduce<{ distance: number; index: number } | null>( + (acc, value: number, index: number) => { + const distance = Math.abs(currentValue - value); + + if (acc === null || distance < acc.distance || distance === acc.distance) { + return { + distance, + index, + }; + } + + return acc; + }, + null, + ) ?? {}; + return closestIndex; +} + +// TODO: Set the correct event type +function trackFinger(event: any, touchId: React.RefObject) { + 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.current) { + return { + x: touch.clientX, + y: touch.clientY, + }; + } + } + + return false; + } + + return { + x: event.clientX, + y: event.clientY, + }; +} + +export function valueToPercent(value: number, min: number, max: number) { + return ((value - min) * 100) / (max - min); +} + +function percentToValue(percent: number, min: number, max: number) { + return (max - min) * percent + min; +} + +function getDecimalPrecision(num: number) { + // This handles the case when num is very small (0.00000001), js will turn this into 1e-8. + // When num is bigger than 1 or less than -1 it won't get converted to this notation so it's fine. + if (Math.abs(num) < 1) { + const parts = num.toExponential().split('e-'); + const matissaDecimalPart = parts[0].split('.')[1]; + return (matissaDecimalPart ? matissaDecimalPart.length : 0) + parseInt(parts[1], 10); + } + + const decimalPart = num.toString().split('.')[1]; + return decimalPart ? decimalPart.length : 0; +} + +function roundValueToStep(value: number, step: number, min: number) { + const nearest = Math.round((value - min) / step) * step + min; + return Number(nearest.toFixed(getDecimalPrecision(step))); +} + +function setValueIndex({ + values, + newValue, + index, +}: { + values: number[]; + newValue: number; + index: number; +}) { + const output = values.slice(); + output[index] = newValue; + return output.sort(asc); +} + +function focusThumb({ + sliderRef, + activeIndex, + setActive, +}: { + sliderRef: React.RefObject; + activeIndex: number; + setActive?: (num: number) => void; +}) { + const doc = ownerDocument(sliderRef.current); + if ( + !sliderRef.current?.contains(doc.activeElement) || + Number(doc?.activeElement?.getAttribute('data-index')) !== activeIndex + ) { + // @ts-ignore TODO: Property focus does not exists on type string + sliderRef.current?.querySelector(`[type="range"][data-index="${activeIndex}"]`).focus(); + } + + if (setActive) { + setActive(activeIndex); + } +} + +const axisProps = { + horizontal: { + offset: (percent: number) => ({ left: `${percent}%` }), + leap: (percent: number) => ({ width: `${percent}%` }), + }, + 'horizontal-reverse': { + offset: (percent: number) => ({ right: `${percent}%` }), + leap: (percent: number) => ({ width: `${percent}%` }), + }, + vertical: { + offset: (percent: number) => ({ bottom: `${percent}%` }), + leap: (percent: number) => ({ height: `${percent}%` }), + }, +}; + +export const Identity = (x: any) => x; + +// TODO: remove support for Safari < 13. +// https://caniuse.com/#search=touch-action +// +// Safari, on iOS, supports touch action since v13. +// Over 80% of the iOS phones are compatible +// in August 2020. +// Utilizing the CSS.supports method to check if touch-action is supported. +// Since CSS.supports is supported on all but Edge@12 and IE and touch-action +// is supported on both Edge@12 and IE if CSS.supports is not available that means that +// touch-action will be supported +let cachedSupportsTouchActionNone: any; +function doesSupportTouchActionNone() { + if (cachedSupportsTouchActionNone === undefined) { + if (typeof CSS !== 'undefined' && typeof CSS.supports === 'function') { + cachedSupportsTouchActionNone = CSS.supports('touch-action', 'none'); + } else { + cachedSupportsTouchActionNone = true; + } + } + return cachedSupportsTouchActionNone; +} + +export default function useSlider(props: UseSliderProps) { + const { + ref, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledby, + 'aria-valuetext': ariaValuetext, + className, + classes: classesProp, + defaultValue, + disableSwap = false, + disabled = false, + getAriaLabel, + getAriaValueText, + marks: marksProp = false, + max = 100, + min = 0, + name, + onChange, + onChangeCommitted, + onMouseDown, + orientation = 'horizontal', + scale = Identity, + step = 1, + tabIndex, + track = 'normal', + value: valueProp, + valueLabelDisplay = 'off', + valueLabelFormat = Identity, + isRtl = false, + components = {}, + componentsProps = {}, + ...other + } = props; + + 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 transferred when inversing a range slider. + const [active, setActive] = React.useState(-1); + const [open, setOpen] = React.useState(-1); + const [dragging, setDragging] = React.useState(false); + const moveCount = React.useRef(0); + + const [valueDerived, setValueState] = useControlled({ + controlled: valueProp, + default: defaultValue ?? min, + name: 'Slider', + }); + + const handleChange = + onChange && + (( + event: Event | React.SyntheticEvent, + value: SliderUnstyledProps['value'], + thumbIndex: number, + ) => { + // Redefine target to allow name and value to be read. + // This allows seamless integration with the most popular form libraries. + // https://github.com/mui-org/material-ui/issues/13485#issuecomment-676048492 + // Clone the event to not override `target` of the original event. + // @ts-ignore nativeEvent does not exists on Event + const nativeEvent = event.nativeEvent || event; + // @ts-ignore TODO: check this again + const clonedEvent = new nativeEvent.constructor(nativeEvent.type, nativeEvent); + + Object.defineProperty(clonedEvent, 'target', { + writable: true, + value: { value, name }, + }); + + // @ts-ignore TODO: value could be undefined? + onChange(clonedEvent, value, thumbIndex); + }); + + const range = Array.isArray(valueDerived); + let values = range ? valueDerived.slice().sort(asc) : [valueDerived]; + values = values.map((value) => clamp(value, min, max)); + const marks = + marksProp === true && step !== null + ? [...Array(Math.floor((max - min) / step) + 1)].map((_, index) => ({ + value: min + step * index, + })) + : marksProp || []; + + const marksValues = (marks as Mark[]).map((mark: Mark) => mark.value); + + const { + isFocusVisibleRef, + onBlur: handleBlurVisible, + onFocus: handleFocusVisible, + ref: focusVisibleRef, + } = useIsFocusVisible(); + const [focusVisible, setFocusVisible] = React.useState(-1); + + const sliderRef = React.useRef(); + const handleFocusRef = useForkRef(focusVisibleRef, sliderRef); + const handleRef = useForkRef(ref, handleFocusRef); + + const handleFocus = (event: React.FocusEvent) => { + const index = Number(event.currentTarget.getAttribute('data-index')); + handleFocusVisible(event); + if (isFocusVisibleRef.current === true) { + setFocusVisible(index); + } + setOpen(index); + }; + const handleBlur = (event: React.FocusEvent) => { + handleBlurVisible(event); + if (isFocusVisibleRef.current === false) { + setFocusVisible(-1); + } + setOpen(-1); + }; + const handleMouseOver = useEventCallback((event: React.MouseEvent) => { + const index = Number(event.currentTarget.getAttribute('data-index')); + setOpen(index); + }); + const handleMouseLeave = useEventCallback(() => { + setOpen(-1); + }); + + useEnhancedEffect(() => { + if (disabled && sliderRef.current!.contains(document.activeElement)) { + // This is necessary because Firefox and Safari will keep focus + // on a disabled element: + // https://codesandbox.io/s/mui-pr-22247-forked-h151h?file=/src/App.js + // @ts-ignore + document.activeElement?.blur(); + } + }, [disabled]); + + if (disabled && active !== -1) { + setActive(-1); + } + if (disabled && focusVisible !== -1) { + setFocusVisible(-1); + } + + const handleHiddenInputChange = (event: React.ChangeEvent) => { + const index = Number(event.currentTarget.getAttribute('data-index')); + const value = values[index]; + const marksIndex = marksValues.indexOf(value); + + // @ts-ignore + let newValue = event.target.valueAsNumber; + + if (marks && step == null) { + newValue = newValue < value ? marksValues[marksIndex - 1] : marksValues[marksIndex + 1]; + } + + newValue = clamp(newValue, min, max); + + if (marks && step == null) { + const currentMarkIndex = marksValues.indexOf(values[index]); + + newValue = + newValue < values[index] + ? marksValues[currentMarkIndex - 1] + : marksValues[currentMarkIndex + 1]; + } + + if (range) { + // Bound the new value to the thumb's neighbours. + if (disableSwap) { + newValue = clamp(newValue, values[index - 1] || -Infinity, values[index + 1] || Infinity); + } + + const previousValue = newValue; + newValue = setValueIndex({ + values, + newValue, + index, + }); + + let activeIndex = index; + + // Potentially swap the index if needed. + if (!disableSwap) { + activeIndex = newValue.indexOf(previousValue); + } + + focusThumb({ sliderRef, activeIndex }); + } + + setValueState(newValue); + setFocusVisible(index); + + if (handleChange) { + handleChange(event, newValue, index); + } + + if (onChangeCommitted) { + onChangeCommitted(event, newValue); + } + }; + + const previousIndex = React.useRef(); + let axis = orientation; + if (isRtl && orientation === 'horizontal') { + axis += '-reverse'; + } + + const getFingerNewValue = ({ + finger, + move = false, + values: values2, + }: { + finger: { x: number; y: number }; + move?: boolean; + values: number[]; + }) => { + const { current: slider } = sliderRef; + const { width, height, bottom, left } = slider!.getBoundingClientRect(); + let percent; + + if (axis.indexOf('vertical') === 0) { + percent = (bottom - finger.y) / height; + } else { + percent = (finger.x - left) / width; + } + + if (axis.indexOf('-reverse') !== -1) { + percent = 1 - percent; + } + + let newValue; + newValue = percentToValue(percent, min, max); + if (step) { + newValue = roundValueToStep(newValue, step, min); + } else { + const closestIndex = findClosest(marksValues, newValue); + newValue = marksValues[closestIndex!]; + } + + newValue = clamp(newValue, min, max); + let activeIndex = 0; + + if (range) { + if (!move) { + activeIndex = findClosest(values2, newValue)!; + } else { + activeIndex = previousIndex.current!; + } + + // Bound the new value to the thumb's neighbours. + if (disableSwap) { + newValue = clamp( + newValue, + values2[activeIndex - 1] || -Infinity, + values2[activeIndex + 1] || Infinity, + ); + } + + const previousValue = newValue; + newValue = setValueIndex({ + values: values2, + newValue, + index: activeIndex, + }); + + // Potentially swap the index if needed. + if (!(disableSwap && move)) { + activeIndex = newValue.indexOf(previousValue); + previousIndex.current = activeIndex; + } + } + + return { newValue, activeIndex }; + }; + + const handleTouchMove = useEventCallback((nativeEvent: TouchEvent | MouseEvent) => { + const finger = trackFinger(nativeEvent, touchId); + + if (!finger) { + return; + } + + moveCount.current += 1; + + // Cancel move in case some other element consumed a mouseup event and it was not fired. + // @ts-ignore buttons doesn't not exists on touch event + if (nativeEvent.type === 'mousemove' && nativeEvent.buttons === 0) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + handleTouchEnd(nativeEvent); + return; + } + + const { newValue, activeIndex } = getFingerNewValue({ + finger, + move: true, + values, + }); + + focusThumb({ sliderRef, activeIndex, setActive }); + setValueState(newValue); + + if (!dragging && moveCount.current > INTENTIONAL_DRAG_COUNT_THRESHOLD) { + setDragging(true); + } + + if (handleChange) { + handleChange(nativeEvent, newValue, activeIndex); + } + }); + + const handleTouchEnd = useEventCallback((nativeEvent: TouchEvent | MouseEvent) => { + const finger = trackFinger(nativeEvent, touchId); + setDragging(false); + + if (!finger) { + return; + } + + const { newValue } = getFingerNewValue({ finger, values }); + + setActive(-1); + if (nativeEvent.type === 'touchend') { + setOpen(-1); + } + + if (onChangeCommitted) { + onChangeCommitted(nativeEvent, newValue); + } + + touchId.current = undefined; + + // eslint-disable-next-line @typescript-eslint/no-use-before-define + stopListening(); + }); + + const handleTouchStart = useEventCallback((nativeEvent: TouchEvent) => { + // If touch-action: none; is not supported we need to prevent the scroll manually. + if (!doesSupportTouchActionNone()) { + nativeEvent.preventDefault(); + } + + const touch = nativeEvent.changedTouches[0]; + if (touch != null) { + // A number that uniquely identifies the current finger in the touch session. + touchId.current = touch.identifier; + } + const finger = trackFinger(nativeEvent, touchId); + if (finger !== false) { + const { newValue, activeIndex } = getFingerNewValue({ finger, values }); + focusThumb({ sliderRef, activeIndex, setActive }); + + setValueState(newValue); + + if (handleChange) { + handleChange(nativeEvent, newValue, activeIndex); + } + } + + moveCount.current = 0; + const doc = ownerDocument(sliderRef.current); + doc.addEventListener('touchmove', handleTouchMove); + doc.addEventListener('touchend', handleTouchEnd); + }); + + const stopListening = React.useCallback(() => { + const doc = ownerDocument(sliderRef.current); + doc.removeEventListener('mousemove', handleTouchMove); + doc.removeEventListener('mouseup', handleTouchEnd); + doc.removeEventListener('touchmove', handleTouchMove); + doc.removeEventListener('touchend', handleTouchEnd); + }, [handleTouchEnd, handleTouchMove]); + + React.useEffect(() => { + const { current: slider } = sliderRef; + slider!.addEventListener('touchstart', handleTouchStart, { + passive: doesSupportTouchActionNone(), + }); + + return () => { + // @ts-ignore + slider!.removeEventListener('touchstart', handleTouchStart, { + passive: doesSupportTouchActionNone(), + }); + + stopListening(); + }; + }, [stopListening, handleTouchStart]); + + React.useEffect(() => { + if (disabled) { + stopListening(); + } + }, [disabled, stopListening]); + + const handleMouseDown = useEventCallback( + (event: React.MouseEvent) => { + if (onMouseDown) { + onMouseDown(event); + } + + // Only handle left clicks + if (event.button !== 0) { + return; + } + + // Avoid text selection + event.preventDefault(); + const finger = trackFinger(event, touchId); + if (finger !== false) { + const { newValue, activeIndex } = getFingerNewValue({ finger, values }); + focusThumb({ sliderRef, activeIndex, setActive }); + + setValueState(newValue); + + if (handleChange) { + handleChange(event, newValue, activeIndex); + } + } + + moveCount.current = 0; + const doc = ownerDocument(sliderRef.current); + doc.addEventListener('mousemove', handleTouchMove); + doc.addEventListener('mouseup', handleTouchEnd); + }, + ); + + const trackOffset = valueToPercent(range ? values[0] : min, min, max); + const trackLeap = valueToPercent(values[values.length - 1], min, max) - trackOffset; + const trackStyle = { + // @ts-ignore + ...axisProps[axis].offset(trackOffset), + // @ts-ignore + ...axisProps[axis].leap(trackLeap), + }; + + const getRootProps = () => { + return { + ref: handleRef, + onMouseDown: handleMouseDown, + }; + }; + + const getTrackProps = () => { + return { + style: { ...trackStyle, ...componentsProps.track?.style }, + }; + }; + + const getHiddenInputProps = () => { + return { + tabIndex, + 'aria-labelledby': ariaLabelledby, + 'aria-orientation': orientation, + 'aria-valuemax': scale(max), + 'aria-valuemin': scale(min), + onFocus: handleFocus, + onBlur: handleBlur, + name, + type: 'range', + min: props.min, + max: props.max, + step: props.step, + disabled, + onChange: handleHiddenInputChange, + style: { + ...visuallyHidden, + direction: isRtl ? 'rtl' : 'ltr', + // So that VoiceOver's focus indicator matches the thumb's dimensions + width: '100%', + height: '100%', + }, + }; + }; + + return { + axis, + axisProps, + getRootProps, + getTrackProps, + getHiddenInputProps, + dragging, + marks, + values, + active, + handleMouseOver, + handleMouseLeave, + focusVisible, + range, + open, + }; +} From f47b01c24e7e2f46635e0674e14c1eab2f1c0498 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Tue, 7 Dec 2021 13:33:45 +0100 Subject: [PATCH 03/18] proptypes & docs:api --- docs/pages/api-docs/slider.json | 1 - docs/translations/api-docs/slider/slider.json | 3 +-- packages/mui-base/src/SliderUnstyled/SliderUnstyled.js | 4 +++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/pages/api-docs/slider.json b/docs/pages/api-docs/slider.json index e97dc95a96d625..6c705efe569abc 100644 --- a/docs/pages/api-docs/slider.json +++ b/docs/pages/api-docs/slider.json @@ -11,7 +11,6 @@ }, "default": "'primary'" }, - "component": { "type": { "name": "elementType" } }, "components": { "type": { "name": "shape", diff --git a/docs/translations/api-docs/slider/slider.json b/docs/translations/api-docs/slider/slider.json index 2eb768d99df918..e6fc0d9383f429 100644 --- a/docs/translations/api-docs/slider/slider.json +++ b/docs/translations/api-docs/slider/slider.json @@ -29,8 +29,7 @@ "track": "The track presentation:
- normal the track will render a bar representing the slider value. - inverted the track will render a bar representing the remaining slider value. - false the track will render without a bar.", "value": "The value of the slider. For ranged sliders, provide an array with two values.", "valueLabelDisplay": "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": "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", - "component": "The component used for the root node. Either a string to use a HTML element or a component." + "valueLabelFormat": "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" }, "classDescriptions": { "root": { "description": "Class name applied to the root element." }, diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index df01e1841b5295..eb73baf4279fa1 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -7,7 +7,9 @@ import isHostComponent from '../utils/isHostComponent'; import composeClasses from '../composeClasses'; import { getSliderUtilityClass } from './sliderUnstyledClasses'; import SliderValueLabelUnstyled from './SliderValueLabelUnstyled'; -import useSlider, { Identity, valueToPercent } from './useSlider'; +import useSlider, { valueToPercent } from './useSlider'; + +const Identity = (x) => x; const useUtilityClasses = (ownerState) => { const { disabled, dragging, marked, orientation, track, classes } = ownerState; From 086aa866286fd0d3b0fbbfd19771a37092a48876 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Wed, 8 Dec 2021 09:41:12 +0100 Subject: [PATCH 04/18] Improve logic for forwarding props --- .../src/SliderUnstyled/SliderUnstyled.js | 3 --- .../mui-base/src/utils/appendOwnerState.ts | 6 ++--- packages/mui-material/src/Slider/Slider.js | 24 +++++++++++++++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index eb73baf4279fa1..077c4896100e09 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -138,9 +138,6 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { diff --git a/packages/mui-base/src/utils/appendOwnerState.ts b/packages/mui-base/src/utils/appendOwnerState.ts index dd74ee4214bfc5..c1255ffa62eb7e 100644 --- a/packages/mui-base/src/utils/appendOwnerState.ts +++ b/packages/mui-base/src/utils/appendOwnerState.ts @@ -12,14 +12,12 @@ export default function appendOwnerState( existingProps: Record = {}, ownerState: object, ) { - const { ownerState: existingPropsOwnerState, ...otherExistingProps } = existingProps; - if (isHostComponent(elementType)) { - return otherExistingProps; + return existingProps; } return { ...existingProps, - ownerState: { ...existingPropsOwnerState, ...ownerState }, + ownerState: { ...existingProps.ownerState, ...ownerState }, }; } diff --git a/packages/mui-material/src/Slider/Slider.js b/packages/mui-material/src/Slider/Slider.js index 6830d0f2d8f3d4..f98cc2db6b11b7 100644 --- a/packages/mui-material/src/Slider/Slider.js +++ b/packages/mui-material/src/Slider/Slider.js @@ -2,7 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { chainPropTypes } from '@mui/utils'; -import { generateUtilityClasses } from '@mui/base'; +import { generateUtilityClasses, isHostComponent } from '@mui/base'; import SliderUnstyled, { SliderValueLabelUnstyled, sliderUnstyledClasses, @@ -417,6 +417,10 @@ const extendUtilityClasses = (ownerState) => { }; }; +const shouldSpreadAdditionalProps = (Component) => { + return !Component || !isHostComponent(Component); +}; + const Slider = React.forwardRef(function Slider(inputProps, ref) { const props = useThemeProps({ props: inputProps, name: 'MuiSlider' }); @@ -424,6 +428,7 @@ const Slider = React.forwardRef(function Slider(inputProps, ref) { const isRtl = theme.direction === 'rtl'; const { + component = 'span', components = {}, componentsProps = {}, color = 'primary', @@ -453,19 +458,28 @@ const Slider = React.forwardRef(function Slider(inputProps, ref) { ...componentsProps, root: { ...componentsProps.root, - ownerState: { ...componentsProps.root?.ownerState, color, size }, + ...(shouldSpreadAdditionalProps(components.Root) && { + as: component, + ownerState: { ...componentsProps.root?.ownerState, color, size }, + }), }, thumb: { ...componentsProps.thumb, - ownerState: { ...componentsProps.thumb?.ownerState, color, size }, + ...(shouldSpreadAdditionalProps(components.Thumb) && { + ownerState: { ...componentsProps.thumb?.ownerState, color, size }, + }), }, track: { ...componentsProps.track, - ownerState: { ...componentsProps.track?.ownerState, color, size }, + ...(shouldSpreadAdditionalProps(components.Track) && { + ownerState: { ...componentsProps.track?.ownerState, color, size }, + }), }, valueLabel: { ...componentsProps.valueLabel, - ownerState: { ...componentsProps.valueLabel?.ownerState, color, size }, + ...(shouldSpreadAdditionalProps(components.ValueLabel) && { + ownerState: { ...componentsProps.valueLabel?.ownerState, color, size }, + }), }, }} classes={classes} From 6f925a19e8f4afeca070f34009e53f43333dab66 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Wed, 8 Dec 2021 11:42:02 +0100 Subject: [PATCH 05/18] Various fixes --- .../src/SliderUnstyled/SliderUnstyled.js | 4 +- .../mui-base/src/SliderUnstyled/useSlider.tsx | 43 +++++++++++++------ packages/mui-material/src/Slider/Slider.js | 1 + 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index 077c4896100e09..25e5230fbc8d30 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import { chainPropTypes, visuallyHidden } from '@mui/utils'; +import { chainPropTypes } from '@mui/utils'; import appendOwnerState from '../utils/appendOwnerState'; import isHostComponent from '../utils/isHostComponent'; import composeClasses from '../composeClasses'; @@ -78,6 +78,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { // consider extracting to hook an reusing the lint rule for the varints const ownerState = { ...props, + mark: marksProp, classes: classesProp, disabled, isRtl, @@ -240,6 +241,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { ...thumbProps.style, }} > + {/* eslint-disable-next-line jsx-a11y/role-supports-aria-props */} ; + 'aria-labelledby'?: string; + componentsProps?: { + track?: { style?: React.CSSProperties }; + }; + defaultValue?: number | number[]; + disabled?: boolean; + disableSwap?: boolean; + isRtl?: boolean; + marks?: boolean | Mark[]; + max?: number; + min?: number; + name?: string; + onChange?: (event: Event, value: number | number[], activeThumb: number) => void; + onMouseDown?: (event: React.MouseEvent) => void; + onChangeCommitted?: (event: React.SyntheticEvent | Event, value: number | number[]) => void; + orientation?: 'horizontal' | 'vertical'; + scale?: (value: number) => number; + step?: number | null; + tabIndex?: number; + value?: number | number[]; +} const INTENTIONAL_DRAG_COUNT_THRESHOLD = 2; @@ -172,16 +198,10 @@ function doesSupportTouchActionNone() { export default function useSlider(props: UseSliderProps) { const { ref, - 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, - 'aria-valuetext': ariaValuetext, - className, - classes: classesProp, defaultValue, disableSwap = false, disabled = false, - getAriaLabel, - getAriaValueText, marks: marksProp = false, max = 100, min = 0, @@ -193,14 +213,9 @@ export default function useSlider(props: UseSliderProps) { scale = Identity, step = 1, tabIndex, - track = 'normal', value: valueProp, - valueLabelDisplay = 'off', - valueLabelFormat = Identity, isRtl = false, - components = {}, componentsProps = {}, - ...other } = props; const touchId = React.useRef(); @@ -222,7 +237,7 @@ export default function useSlider(props: UseSliderProps) { onChange && (( event: Event | React.SyntheticEvent, - value: SliderUnstyledProps['value'], + value: number | number[], thumbIndex: number, ) => { // Redefine target to allow name and value to be read. diff --git a/packages/mui-material/src/Slider/Slider.js b/packages/mui-material/src/Slider/Slider.js index f98cc2db6b11b7..30ca9055c51ea6 100644 --- a/packages/mui-material/src/Slider/Slider.js +++ b/packages/mui-material/src/Slider/Slider.js @@ -428,6 +428,7 @@ const Slider = React.forwardRef(function Slider(inputProps, ref) { const isRtl = theme.direction === 'rtl'; const { + // eslint-disable-next-line react/prop-types component = 'span', components = {}, componentsProps = {}, From d3c0033083c6fc33ca1112f632f8649052e466d3 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Wed, 8 Dec 2021 12:43:16 +0100 Subject: [PATCH 06/18] prettier --- packages/mui-base/src/SliderUnstyled/useSlider.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/useSlider.tsx b/packages/mui-base/src/SliderUnstyled/useSlider.tsx index b86b73b0cf8fb3..d0270e4ff03948 100644 --- a/packages/mui-base/src/SliderUnstyled/useSlider.tsx +++ b/packages/mui-base/src/SliderUnstyled/useSlider.tsx @@ -235,11 +235,7 @@ export default function useSlider(props: UseSliderProps) { const handleChange = onChange && - (( - event: Event | React.SyntheticEvent, - value: number | number[], - thumbIndex: number, - ) => { + ((event: Event | React.SyntheticEvent, value: number | number[], thumbIndex: number) => { // Redefine target to allow name and value to be read. // This allows seamless integration with the most popular form libraries. // https://github.com/mui-org/material-ui/issues/13485#issuecomment-676048492 From 26287cc47414312a553999107806f6016a71f51e Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Wed, 8 Dec 2021 13:15:30 +0100 Subject: [PATCH 07/18] Update test/utils/describeConformanceUnstyled.tsx --- test/utils/describeConformanceUnstyled.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/describeConformanceUnstyled.tsx b/test/utils/describeConformanceUnstyled.tsx index d61033bcf2c68c..e54f476137f16a 100644 --- a/test/utils/describeConformanceUnstyled.tsx +++ b/test/utils/describeConformanceUnstyled.tsx @@ -42,7 +42,7 @@ interface WithClassName { interface WithCustomProp { fooBar: string; - role: number; + role: string } interface WithOwnerState { From bd62bb241d91b46c3543b3892769d69c1d36f5b4 Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Wed, 8 Dec 2021 19:30:07 +0000 Subject: [PATCH 08/18] Run prettier --- test/utils/describeConformanceUnstyled.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/describeConformanceUnstyled.tsx b/test/utils/describeConformanceUnstyled.tsx index e54f476137f16a..2824b0eac527eb 100644 --- a/test/utils/describeConformanceUnstyled.tsx +++ b/test/utils/describeConformanceUnstyled.tsx @@ -42,7 +42,7 @@ interface WithClassName { interface WithCustomProp { fooBar: string; - role: string + role: string; } interface WithOwnerState { From d7607ed9c65abf5a3692d218c1a1337346e4bd21 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Fri, 10 Dec 2021 12:40:24 +0100 Subject: [PATCH 09/18] add handlers in the callback for the props --- .../mui-base/src/SliderUnstyled/useSlider.tsx | 161 ++++++++++-------- packages/mui-material/src/Badge/Badge.js | 7 +- packages/mui-material/src/Slider/Slider.js | 7 +- .../src/utils/shouldSpreadAdditionalProps.js | 7 + 4 files changed, 105 insertions(+), 77 deletions(-) create mode 100644 packages/mui-material/src/utils/shouldSpreadAdditionalProps.js diff --git a/packages/mui-base/src/SliderUnstyled/useSlider.tsx b/packages/mui-base/src/SliderUnstyled/useSlider.tsx index d0270e4ff03948..193a24393e8c0f 100644 --- a/packages/mui-base/src/SliderUnstyled/useSlider.tsx +++ b/packages/mui-base/src/SliderUnstyled/useSlider.tsx @@ -8,6 +8,7 @@ import { unstable_useControlled as useControlled, visuallyHidden, } from '@mui/utils'; +import extractEventHandlers from '../utils/extractEventHandlers'; interface Mark { value: number; @@ -278,21 +279,25 @@ export default function useSlider(props: UseSliderProps) { const handleFocusRef = useForkRef(focusVisibleRef, sliderRef); const handleRef = useForkRef(ref, handleFocusRef); - const handleFocus = (event: React.FocusEvent) => { - const index = Number(event.currentTarget.getAttribute('data-index')); - handleFocusVisible(event); - if (isFocusVisibleRef.current === true) { - setFocusVisible(index); - } - setOpen(index); - }; - const handleBlur = (event: React.FocusEvent) => { - handleBlurVisible(event); - if (isFocusVisibleRef.current === false) { - setFocusVisible(-1); - } - setOpen(-1); - }; + const createHandleHiddenInputFocus = + (otherHandlers: Record>) => (event: React.FocusEvent) => { + const index = Number(event.currentTarget.getAttribute('data-index')); + handleFocusVisible(event); + if (isFocusVisibleRef.current === true) { + setFocusVisible(index); + } + setOpen(index); + otherHandlers?.onFocus?.(event); + }; + const createHandleHidenInputBlur = + (otherHandlers: Record>) => (event: React.FocusEvent) => { + handleBlurVisible(event); + if (isFocusVisibleRef.current === false) { + setFocusVisible(-1); + } + setOpen(-1); + otherHandlers?.onBlur?.(event); + }; const handleMouseOver = useEventCallback((event: React.MouseEvent) => { const index = Number(event.currentTarget.getAttribute('data-index')); setOpen(index); @@ -318,63 +323,66 @@ export default function useSlider(props: UseSliderProps) { setFocusVisible(-1); } - const handleHiddenInputChange = (event: React.ChangeEvent) => { - const index = Number(event.currentTarget.getAttribute('data-index')); - const value = values[index]; - const marksIndex = marksValues.indexOf(value); + const createHandleHiddenInputChange = + (otherHandlers: Record>) => (event: React.ChangeEvent) => { + const index = Number(event.currentTarget.getAttribute('data-index')); + const value = values[index]; + const marksIndex = marksValues.indexOf(value); - // @ts-ignore - let newValue = event.target.valueAsNumber; - - if (marks && step == null) { - newValue = newValue < value ? marksValues[marksIndex - 1] : marksValues[marksIndex + 1]; - } + // @ts-ignore + let newValue = event.target.valueAsNumber; - newValue = clamp(newValue, min, max); + if (marks && step == null) { + newValue = newValue < value ? marksValues[marksIndex - 1] : marksValues[marksIndex + 1]; + } - if (marks && step == null) { - const currentMarkIndex = marksValues.indexOf(values[index]); + newValue = clamp(newValue, min, max); - newValue = - newValue < values[index] - ? marksValues[currentMarkIndex - 1] - : marksValues[currentMarkIndex + 1]; - } + if (marks && step == null) { + const currentMarkIndex = marksValues.indexOf(values[index]); - if (range) { - // Bound the new value to the thumb's neighbours. - if (disableSwap) { - newValue = clamp(newValue, values[index - 1] || -Infinity, values[index + 1] || Infinity); + newValue = + newValue < values[index] + ? marksValues[currentMarkIndex - 1] + : marksValues[currentMarkIndex + 1]; } - const previousValue = newValue; - newValue = setValueIndex({ - values, - newValue, - index, - }); + if (range) { + // Bound the new value to the thumb's neighbours. + if (disableSwap) { + newValue = clamp(newValue, values[index - 1] || -Infinity, values[index + 1] || Infinity); + } - let activeIndex = index; + const previousValue = newValue; + newValue = setValueIndex({ + values, + newValue, + index, + }); - // Potentially swap the index if needed. - if (!disableSwap) { - activeIndex = newValue.indexOf(previousValue); + let activeIndex = index; + + // Potentially swap the index if needed. + if (!disableSwap) { + activeIndex = newValue.indexOf(previousValue); + } + + focusThumb({ sliderRef, activeIndex }); } - focusThumb({ sliderRef, activeIndex }); - } + setValueState(newValue); + setFocusVisible(index); - setValueState(newValue); - setFocusVisible(index); + if (handleChange) { + handleChange(event, newValue, index); + } - if (handleChange) { - handleChange(event, newValue, index); - } + if (onChangeCommitted) { + onChangeCommitted(event, newValue); + } - if (onChangeCommitted) { - onChangeCommitted(event, newValue); - } - }; + otherHandlers.onChange?.(event); + }; const previousIndex = React.useRef(); let axis = orientation; @@ -569,7 +577,8 @@ export default function useSlider(props: UseSliderProps) { } }, [disabled, stopListening]); - const handleMouseDown = useEventCallback( + const createHandleMouseDown = + (otherHandlers: Record>) => (event: React.MouseEvent) => { if (onMouseDown) { onMouseDown(event); @@ -598,8 +607,9 @@ export default function useSlider(props: UseSliderProps) { const doc = ownerDocument(sliderRef.current); doc.addEventListener('mousemove', handleTouchMove); doc.addEventListener('mouseup', handleTouchEnd); - }, - ); + + otherHandlers.onMouseDown?.(event); + }; const trackOffset = valueToPercent(range ? values[0] : min, min, max); const trackLeap = valueToPercent(values[values.length - 1], min, max) - trackOffset; @@ -610,10 +620,18 @@ export default function useSlider(props: UseSliderProps) { ...axisProps[axis].leap(trackLeap), }; - const getRootProps = () => { + const getRootProps = (otherHandlers?: Record>) => { + const ownEventHandlers = { + onMouseDown: createHandleMouseDown(otherHandlers || {}), + }; + + const mergedEventHandlers: Record> = { + ...otherHandlers, + ...ownEventHandlers, + }; return { ref: handleRef, - onMouseDown: handleMouseDown, + ...mergedEventHandlers, }; }; @@ -623,22 +641,31 @@ export default function useSlider(props: UseSliderProps) { }; }; - const getHiddenInputProps = () => { + const getHiddenInputProps = (otherHandlers?: Record>) => { + const ownEventHandlers = { + onChange: createHandleHiddenInputChange(otherHandlers || {}), + onFocus: createHandleHiddenInputFocus(otherHandlers || {}), + onBlur: createHandleHidenInputBlur(otherHandlers || {}), + }; + + const mergedEventHandlers: Record> = { + ...otherHandlers, + ...ownEventHandlers, + }; + return { tabIndex, 'aria-labelledby': ariaLabelledby, 'aria-orientation': orientation, 'aria-valuemax': scale(max), 'aria-valuemin': scale(min), - onFocus: handleFocus, - onBlur: handleBlur, name, type: 'range', min: props.min, max: props.max, step: props.step, disabled, - onChange: handleHiddenInputChange, + ...mergedEventHandlers, style: { ...visuallyHidden, direction: isRtl ? 'rtl' : 'ltr', diff --git a/packages/mui-material/src/Badge/Badge.js b/packages/mui-material/src/Badge/Badge.js index c670f55e6f1033..b220997abdb351 100644 --- a/packages/mui-material/src/Badge/Badge.js +++ b/packages/mui-material/src/Badge/Badge.js @@ -2,10 +2,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { usePreviousProps } from '@mui/utils'; -import { generateUtilityClasses, isHostComponent } from '@mui/base'; +import { generateUtilityClasses } from '@mui/base'; import BadgeUnstyled, { badgeUnstyledClasses, getBadgeUtilityClass } from '@mui/base/BadgeUnstyled'; import styled from '../styles/styled'; import useThemeProps from '../styles/useThemeProps'; +import shouldSpreadAdditionalProps from '../utils/shouldSpreadAdditionalProps'; import capitalize from '../utils/capitalize'; export const badgeClasses = { @@ -193,10 +194,6 @@ const BadgeBadge = styled('span', { }), })); -const shouldSpreadAdditionalProps = (Slot) => { - return !Slot || !isHostComponent(Slot); -}; - const Badge = React.forwardRef(function Badge(inProps, ref) { const props = useThemeProps({ props: inProps, name: 'MuiBadge' }); const { diff --git a/packages/mui-material/src/Slider/Slider.js b/packages/mui-material/src/Slider/Slider.js index 30ca9055c51ea6..46a0cc3e4f0988 100644 --- a/packages/mui-material/src/Slider/Slider.js +++ b/packages/mui-material/src/Slider/Slider.js @@ -2,7 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { chainPropTypes } from '@mui/utils'; -import { generateUtilityClasses, isHostComponent } from '@mui/base'; +import { generateUtilityClasses } from '@mui/base'; import SliderUnstyled, { SliderValueLabelUnstyled, sliderUnstyledClasses, @@ -12,6 +12,7 @@ import { alpha, lighten, darken } from '@mui/system'; import useThemeProps from '../styles/useThemeProps'; import styled, { slotShouldForwardProp } from '../styles/styled'; import useTheme from '../styles/useTheme'; +import shouldSpreadAdditionalProps from '../utils/shouldSpreadAdditionalProps'; import capitalize from '../utils/capitalize'; export const sliderClasses = { @@ -417,10 +418,6 @@ const extendUtilityClasses = (ownerState) => { }; }; -const shouldSpreadAdditionalProps = (Component) => { - return !Component || !isHostComponent(Component); -}; - const Slider = React.forwardRef(function Slider(inputProps, ref) { const props = useThemeProps({ props: inputProps, name: 'MuiSlider' }); diff --git a/packages/mui-material/src/utils/shouldSpreadAdditionalProps.js b/packages/mui-material/src/utils/shouldSpreadAdditionalProps.js new file mode 100644 index 00000000000000..751ce4ee7aef79 --- /dev/null +++ b/packages/mui-material/src/utils/shouldSpreadAdditionalProps.js @@ -0,0 +1,7 @@ +import { isHostComponent } from '@mui/base'; + +const shouldSpreadAdditionalProps = (Slot) => { + return !Slot || !isHostComponent(Slot); +}; + +export default shouldSpreadAdditionalProps; From d81b1c8d9a206e023de806cc5811dd727cdb004e Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Fri, 10 Dec 2021 13:09:29 +0100 Subject: [PATCH 10/18] Remove unused imports --- packages/mui-base/src/SliderUnstyled/useSlider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/mui-base/src/SliderUnstyled/useSlider.tsx b/packages/mui-base/src/SliderUnstyled/useSlider.tsx index 193a24393e8c0f..27f16ef5ac4e82 100644 --- a/packages/mui-base/src/SliderUnstyled/useSlider.tsx +++ b/packages/mui-base/src/SliderUnstyled/useSlider.tsx @@ -8,7 +8,6 @@ import { unstable_useControlled as useControlled, visuallyHidden, } from '@mui/utils'; -import extractEventHandlers from '../utils/extractEventHandlers'; interface Mark { value: number; From 662f1cde4f3050943038e65be50594b4f3dd0b0b Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Fri, 17 Dec 2021 06:53:22 +0100 Subject: [PATCH 11/18] Michal's review --- .../src/SliderUnstyled/SliderUnstyled.js | 10 ++++++++-- .../{useSlider.tsx => useSlider.ts} | 17 +++-------------- 2 files changed, 11 insertions(+), 16 deletions(-) rename packages/mui-base/src/SliderUnstyled/{useSlider.tsx => useSlider.ts} (98%) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index 25e5230fbc8d30..7e0e91e376fcea 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -107,6 +107,8 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { dragging, marks, values, + trackOffset, + trackLeap, } = useSlider({ ...ownerState, ref }); ownerState.marked = marks.length > 0 && marks.some((mark) => mark.label); @@ -120,9 +122,13 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { const Track = components.Track ?? 'span'; const trackProps = appendOwnerState(Track, componentsProps.track, ownerState); + const trackStyle = { + ...axisProps[axis].offset(trackOffset), + ...axisProps[axis].leap(trackLeap), + }; const Thumb = components.Thumb ?? 'span'; - const thumbProps = componentsProps.thumb || {}; + const thumbProps = appendOwnerState(Thumb, componentsProps.thumb, ownerState); const ValueLabel = components.ValueLabel ?? SliderValueLabelUnstyled; const valueLabelProps = appendOwnerState(ValueLabel, componentsProps.valueLabel, ownerState); @@ -147,7 +153,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { {...getTrackProps()} {...trackProps} className={clsx(classes.track, trackProps.className)} - style={{ ...getTrackProps().style, ...trackProps.style }} + style={{ trackStyle, ...trackProps.style }} /> {marks.map((mark, index) => { const percent = valueToPercent(mark.value, min, max); diff --git a/packages/mui-base/src/SliderUnstyled/useSlider.tsx b/packages/mui-base/src/SliderUnstyled/useSlider.ts similarity index 98% rename from packages/mui-base/src/SliderUnstyled/useSlider.tsx rename to packages/mui-base/src/SliderUnstyled/useSlider.ts index 27f16ef5ac4e82..7caadf9eac929b 100644 --- a/packages/mui-base/src/SliderUnstyled/useSlider.tsx +++ b/packages/mui-base/src/SliderUnstyled/useSlider.ts @@ -612,12 +612,6 @@ export default function useSlider(props: UseSliderProps) { const trackOffset = valueToPercent(range ? values[0] : min, min, max); const trackLeap = valueToPercent(values[values.length - 1], min, max) - trackOffset; - const trackStyle = { - // @ts-ignore - ...axisProps[axis].offset(trackOffset), - // @ts-ignore - ...axisProps[axis].leap(trackLeap), - }; const getRootProps = (otherHandlers?: Record>) => { const ownEventHandlers = { @@ -634,12 +628,6 @@ export default function useSlider(props: UseSliderProps) { }; }; - const getTrackProps = () => { - return { - style: { ...trackStyle, ...componentsProps.track?.style }, - }; - }; - const getHiddenInputProps = (otherHandlers?: Record>) => { const ownEventHandlers = { onChange: createHandleHiddenInputChange(otherHandlers || {}), @@ -679,7 +667,6 @@ export default function useSlider(props: UseSliderProps) { axis, axisProps, getRootProps, - getTrackProps, getHiddenInputProps, dragging, marks, @@ -688,7 +675,9 @@ export default function useSlider(props: UseSliderProps) { handleMouseOver, handleMouseLeave, focusVisible, - range, open, + range, + trackOffset, + trackLeap, }; } From 80fd507152e99f2c2c0c7b6c81604f264facf99c Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Fri, 17 Dec 2021 07:13:55 +0100 Subject: [PATCH 12/18] Update the conformance test, remove getTrackProps usages --- .../mui-base/src/SliderUnstyled/SliderUnstyled.js | 2 -- test/utils/describeConformanceUnstyled.tsx | 14 +++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index 7e0e91e376fcea..883df5e6e4803e 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -95,7 +95,6 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { const { axisProps, getRootProps, - getTrackProps, getHiddenInputProps, open, active, @@ -150,7 +149,6 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { > { const CustomRoot = React.forwardRef( - ({ fooBar, role }: WithCustomProp, ref: React.ForwardedRef) => { + ({ fooBar, lang }: WithCustomProp, ref: React.ForwardedRef) => { // @ts-ignore - return ; + return ; }, ); const otherProps = { - role: 'button', + lang: 'fr', fooBar: randomStringValue(), }; @@ -90,13 +90,13 @@ function testPropForwarding( React.cloneElement(element, { components: { Root: CustomRoot }, ...otherProps }), ); - expect(container.firstChild).to.have.attribute('role', otherProps.role.toString()); + expect(container.firstChild).to.have.attribute('lang', otherProps.lang); expect(container.firstChild).to.have.attribute('data-foobar', otherProps.fooBar); }); it('does forward standard props to the root element if an intrinsic element is provided', () => { const otherProps = { - role: 'button', + lang: 'fr', 'data-foobar': randomStringValue(), }; @@ -104,7 +104,7 @@ function testPropForwarding( React.cloneElement(element, { components: { Root: Element }, ...otherProps }), ); - expect(container.firstChild).to.have.attribute('role', otherProps.role); + expect(container.firstChild).to.have.attribute('lang', otherProps.lang); expect(container.firstChild).to.have.attribute('data-foobar', otherProps['data-foobar']); }); } From 967978a5ed963cfbcc539dbd519b6e73a1fa79f5 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Fri, 17 Dec 2021 12:32:16 +0100 Subject: [PATCH 13/18] Spread trackStyle --- packages/mui-base/src/SliderUnstyled/SliderUnstyled.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index 883df5e6e4803e..3fa0a2eb2980f9 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -151,7 +151,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { {marks.map((mark, index) => { const percent = valueToPercent(mark.value, min, max); From 3cf779a27cb31beaa3cf70c3fc04a63fd17349c0 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Fri, 17 Dec 2021 13:24:06 +0100 Subject: [PATCH 14/18] Various fixes --- .../src/SliderUnstyled/SliderUnstyled.d.ts | 218 +----------------- .../src/SliderUnstyled/SliderUnstyledProps.ts | 218 ++++++++++++++++++ .../mui-base/src/SliderUnstyled/index.d.ts | 3 + .../mui-base/src/SliderUnstyled/useSlider.ts | 39 ++-- packages/mui-material/src/Slider/Slider.js | 132 +++++++---- scripts/generateProptypes.ts | 4 +- 6 files changed, 332 insertions(+), 282 deletions(-) create mode 100644 packages/mui-base/src/SliderUnstyled/SliderUnstyledProps.ts diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.d.ts b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.d.ts index f127ad3139bd7a..ffd3d1bd17fe06 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.d.ts +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.d.ts @@ -1,214 +1,5 @@ -import { OverridableComponent, OverridableTypeMap, OverrideProps } from '@mui/types'; -import { SliderUnstyledClasses } from './sliderUnstyledClasses'; -import SliderValueLabelUnstyled from './SliderValueLabelUnstyled'; - -export interface SliderOwnerStateOverrides {} - -export interface Mark { - value: number; - label?: React.ReactNode; -} - -export interface ValueLabelProps extends React.HTMLAttributes { - children: React.ReactElement; - index: number; - open: boolean; - value: number; -} - -export interface SliderUnstyledComponentsPropsOverrides {} - -export interface SliderUnstyledTypeMap

{ - props: P & { - /** - * The label of the slider. - */ - 'aria-label'?: string; - /** - * The id of the element containing a label for the slider. - */ - 'aria-labelledby'?: string; - /** - * A string value that provides a user-friendly name for the current value of the slider. - */ - 'aria-valuetext'?: string; - /** - * Override or extend the styles applied to the component. - */ - classes?: Partial; - /** - * The components used for each slot inside the Slider. - * Either a string to use a HTML element or a component. - * @default {} - */ - components?: { - Root?: React.ElementType; - Track?: React.ElementType; - Rail?: React.ElementType; - Thumb?: React.ElementType; - Mark?: React.ElementType; - MarkLabel?: React.ElementType; - ValueLabel?: React.ElementType; - }; - /** - * The props used for each slot inside the Slider. - * @default {} - */ - componentsProps?: { - root?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; - track?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; - rail?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; - thumb?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; - mark?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; - markLabel?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; - valueLabel?: React.ComponentPropsWithRef & - SliderUnstyledComponentsPropsOverrides; - }; - /** - * The default value. Use when the component is not controlled. - */ - defaultValue?: number | number[]; - /** - * If `true`, the component is disabled. - * @default false - */ - disabled?: boolean; - /** - * If `true`, the active thumb doesn't swap when moving pointer over a thumb while dragging another thumb. - * @default false - */ - disableSwap?: boolean; - /** - * Accepts a function which returns a string value that provides a user-friendly name for the thumb labels of the slider. - * This is important for screen reader users. - * @param {number} index The thumb label's index to format. - * @returns {string} - */ - getAriaLabel?: (index: number) => string; - /** - * Accepts a function which returns a string value that provides a user-friendly name for the current value of the slider. - * This is important for screen reader users. - * @param {number} value The thumb label's value to format. - * @param {number} index The thumb label's index to format. - * @returns {string} - */ - getAriaValueText?: (value: number, index: number) => string; - /** - * Indicates whether the theme context has rtl direction. It is set automatically. - * @default false - */ - isRtl?: boolean; - /** - * Marks indicate predetermined values to which the user can move the slider. - * If `true` the marks are spaced according the value of the `step` prop. - * If an array, it should contain objects with `value` and an optional `label` keys. - * @default false - */ - marks?: boolean | Mark[]; - /** - * The maximum allowed value of the slider. - * Should not be equal to min. - * @default 100 - */ - max?: number; - /** - * The minimum allowed value of the slider. - * Should not be equal to max. - * @default 0 - */ - min?: number; - /** - * Name attribute of the hidden `input` element. - */ - name?: string; - /** - * Callback function that is fired when the slider's value changed. - * - * @param {Event} event The event source of the callback. - * You can pull out the new value by accessing `event.target.value` (any). - * **Warning**: This is a generic event not a change event. - * @param {number | number[]} value The new value. - * @param {number} activeThumb Index of the currently moved thumb. - */ - onChange?: (event: Event, value: number | number[], activeThumb: number) => void; - /** - * Callback function that is fired when the `mouseup` is triggered. - * - * @param {React.SyntheticEvent | Event} event The event source of the callback. **Warning**: This is a generic event not a change event. - * @param {number | number[]} value The new value. - */ - onChangeCommitted?: (event: React.SyntheticEvent | Event, value: number | number[]) => void; - /** - * The component orientation. - * @default 'horizontal' - */ - orientation?: 'horizontal' | 'vertical'; - /** - * A transformation function, to change the scale of the slider. - * @default (x) => x - */ - scale?: (value: number) => number; - /** - * The granularity with which the slider can step through values. (A "discrete" slider.) - * The `min` prop serves as the origin for the valid values. - * We recommend (max - min) to be evenly divisible by the step. - * - * When step is `null`, the thumb can only be slid onto marks provided with the `marks` prop. - * @default 1 - */ - step?: number | null; - /** - * Tab index attribute of the hidden `input` element. - */ - tabIndex?: number; - /** - * The track presentation: - * - * - `normal` the track will render a bar representing the slider value. - * - `inverted` the track will render a bar representing the remaining slider value. - * - `false` the track will render without a bar. - * @default 'normal' - */ - track?: 'normal' | false | 'inverted'; - /** - * The value of the slider. - * For ranged sliders, provide an array with two values. - */ - value?: number | number[]; - /** - * 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. - * @default 'off' - */ - valueLabelDisplay?: 'on' | 'auto' | 'off'; - /** - * 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 - * @default (x) => x - */ - valueLabelFormat?: string | ((value: number, index: number) => React.ReactNode); - }; - defaultComponent: D; -} - -/** - * Utility to create component types that inherit props from SliderUnstyled. - */ -export interface ExtendSliderUnstyledTypeMap { - props: M['props'] & SliderUnstyledTypeMap['props']; - defaultComponent: M['defaultComponent']; -} - -export type ExtendSliderUnstyled = OverridableComponent< - ExtendSliderUnstyledTypeMap ->; +import { OverridableComponent } from '@mui/types'; +import { SliderUnstyledTypeMap } from './SliderUnstyledProps'; /** * @@ -222,9 +13,4 @@ export type ExtendSliderUnstyled = OverridableComp */ declare const SliderUnstyled: OverridableComponent; -export type SliderUnstyledProps< - D extends React.ElementType = SliderUnstyledTypeMap['defaultComponent'], - P = {}, -> = OverrideProps, D>; - export default SliderUnstyled; diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyledProps.ts b/packages/mui-base/src/SliderUnstyled/SliderUnstyledProps.ts new file mode 100644 index 00000000000000..3c8740503373d1 --- /dev/null +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyledProps.ts @@ -0,0 +1,218 @@ +import { OverridableComponent, OverridableTypeMap, OverrideProps } from '@mui/types'; +import { SliderUnstyledClasses } from './sliderUnstyledClasses'; +import SliderValueLabelUnstyled from './SliderValueLabelUnstyled'; + +export interface SliderOwnerStateOverrides {} + +export interface Mark { + value: number; + label?: React.ReactNode; +} + +export interface ValueLabelProps extends React.HTMLAttributes { + children: React.ReactElement; + index: number; + open: boolean; + value: number; +} + +export interface SliderUnstyledComponentsPropsOverrides {} + +export interface SliderUnstyledTypeMap

{ + props: P & { + /** + * The label of the slider. + */ + 'aria-label'?: string; + /** + * The id of the element containing a label for the slider. + */ + 'aria-labelledby'?: string; + /** + * A string value that provides a user-friendly name for the current value of the slider. + */ + 'aria-valuetext'?: string; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * The components used for each slot inside the Slider. + * Either a string to use a HTML element or a component. + * @default {} + */ + components?: { + Root?: React.ElementType; + Track?: React.ElementType; + Rail?: React.ElementType; + Thumb?: React.ElementType; + Mark?: React.ElementType; + MarkLabel?: React.ElementType; + ValueLabel?: React.ElementType; + }; + /** + * The props used for each slot inside the Slider. + * @default {} + */ + componentsProps?: { + root?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; + track?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; + rail?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; + thumb?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; + mark?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; + markLabel?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; + valueLabel?: React.ComponentPropsWithRef & + SliderUnstyledComponentsPropsOverrides; + }; + /** + * The default value. Use when the component is not controlled. + */ + defaultValue?: number | number[]; + /** + * If `true`, the component is disabled. + * @default false + */ + disabled?: boolean; + /** + * If `true`, the active thumb doesn't swap when moving pointer over a thumb while dragging another thumb. + * @default false + */ + disableSwap?: boolean; + /** + * Accepts a function which returns a string value that provides a user-friendly name for the thumb labels of the slider. + * This is important for screen reader users. + * @param {number} index The thumb label's index to format. + * @returns {string} + */ + getAriaLabel?: (index: number) => string; + /** + * Accepts a function which returns a string value that provides a user-friendly name for the current value of the slider. + * This is important for screen reader users. + * @param {number} value The thumb label's value to format. + * @param {number} index The thumb label's index to format. + * @returns {string} + */ + getAriaValueText?: (value: number, index: number) => string; + /** + * Indicates whether the theme context has rtl direction. It is set automatically. + * @default false + */ + isRtl?: boolean; + /** + * Marks indicate predetermined values to which the user can move the slider. + * If `true` the marks are spaced according the value of the `step` prop. + * If an array, it should contain objects with `value` and an optional `label` keys. + * @default false + */ + marks?: boolean | Mark[]; + /** + * The maximum allowed value of the slider. + * Should not be equal to min. + * @default 100 + */ + max?: number; + /** + * The minimum allowed value of the slider. + * Should not be equal to max. + * @default 0 + */ + min?: number; + /** + * Name attribute of the hidden `input` element. + */ + name?: string; + /** + * Callback function that is fired when the slider's value changed. + * + * @param {Event} event The event source of the callback. + * You can pull out the new value by accessing `event.target.value` (any). + * **Warning**: This is a generic event not a change event. + * @param {number | number[]} value The new value. + * @param {number} activeThumb Index of the currently moved thumb. + */ + onChange?: (event: Event, value: number | number[], activeThumb: number) => void; + /** + * Callback function that is fired when the `mouseup` is triggered. + * + * @param {React.SyntheticEvent | Event} event The event source of the callback. **Warning**: This is a generic event not a change event. + * @param {number | number[]} value The new value. + */ + onChangeCommitted?: (event: React.SyntheticEvent | Event, value: number | number[]) => void; + /** + * The component orientation. + * @default 'horizontal' + */ + orientation?: 'horizontal' | 'vertical'; + /** + * A transformation function, to change the scale of the slider. + * @default (x) => x + */ + scale?: (value: number) => number; + /** + * The granularity with which the slider can step through values. (A "discrete" slider.) + * The `min` prop serves as the origin for the valid values. + * We recommend (max - min) to be evenly divisible by the step. + * + * When step is `null`, the thumb can only be slid onto marks provided with the `marks` prop. + * @default 1 + */ + step?: number | null; + /** + * Tab index attribute of the hidden `input` element. + */ + tabIndex?: number; + /** + * The track presentation: + * + * - `normal` the track will render a bar representing the slider value. + * - `inverted` the track will render a bar representing the remaining slider value. + * - `false` the track will render without a bar. + * @default 'normal' + */ + track?: 'normal' | false | 'inverted'; + /** + * The value of the slider. + * For ranged sliders, provide an array with two values. + */ + value?: number | number[]; + /** + * 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. + * @default 'off' + */ + valueLabelDisplay?: 'on' | 'auto' | 'off'; + /** + * 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 + * @default (x) => x + */ + valueLabelFormat?: string | ((value: number, index: number) => React.ReactNode); + }; + defaultComponent: D; +} + +/** + * Utility to create component types that inherit props from SliderUnstyled. + */ +export interface ExtendSliderUnstyledTypeMap { + props: M['props'] & SliderUnstyledTypeMap['props']; + defaultComponent: M['defaultComponent']; +} + +export type ExtendSliderUnstyled = OverridableComponent< + ExtendSliderUnstyledTypeMap +>; + +type SliderUnstyledProps< + D extends React.ElementType = SliderUnstyledTypeMap['defaultComponent'], + P = {}, +> = OverrideProps, D>; + +export default SliderUnstyledProps; diff --git a/packages/mui-base/src/SliderUnstyled/index.d.ts b/packages/mui-base/src/SliderUnstyled/index.d.ts index 3040453cedd4f5..f6a8dad8b6aa0b 100644 --- a/packages/mui-base/src/SliderUnstyled/index.d.ts +++ b/packages/mui-base/src/SliderUnstyled/index.d.ts @@ -7,5 +7,8 @@ export * from './SliderValueLabelUnstyled'; export { default as useSlider } from './useSlider'; export * from './useSlider'; +export { default as SliderUnstyledProps } from './SliderUnstyledProps'; +export * from './SliderUnstyledProps'; + export { default as sliderUnstyledClasses } from './sliderUnstyledClasses'; export * from './sliderUnstyledClasses'; diff --git a/packages/mui-base/src/SliderUnstyled/useSlider.ts b/packages/mui-base/src/SliderUnstyled/useSlider.ts index 7caadf9eac929b..29b6c834a6aa1b 100644 --- a/packages/mui-base/src/SliderUnstyled/useSlider.ts +++ b/packages/mui-base/src/SliderUnstyled/useSlider.ts @@ -8,6 +8,7 @@ import { unstable_useControlled as useControlled, visuallyHidden, } from '@mui/utils'; +import SliderUnstyledProps from './SliderUnstyledProps'; interface Mark { value: number; @@ -16,26 +17,23 @@ interface Mark { export interface UseSliderProps { ref: React.Ref; - 'aria-labelledby'?: string; - componentsProps?: { - track?: { style?: React.CSSProperties }; - }; - defaultValue?: number | number[]; - disabled?: boolean; - disableSwap?: boolean; - isRtl?: boolean; - marks?: boolean | Mark[]; - max?: number; - min?: number; - name?: string; - onChange?: (event: Event, value: number | number[], activeThumb: number) => void; - onMouseDown?: (event: React.MouseEvent) => void; - onChangeCommitted?: (event: React.SyntheticEvent | Event, value: number | number[]) => void; - orientation?: 'horizontal' | 'vertical'; - scale?: (value: number) => number; - step?: number | null; - tabIndex?: number; - value?: number | number[]; + 'aria-labelledby'?: SliderUnstyledProps['aria-labelledby']; + defaultValue?: SliderUnstyledProps['defaultValue']; + disabled?: SliderUnstyledProps['disabled']; + disableSwap?: SliderUnstyledProps['disableSwap']; + isRtl?: SliderUnstyledProps['isRtl']; + marks?: SliderUnstyledProps['marks']; + max?: SliderUnstyledProps['max']; + min?: SliderUnstyledProps['min']; + name?: SliderUnstyledProps['name']; + onChange?: SliderUnstyledProps['onChange']; + onMouseDown?: SliderUnstyledProps['onMouseDown']; + onChangeCommitted?: SliderUnstyledProps['onChangeCommitted']; + orientation?: SliderUnstyledProps['orientation']; + scale?: SliderUnstyledProps['scale']; + step?: SliderUnstyledProps['step']; + tabIndex?: SliderUnstyledProps['tabIndex']; + value?: SliderUnstyledProps['value']; } const INTENTIONAL_DRAG_COUNT_THRESHOLD = 2; @@ -215,7 +213,6 @@ export default function useSlider(props: UseSliderProps) { tabIndex, value: valueProp, isRtl = false, - componentsProps = {}, } = props; const touchId = React.useRef(); diff --git a/packages/mui-material/src/Slider/Slider.js b/packages/mui-material/src/Slider/Slider.js index 46a0cc3e4f0988..e9bf736c986352 100644 --- a/packages/mui-material/src/Slider/Slider.js +++ b/packages/mui-material/src/Slider/Slider.js @@ -27,7 +27,7 @@ export const sliderClasses = { ]), }; -export const SliderRoot = styled('span', { +const SliderRoot = styled('span', { name: 'MuiSlider', slot: 'Root', overridesResolver: (props, styles) => { @@ -110,7 +110,20 @@ export const SliderRoot = styled('span', { }, })); -export const SliderRail = styled('span', { +SliderRoot.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the d.ts file and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * @ignore + */ + children: PropTypes.node, +}; + +export { SliderRoot }; + +const SliderRail = styled('span', { name: 'MuiSlider', slot: 'Rail', overridesResolver: (props, styles) => styles.rail, @@ -137,7 +150,20 @@ export const SliderRail = styled('span', { }), })); -export const SliderTrack = styled('span', { +SliderRail.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the d.ts file and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * @ignore + */ + children: PropTypes.node, +}; + +export { SliderRail }; + +const SliderTrack = styled('span', { name: 'MuiSlider', slot: 'Track', overridesResolver: (props, styles) => styles.track, @@ -178,7 +204,20 @@ export const SliderTrack = styled('span', { }; }); -export const SliderThumb = styled('span', { +SliderTrack.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the d.ts file and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * @ignore + */ + children: PropTypes.node, +}; + +export { SliderTrack }; + +const SliderThumb = styled('span', { name: 'MuiSlider', slot: 'Thumb', overridesResolver: (props, styles) => { @@ -253,7 +292,20 @@ export const SliderThumb = styled('span', { }, })); -export const SliderValueLabel = styled(SliderValueLabelUnstyled, { +SliderThumb.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the d.ts file and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * @ignore + */ + children: PropTypes.node, +}; + +export { SliderThumb }; + +const SliderValueLabel = styled(SliderValueLabelUnstyled, { name: 'MuiSlider', slot: 'ValueLabel', overridesResolver: (props, styles) => styles.valueLabel, @@ -295,7 +347,20 @@ export const SliderValueLabel = styled(SliderValueLabelUnstyled, { }, })); -export const SliderMark = styled('span', { +SliderValueLabel.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the d.ts file and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * @ignore + */ + children: PropTypes.node, +}; + +export { SliderValueLabel }; + +const SliderMark = styled('span', { name: 'MuiSlider', slot: 'Mark', shouldForwardProp: (prop) => slotShouldForwardProp(prop) && prop !== 'markActive', @@ -320,7 +385,20 @@ export const SliderMark = styled('span', { }), })); -export const SliderMarkLabel = styled('span', { +SliderMark.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the d.ts file and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * @ignore + */ + children: PropTypes.node, +}; + +export { SliderMark }; + +const SliderMarkLabel = styled('span', { name: 'MuiSlider', slot: 'MarkLabel', shouldForwardProp: (prop) => slotShouldForwardProp(prop) && prop !== 'markLabelActive', @@ -349,7 +427,7 @@ export const SliderMarkLabel = styled('span', { }), })); -SliderRoot.propTypes = { +SliderMarkLabel.propTypes /* remove-proptypes */ = { // ----------------------------- Warning -------------------------------- // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the d.ts file and run "yarn proptypes" | @@ -358,44 +436,10 @@ SliderRoot.propTypes = { * @ignore */ children: PropTypes.node, - /** - * @ignore - */ - ownerState: PropTypes.shape({ - 'aria-label': PropTypes.string, - 'aria-labelledby': PropTypes.string, - 'aria-valuetext': PropTypes.string, - classes: PropTypes.object, - color: PropTypes.oneOf(['primary', 'secondary']), - defaultValue: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]), - disabled: PropTypes.bool, - getAriaLabel: PropTypes.func, - getAriaValueText: PropTypes.func, - isRtl: PropTypes.bool, - marks: PropTypes.oneOfType([ - PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.node, - value: PropTypes.number.isRequired, - }), - ), - PropTypes.bool, - ]), - max: PropTypes.number, - min: PropTypes.number, - name: PropTypes.string, - onChange: PropTypes.func, - onChangeCommitted: PropTypes.func, - orientation: PropTypes.oneOf(['horizontal', 'vertical']), - scale: PropTypes.func, - step: PropTypes.number, - track: PropTypes.oneOf(['inverted', 'normal', false]), - value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]), - valueLabelDisplay: PropTypes.oneOf(['auto', 'off', 'on']), - valueLabelFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - }), }; +export { SliderMarkLabel }; + const extendUtilityClasses = (ownerState) => { const { color, size, classes = {} } = ownerState; diff --git a/scripts/generateProptypes.ts b/scripts/generateProptypes.ts index 77f401fb539f4a..1a010039b16820 100644 --- a/scripts/generateProptypes.ts +++ b/scripts/generateProptypes.ts @@ -208,6 +208,7 @@ async function generateProptypes( const isTsFile = /(\.(ts|tsx))/.test(sourceFile); const unstyledFile = getUnstyledFilename(tsFile, true); + const unstyledPropsFile = unstyledFile.replace('.d.ts', 'Props.ts'); const generatedForTypeScriptFile = sourceFile === tsFile; const result = ttp.inject(proptypes, sourceContent, { @@ -258,7 +259,8 @@ async function generateProptypes( prop.filenames.forEach((filename) => { const isExternal = filename !== tsFile; - const implementedByUnstyledVariant = filename === unstyledFile; + const implementedByUnstyledVariant = + filename === unstyledFile || filename === unstyledPropsFile; if (!isExternal || implementedByUnstyledVariant) { shouldDocument = true; } From 52e5a7c166a66a8756de57bd61ad95e021cd2305 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Tue, 21 Dec 2021 11:20:53 +0100 Subject: [PATCH 15/18] Address review comments --- .../src/SliderUnstyled/SliderUnstyled.js | 9 ++-- .../src/SliderUnstyled/SliderUnstyled.test.js | 9 ++++ .../mui-base/src/SliderUnstyled/useSlider.ts | 42 +++++++++++++++---- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index 3fa0a2eb2980f9..6e85cae0d50056 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -96,13 +96,12 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { axisProps, getRootProps, getHiddenInputProps, + getThumbProps, open, active, axis, range, focusVisible, - handleMouseOver, - handleMouseLeave, dragging, marks, values, @@ -142,9 +141,8 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { return ( @@ -229,9 +227,8 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { > ', () => { root: { expectedClassName: classes.root, }, + thumb: { + expectedClassName: classes.thumb, + }, + track: { + expectedClassName: classes.track, + }, + rail: { + expectedClassName: classes.rail, + }, }, })); diff --git a/packages/mui-base/src/SliderUnstyled/useSlider.ts b/packages/mui-base/src/SliderUnstyled/useSlider.ts index 29b6c834a6aa1b..e90ada6e29e74d 100644 --- a/packages/mui-base/src/SliderUnstyled/useSlider.ts +++ b/packages/mui-base/src/SliderUnstyled/useSlider.ts @@ -294,13 +294,6 @@ export default function useSlider(props: UseSliderProps) { setOpen(-1); otherHandlers?.onBlur?.(event); }; - const handleMouseOver = useEventCallback((event: React.MouseEvent) => { - const index = Number(event.currentTarget.getAttribute('data-index')); - setOpen(index); - }); - const handleMouseLeave = useEventCallback(() => { - setOpen(-1); - }); useEnhancedEffect(() => { if (disabled && sliderRef.current!.contains(document.activeElement)) { @@ -625,6 +618,38 @@ export default function useSlider(props: UseSliderProps) { }; }; + const createHandleMouseOver = + (otherHandlers: Record>) => + (event: React.MouseEvent) => { + const index = Number(event.currentTarget.getAttribute('data-index')); + setOpen(index); + + otherHandlers.onMouseOver?.(event); + }; + + const createHandleMouseLeave = + (otherHandlers: Record>) => + (event: React.MouseEvent) => { + setOpen(-1); + + otherHandlers.onMouseLeave?.(event); + }; + + const getThumbProps = (otherHandlers?: Record>) => { + const ownEventHandlers = { + onMouseOver: createHandleMouseOver(otherHandlers || {}), + onMouseLeave: createHandleMouseLeave(otherHandlers || {}), + }; + + const mergedEventHandlers: Record> = { + ...otherHandlers, + ...ownEventHandlers, + }; + return { + ...mergedEventHandlers, + }; + }; + const getHiddenInputProps = (otherHandlers?: Record>) => { const ownEventHandlers = { onChange: createHandleHiddenInputChange(otherHandlers || {}), @@ -665,12 +690,11 @@ export default function useSlider(props: UseSliderProps) { axisProps, getRootProps, getHiddenInputProps, + getThumbProps, dragging, marks, values, active, - handleMouseOver, - handleMouseLeave, focusVisible, open, range, From afac9ca0e7baa71c913475d512476fa0cd4fe204 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Wed, 22 Dec 2021 13:52:56 +0100 Subject: [PATCH 16/18] Improve onMouseDown handling --- packages/mui-base/src/SliderUnstyled/SliderUnstyled.js | 2 +- packages/mui-base/src/SliderUnstyled/useSlider.ts | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index 6e85cae0d50056..ae3b4320f72f4d 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -142,7 +142,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { return ( diff --git a/packages/mui-base/src/SliderUnstyled/useSlider.ts b/packages/mui-base/src/SliderUnstyled/useSlider.ts index e90ada6e29e74d..7761d5181f7c9b 100644 --- a/packages/mui-base/src/SliderUnstyled/useSlider.ts +++ b/packages/mui-base/src/SliderUnstyled/useSlider.ts @@ -27,7 +27,6 @@ export interface UseSliderProps { min?: SliderUnstyledProps['min']; name?: SliderUnstyledProps['name']; onChange?: SliderUnstyledProps['onChange']; - onMouseDown?: SliderUnstyledProps['onMouseDown']; onChangeCommitted?: SliderUnstyledProps['onChangeCommitted']; orientation?: SliderUnstyledProps['orientation']; scale?: SliderUnstyledProps['scale']; @@ -206,7 +205,6 @@ export default function useSlider(props: UseSliderProps) { name, onChange, onChangeCommitted, - onMouseDown, orientation = 'horizontal', scale = Identity, step = 1, @@ -569,10 +567,6 @@ export default function useSlider(props: UseSliderProps) { const createHandleMouseDown = (otherHandlers: Record>) => (event: React.MouseEvent) => { - if (onMouseDown) { - onMouseDown(event); - } - // Only handle left clicks if (event.button !== 0) { return; From 9f835697cc3c582e5f821873ba725ec9a08adbef Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Mon, 27 Dec 2021 13:56:10 +0100 Subject: [PATCH 17/18] Refactor event handlers --- .../mui-base/src/SliderUnstyled/useSlider.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/useSlider.ts b/packages/mui-base/src/SliderUnstyled/useSlider.ts index 7761d5181f7c9b..f07b55abeccbb5 100644 --- a/packages/mui-base/src/SliderUnstyled/useSlider.ts +++ b/packages/mui-base/src/SliderUnstyled/useSlider.ts @@ -312,6 +312,8 @@ export default function useSlider(props: UseSliderProps) { const createHandleHiddenInputChange = (otherHandlers: Record>) => (event: React.ChangeEvent) => { + otherHandlers.onChange?.(event); + const index = Number(event.currentTarget.getAttribute('data-index')); const value = values[index]; const marksIndex = marksValues.indexOf(value); @@ -367,8 +369,6 @@ export default function useSlider(props: UseSliderProps) { if (onChangeCommitted) { onChangeCommitted(event, newValue); } - - otherHandlers.onChange?.(event); }; const previousIndex = React.useRef(); @@ -567,6 +567,11 @@ export default function useSlider(props: UseSliderProps) { const createHandleMouseDown = (otherHandlers: Record>) => (event: React.MouseEvent) => { + otherHandlers.onMouseDown?.(event); + if (event.defaultPrevented) { + return; + } + // Only handle left clicks if (event.button !== 0) { return; @@ -590,8 +595,6 @@ export default function useSlider(props: UseSliderProps) { const doc = ownerDocument(sliderRef.current); doc.addEventListener('mousemove', handleTouchMove); doc.addEventListener('mouseup', handleTouchEnd); - - otherHandlers.onMouseDown?.(event); }; const trackOffset = valueToPercent(range ? values[0] : min, min, max); @@ -615,18 +618,18 @@ export default function useSlider(props: UseSliderProps) { const createHandleMouseOver = (otherHandlers: Record>) => (event: React.MouseEvent) => { + otherHandlers.onMouseOver?.(event); + const index = Number(event.currentTarget.getAttribute('data-index')); setOpen(index); - - otherHandlers.onMouseOver?.(event); }; const createHandleMouseLeave = (otherHandlers: Record>) => (event: React.MouseEvent) => { - setOpen(-1); - otherHandlers.onMouseLeave?.(event); + + setOpen(-1); }; const getThumbProps = (otherHandlers?: Record>) => { From d556c511e17809079e2c7978cb535efa1ec251dd Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Tue, 11 Jan 2022 14:28:10 +0100 Subject: [PATCH 18/18] Fixes after merge --- .../src/SliderUnstyled/SliderUnstyled.js | 17 ++++++++++++++--- .../src/SliderUnstyled/SliderUnstyledProps.ts | 2 ++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index a4122910e60794..44e07796ebb192 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -137,6 +137,10 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { const MarkLabel = components.MarkLabel ?? 'span'; const markLabelProps = appendOwnerState(MarkLabel, componentsProps.markLabel, ownerState); + const Input = components.Input || 'input'; + const inputProps = appendOwnerState(Input, componentsProps.input, ownerState); + const hiddenInputProps = getHiddenInputProps(); + const classes = useUtilityClasses(ownerState); return ( @@ -242,9 +246,8 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { ...thumbProps.style, }} > - {/* eslint-disable-next-line jsx-a11y/role-supports-aria-props */} - diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyledProps.ts b/packages/mui-base/src/SliderUnstyled/SliderUnstyledProps.ts index 3c8740503373d1..533ffbe10deb0f 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyledProps.ts +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyledProps.ts @@ -49,6 +49,7 @@ export interface SliderUnstyledTypeMap

& SliderUnstyledComponentsPropsOverrides; valueLabel?: React.ComponentPropsWithRef & SliderUnstyledComponentsPropsOverrides; + input?: React.ComponentPropsWithRef<'input'> & SliderUnstyledComponentsPropsOverrides; }; /** * The default value. Use when the component is not controlled.