From 55473b0263682057bb4258d0199367cd19cf2c05 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 5 Sep 2024 14:20:57 +0800 Subject: [PATCH 01/12] Convert to namespace types --- docs/data/api/slider-root.json | 6 - .../tailwind/index.tsx | 12 +- .../api-docs/slider-root/slider-root.json | 14 - .../src/Slider/Control/SliderControl.test.tsx | 4 +- .../src/Slider/Control/SliderControl.tsx | 10 +- .../src/Slider/Control/SliderControl.types.ts | 34 --- .../src/Slider/Control/useSliderControl.ts | 43 ++- .../Slider/Indicator/SliderIndicator.test.tsx | 4 +- .../src/Slider/Indicator/SliderIndicator.tsx | 12 +- .../Slider/Indicator/SliderIndicator.types.ts | 16 - .../Slider/Indicator/useSliderIndicator.ts | 34 ++- .../src/Slider/Output/SliderOutput.test.tsx | 4 +- .../src/Slider/Output/SliderOutput.tsx | 10 +- .../src/Slider/Output/SliderOutput.types.ts | 14 - .../src/Slider/Output/useSliderOutput.ts | 29 +- .../src/Slider/Root/SliderProvider.tsx | 28 +- .../src/Slider/Root/SliderRoot.test.tsx | 6 +- .../mui-base/src/Slider/Root/SliderRoot.tsx | 102 ++++--- .../src/Slider/Root/SliderRoot.types.ts | 286 ------------------ .../mui-base/src/Slider/Root/styleHooks.ts | 4 +- .../mui-base/src/Slider/Root/useSliderRoot.ts | 198 +++++++++++- .../src/Slider/Thumb/SliderThumb.test.tsx | 4 +- .../mui-base/src/Slider/Thumb/SliderThumb.tsx | 30 +- .../src/Slider/Thumb/SliderThumb.types.ts | 89 ------ .../src/Slider/Thumb/useSliderThumb.ts | 78 ++++- .../src/Slider/Track/SliderTrack.test.tsx | 4 +- .../mui-base/src/Slider/Track/SliderTrack.tsx | 12 +- .../src/Slider/Track/SliderTrack.types.ts | 4 - packages/mui-base/src/Slider/index.barrel.ts | 25 +- packages/mui-base/src/Slider/index.ts | 34 +-- 30 files changed, 519 insertions(+), 631 deletions(-) delete mode 100644 packages/mui-base/src/Slider/Control/SliderControl.types.ts delete mode 100644 packages/mui-base/src/Slider/Indicator/SliderIndicator.types.ts delete mode 100644 packages/mui-base/src/Slider/Output/SliderOutput.types.ts delete mode 100644 packages/mui-base/src/Slider/Root/SliderRoot.types.ts delete mode 100644 packages/mui-base/src/Slider/Thumb/SliderThumb.types.ts delete mode 100644 packages/mui-base/src/Slider/Track/SliderTrack.types.ts diff --git a/docs/data/api/slider-root.json b/docs/data/api/slider-root.json index 6c83045dcb..ecee284f39 100644 --- a/docs/data/api/slider-root.json +++ b/docs/data/api/slider-root.json @@ -10,12 +10,8 @@ "default": "'ltr'" }, "disabled": { "type": { "name": "bool" }, "default": "false" }, - "id": { "type": { "name": "string" } }, "largeStep": { "type": { "name": "number" }, "default": "10" }, - "max": { "type": { "name": "number" }, "default": "100" }, - "min": { "type": { "name": "number" }, "default": "0" }, "minStepsBetweenValues": { "type": { "name": "number" }, "default": "0" }, - "name": { "type": { "name": "string" } }, "onValueChange": { "type": { "name": "func" }, "signature": { @@ -35,8 +31,6 @@ "default": "'horizontal'" }, "render": { "type": { "name": "union", "description": "element
| func" } }, - "step": { "type": { "name": "number" }, "default": "1" }, - "tabIndex": { "type": { "name": "number" } }, "value": { "type": { "name": "union", "description": "Array<number>
| number" } } diff --git a/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.tsx b/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.tsx index f079c3ad49..1fe181cd76 100644 --- a/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.tsx +++ b/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.tsx @@ -36,7 +36,7 @@ export default function UnstyledSliderIntroduction() { } const Slider = React.forwardRef(function Slider( - props: BaseSlider.RootProps, + props: BaseSlider.Root.Props, ref: React.ForwardedRef, ) { return ( @@ -56,7 +56,7 @@ const Slider = React.forwardRef(function Slider( }); const SliderOutput = React.forwardRef(function SliderOutput( - props: BaseSlider.OutputProps, + props: BaseSlider.Output.Props, ref: React.ForwardedRef, ) { return ( @@ -76,7 +76,7 @@ const SliderOutput = React.forwardRef(function SliderOutput( }); const SliderControl = React.forwardRef(function SliderControl( - props: BaseSlider.ControlProps, + props: BaseSlider.Control.Props, ref: React.ForwardedRef, ) { return ( @@ -97,7 +97,7 @@ const SliderControl = React.forwardRef(function SliderControl( }); const SliderTrack = React.forwardRef(function SliderTrack( - props: BaseSlider.TrackProps, + props: BaseSlider.Track.Props, ref: React.ForwardedRef, ) { return ( @@ -117,7 +117,7 @@ const SliderTrack = React.forwardRef(function SliderTrack( }); const SliderThumb = React.forwardRef(function SliderThumb( - props: BaseSlider.ThumbProps, + props: BaseSlider.Thumb.Props, ref: React.ForwardedRef, ) { return ( @@ -139,7 +139,7 @@ const SliderThumb = React.forwardRef(function SliderThumb( }); const SliderIndicator = React.forwardRef(function SliderIndicator( - props: BaseSlider.IndicatorProps, + props: BaseSlider.Indicator.Props, ref: React.ForwardedRef, ) { return ( diff --git a/docs/data/translations/api-docs/slider-root/slider-root.json b/docs/data/translations/api-docs/slider-root/slider-root.json index 392d883f71..0d195e281c 100644 --- a/docs/data/translations/api-docs/slider-root/slider-root.json +++ b/docs/data/translations/api-docs/slider-root/slider-root.json @@ -14,20 +14,12 @@ "description": "Sets the direction. For right-to-left languages, the lowest value is on the right-hand side." }, "disabled": { "description": "If true, the component is disabled." }, - "id": { "description": "The id of the slider element." }, "largeStep": { "description": "The granularity with which the slider can step through values when using Page Up/Page Down or Shift + Arrow Up/Arrow Down." }, - "max": { - "description": "The maximum allowed value of the slider. Should not be equal to min." - }, - "min": { - "description": "The minimum allowed value of the slider. Should not be equal to max." - }, "minStepsBetweenValues": { "description": "The minimum steps between values in a range slider." }, - "name": { "description": "Name attribute of the hidden input element." }, "onValueChange": { "description": "Callback function that is fired when the slider's value changed.", "typeDescriptions": { @@ -45,12 +37,6 @@ }, "orientation": { "description": "The component orientation." }, "render": { "description": "A function to customize rendering of the component." }, - "step": { - "description": "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." - }, - "tabIndex": { - "description": "Tab index attribute of the Thumb component's input element." - }, "value": { "description": "The value of the slider. For ranged sliders, provide an array with two values." } diff --git a/packages/mui-base/src/Slider/Control/SliderControl.test.tsx b/packages/mui-base/src/Slider/Control/SliderControl.test.tsx index 45f5c31eb3..0813071156 100644 --- a/packages/mui-base/src/Slider/Control/SliderControl.test.tsx +++ b/packages/mui-base/src/Slider/Control/SliderControl.test.tsx @@ -1,14 +1,14 @@ import * as React from 'react'; import * as Slider from '@base_ui/react/Slider'; -import { SliderProvider, type SliderProviderValue } from '@base_ui/react/Slider'; import { createRenderer, describeConformance } from '#test-utils'; +import { SliderProvider } from '../Root/SliderProvider'; const NOOP = () => {}; describe('', () => { const { render } = createRenderer(); - const testProviderValue: SliderProviderValue = { + const testProviderValue: SliderProvider.Value = { active: -1, areValuesEqual: () => true, axis: 'horizontal', diff --git a/packages/mui-base/src/Slider/Control/SliderControl.tsx b/packages/mui-base/src/Slider/Control/SliderControl.tsx index a2e6cac4d7..4b5633de4a 100644 --- a/packages/mui-base/src/Slider/Control/SliderControl.tsx +++ b/packages/mui-base/src/Slider/Control/SliderControl.tsx @@ -1,12 +1,12 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; +import type { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useSliderContext } from '../Root/SliderProvider'; import { sliderStyleHookMapping } from '../Root/styleHooks'; -import { SliderControlProps } from './SliderControl.types'; +import type { SliderRoot } from '../Root/SliderRoot'; import { useSliderControl } from './useSliderControl'; - /** * * Demos: @@ -18,7 +18,7 @@ import { useSliderControl } from './useSliderControl'; * - [SliderControl API](https://base-ui.netlify.app/components/react-slider/#api-reference-SliderControl) */ const SliderControl = React.forwardRef(function SliderControl( - props: SliderControlProps, + props: SliderControl.Props, forwardedRef: React.ForwardedRef, ) { const { render: renderProp, className, ...otherProps } = props; @@ -90,4 +90,8 @@ SliderControl.propTypes /* remove-proptypes */ = { render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), } as any; +export namespace SliderControl { + export interface Props extends BaseUIComponentProps<'span', SliderRoot.OwnerState> {} +} + export { SliderControl }; diff --git a/packages/mui-base/src/Slider/Control/SliderControl.types.ts b/packages/mui-base/src/Slider/Control/SliderControl.types.ts deleted file mode 100644 index b7b469d09d..0000000000 --- a/packages/mui-base/src/Slider/Control/SliderControl.types.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { BaseUIComponentProps } from '../../utils/types'; -import { SliderRootOwnerState, UseSliderReturnValue } from '../Root/SliderRoot.types'; - -export interface SliderControlProps extends BaseUIComponentProps<'span', SliderRootOwnerState> {} - -export interface UseSliderControlParameters - extends Pick< - UseSliderReturnValue, - | 'areValuesEqual' - | 'disabled' - | 'dragging' - | 'getFingerNewValue' - | 'handleValueChange' - | 'minStepsBetweenValues' - | 'onValueCommitted' - | 'percentageValues' - | 'registerSliderControl' - | 'setActive' - | 'setDragging' - | 'setValueState' - | 'step' - | 'subitems' - > { - /** - * The ref attached to the control area of the Slider. - */ - rootRef?: React.Ref; -} - -export interface UseSliderControlReturnValue { - getRootProps: ( - externalProps?: React.ComponentPropsWithRef<'span'>, - ) => React.ComponentPropsWithRef<'span'>; -} diff --git a/packages/mui-base/src/Slider/Control/useSliderControl.ts b/packages/mui-base/src/Slider/Control/useSliderControl.ts index 6e20b6cdaa..4b270e0c6a 100644 --- a/packages/mui-base/src/Slider/Control/useSliderControl.ts +++ b/packages/mui-base/src/Slider/Control/useSliderControl.ts @@ -2,17 +2,22 @@ import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { ownerDocument } from '../../utils/owner'; +import type { GenericHTMLProps } from '../../utils/types'; import { useForkRef } from '../../utils/useForkRef'; import { useEventCallback } from '../../utils/useEventCallback'; -import { focusThumb, trackFinger, validateMinimumDistance } from '../Root/useSliderRoot'; -import { UseSliderControlParameters, UseSliderControlReturnValue } from './SliderControl.types'; +import { + focusThumb, + trackFinger, + type useSliderRoot, + validateMinimumDistance, +} from '../Root/useSliderRoot'; import { useFieldControlValidation } from '../../Field/Control/useFieldControlValidation'; const INTENTIONAL_DRAG_COUNT_THRESHOLD = 2; export function useSliderControl( - parameters: UseSliderControlParameters, -): UseSliderControlReturnValue { + parameters: useSliderControl.Parameters, +): useSliderControl.ReturnValue { const { areValuesEqual, disabled, @@ -283,3 +288,33 @@ export function useSliderControl( [getRootProps], ); } + +export namespace useSliderControl { + export interface Parameters + extends Pick< + useSliderRoot.ReturnValue, + | 'areValuesEqual' + | 'disabled' + | 'dragging' + | 'getFingerNewValue' + | 'handleValueChange' + | 'minStepsBetweenValues' + | 'onValueCommitted' + | 'percentageValues' + | 'registerSliderControl' + | 'setActive' + | 'setDragging' + | 'setValueState' + | 'step' + | 'subitems' + > { + /** + * The ref attached to the control area of the Slider. + */ + rootRef?: React.Ref; + } + + export interface ReturnValue { + getRootProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + } +} diff --git a/packages/mui-base/src/Slider/Indicator/SliderIndicator.test.tsx b/packages/mui-base/src/Slider/Indicator/SliderIndicator.test.tsx index b5f19049ba..36d2489dda 100644 --- a/packages/mui-base/src/Slider/Indicator/SliderIndicator.test.tsx +++ b/packages/mui-base/src/Slider/Indicator/SliderIndicator.test.tsx @@ -1,14 +1,14 @@ import * as React from 'react'; import * as Slider from '@base_ui/react/Slider'; -import { SliderProvider, type SliderProviderValue } from '@base_ui/react/Slider'; import { createRenderer, describeConformance } from '#test-utils'; +import { SliderProvider } from '../Root/SliderProvider'; const NOOP = () => {}; describe('', () => { const { render } = createRenderer(); - const testProviderValue: SliderProviderValue = { + const testProviderValue: SliderProvider.Value = { active: -1, areValuesEqual: () => true, axis: 'horizontal', diff --git a/packages/mui-base/src/Slider/Indicator/SliderIndicator.tsx b/packages/mui-base/src/Slider/Indicator/SliderIndicator.tsx index 60b85841b8..7230002306 100644 --- a/packages/mui-base/src/Slider/Indicator/SliderIndicator.tsx +++ b/packages/mui-base/src/Slider/Indicator/SliderIndicator.tsx @@ -1,12 +1,12 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; +import type { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useSliderContext } from '../Root/SliderProvider'; import { sliderStyleHookMapping } from '../Root/styleHooks'; -import { SliderIndicatorProps } from './SliderIndicator.types'; +import type { SliderRoot } from '../Root/SliderRoot'; import { useSliderIndicator } from './useSliderIndicator'; - /** * * Demos: @@ -18,8 +18,8 @@ import { useSliderIndicator } from './useSliderIndicator'; * - [SliderIndicator API](https://base-ui.netlify.app/components/react-slider/#api-reference-SliderIndicator) */ const SliderIndicator = React.forwardRef(function SliderIndicator( - props: SliderIndicatorProps, - forwardedRef: React.ForwardedRef, + props: SliderIndicator.Props, + forwardedRef: React.ForwardedRef, ) { const { render, className, ...otherProps } = props; @@ -66,4 +66,8 @@ SliderIndicator.propTypes /* remove-proptypes */ = { render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), } as any; +export namespace SliderIndicator { + export interface Props extends BaseUIComponentProps<'span', SliderRoot.OwnerState> {} +} + export { SliderIndicator }; diff --git a/packages/mui-base/src/Slider/Indicator/SliderIndicator.types.ts b/packages/mui-base/src/Slider/Indicator/SliderIndicator.types.ts deleted file mode 100644 index f8916b0d35..0000000000 --- a/packages/mui-base/src/Slider/Indicator/SliderIndicator.types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BaseUIComponentProps } from '../../utils/types'; -import { SliderRootOwnerState, UseSliderReturnValue } from '../Root/SliderRoot.types'; - -export interface SliderIndicatorProps extends BaseUIComponentProps<'span', SliderRootOwnerState> {} - -export interface UseSliderIndicatorParameters - extends Pick< - UseSliderReturnValue, - 'axis' | 'direction' | 'disabled' | 'orientation' | 'percentageValues' - > {} - -export interface UseSliderIndicatorReturnValue { - getRootProps: ( - externalProps?: React.ComponentPropsWithRef<'span'>, - ) => React.ComponentPropsWithRef<'span'>; -} diff --git a/packages/mui-base/src/Slider/Indicator/useSliderIndicator.ts b/packages/mui-base/src/Slider/Indicator/useSliderIndicator.ts index a7322e43be..0c0255c767 100644 --- a/packages/mui-base/src/Slider/Indicator/useSliderIndicator.ts +++ b/packages/mui-base/src/Slider/Indicator/useSliderIndicator.ts @@ -1,10 +1,8 @@ 'use client'; import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; -import { - UseSliderIndicatorParameters, - UseSliderIndicatorReturnValue, -} from './SliderIndicator.types'; +import type { GenericHTMLProps } from '../../utils/types'; +import type { useSliderRoot } from '../Root/useSliderRoot'; const axisProps = { horizontal: { @@ -21,9 +19,19 @@ const axisProps = { }, }; -function useSliderIndicator( - parameters: UseSliderIndicatorParameters, -): UseSliderIndicatorReturnValue { +/** + * + * Demos: + * + * - [Slider](https://mui.com/base-ui/react-slider/#hooks) + * + * API: + * + * - [useSliderIndicator API](https://mui.com/base-ui/react-slider/hooks-api/#use-slider-indicator) + */ +export function useSliderIndicator( + parameters: useSliderIndicator.Parameters, +): useSliderIndicator.ReturnValue { const { axis, direction, orientation, percentageValues } = parameters; const isRange = percentageValues.length > 1; @@ -74,4 +82,14 @@ function useSliderIndicator( ); } -export { useSliderIndicator }; +export namespace useSliderIndicator { + export interface Parameters + extends Pick< + useSliderRoot.ReturnValue, + 'axis' | 'direction' | 'disabled' | 'orientation' | 'percentageValues' + > {} + + export interface ReturnValue { + getRootProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + } +} diff --git a/packages/mui-base/src/Slider/Output/SliderOutput.test.tsx b/packages/mui-base/src/Slider/Output/SliderOutput.test.tsx index fac49ee9e3..22a9e8b347 100644 --- a/packages/mui-base/src/Slider/Output/SliderOutput.test.tsx +++ b/packages/mui-base/src/Slider/Output/SliderOutput.test.tsx @@ -1,15 +1,15 @@ import * as React from 'react'; import { expect } from 'chai'; import * as Slider from '@base_ui/react/Slider'; -import { SliderProvider, type SliderProviderValue } from '@base_ui/react/Slider'; import { createRenderer, describeConformance } from '#test-utils'; +import { SliderProvider } from '../Root/SliderProvider'; const NOOP = () => {}; describe('', () => { const { render } = createRenderer(); - const testProviderValue: SliderProviderValue = { + const testProviderValue: SliderProvider.Value = { active: -1, areValuesEqual: () => true, axis: 'horizontal', diff --git a/packages/mui-base/src/Slider/Output/SliderOutput.tsx b/packages/mui-base/src/Slider/Output/SliderOutput.tsx index 0f69309611..0e77a8d85e 100644 --- a/packages/mui-base/src/Slider/Output/SliderOutput.tsx +++ b/packages/mui-base/src/Slider/Output/SliderOutput.tsx @@ -1,12 +1,12 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; +import type { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useSliderContext } from '../Root/SliderProvider'; import { sliderStyleHookMapping } from '../Root/styleHooks'; -import { SliderOutputProps } from './SliderOutput.types'; +import type { SliderRoot } from '../Root/SliderRoot'; import { useSliderOutput } from './useSliderOutput'; - /** * * Demos: @@ -18,7 +18,7 @@ import { useSliderOutput } from './useSliderOutput'; * - [SliderOutput API](https://base-ui.netlify.app/components/react-slider/#api-reference-SliderOutput) */ const SliderOutput = React.forwardRef(function SliderOutput( - props: SliderOutputProps, + props: SliderOutput.Props, forwardedRef: React.ForwardedRef, ) { const { render, className, ...otherProps } = props; @@ -64,4 +64,8 @@ SliderOutput.propTypes /* remove-proptypes */ = { render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), } as any; +export namespace SliderOutput { + export interface Props extends BaseUIComponentProps<'output', SliderRoot.OwnerState> {} +} + export { SliderOutput }; diff --git a/packages/mui-base/src/Slider/Output/SliderOutput.types.ts b/packages/mui-base/src/Slider/Output/SliderOutput.types.ts deleted file mode 100644 index a93f2cf1ae..0000000000 --- a/packages/mui-base/src/Slider/Output/SliderOutput.types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { BaseUIComponentProps } from '../../utils/types'; -import { SliderRootOwnerState, UseSliderReturnValue } from '../Root/SliderRoot.types'; - -export interface SliderOutputProps extends BaseUIComponentProps<'output', SliderRootOwnerState> {} - -export interface UseSliderOutputParameters extends Pick { - 'aria-live'?: React.AriaAttributes['aria-live']; -} - -export interface UseSliderOutputReturnValue { - getRootProps: ( - externalProps?: React.ComponentPropsWithRef<'output'>, - ) => React.ComponentPropsWithRef<'output'>; -} diff --git a/packages/mui-base/src/Slider/Output/useSliderOutput.ts b/packages/mui-base/src/Slider/Output/useSliderOutput.ts index 7ad3534ec9..93484429eb 100644 --- a/packages/mui-base/src/Slider/Output/useSliderOutput.ts +++ b/packages/mui-base/src/Slider/Output/useSliderOutput.ts @@ -1,9 +1,20 @@ 'use client'; import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; -import { UseSliderOutputParameters, UseSliderOutputReturnValue } from './SliderOutput.types'; - -function useSliderOutput(parameters: UseSliderOutputParameters): UseSliderOutputReturnValue { +import type { useSliderRoot } from '../Root/useSliderRoot'; +/** + * + * Demos: + * + * - [Slider](https://mui.com/base-ui/react-slider/#hooks) + * + * API: + * + * - [useSliderOutput API](https://mui.com/base-ui/react-slider/hooks-api/#use-slider-output) + */ +export function useSliderOutput( + parameters: useSliderOutput.Parameters, +): useSliderOutput.ReturnValue { const { 'aria-live': ariaLive = 'off', subitems } = parameters; const outputFor = Array.from(subitems.values()).reduce((acc, item) => { @@ -30,4 +41,14 @@ function useSliderOutput(parameters: UseSliderOutputParameters): UseSliderOutput ); } -export { useSliderOutput }; +export namespace useSliderOutput { + export interface Parameters extends Pick { + 'aria-live'?: React.AriaAttributes['aria-live']; + } + + export interface ReturnValue { + getRootProps: ( + externalProps?: React.ComponentPropsWithRef<'output'>, + ) => React.ComponentPropsWithRef<'output'>; + } +} diff --git a/packages/mui-base/src/Slider/Root/SliderProvider.tsx b/packages/mui-base/src/Slider/Root/SliderProvider.tsx index 7e3e158a32..9ea695c83d 100644 --- a/packages/mui-base/src/Slider/Root/SliderProvider.tsx +++ b/packages/mui-base/src/Slider/Root/SliderProvider.tsx @@ -1,15 +1,11 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { CompoundComponentContext } from '../../useCompound'; -import { SliderContextValue, SliderProviderValue } from './SliderRoot.types'; +import { CompoundComponentContext, CompoundComponentContextValue } from '../../useCompound'; +import type { useSliderThumb } from '../Thumb/useSliderThumb'; +import type { SliderRoot } from './SliderRoot'; -export interface SliderProviderProps { - value: SliderProviderValue; - children: React.ReactNode; -} - -export const SliderContext = React.createContext(undefined); +export const SliderContext = React.createContext(undefined); if (process.env.NODE_ENV !== 'production') { SliderContext.displayName = 'SliderContext'; @@ -28,7 +24,7 @@ export function useSliderContext() { * * @ignore - do not document. */ -function SliderProvider(props: SliderProviderProps) { +export function SliderProvider(props: SliderProvider.Props) { const { value: valueProp, children } = props; const { compoundComponentContextValue, ...contextValue } = valueProp; @@ -78,6 +74,7 @@ SliderProvider.propTypes /* remove-proptypes */ = { ownerState: PropTypes.shape({ activeThumbIndex: PropTypes.number.isRequired, direction: PropTypes.oneOf(['ltr', 'rtl']).isRequired, + dirty: PropTypes.bool.isRequired, disabled: PropTypes.bool.isRequired, dragging: PropTypes.bool.isRequired, max: PropTypes.number.isRequired, @@ -85,6 +82,8 @@ SliderProvider.propTypes /* remove-proptypes */ = { minStepsBetweenValues: PropTypes.number.isRequired, orientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired, step: PropTypes.number.isRequired, + touched: PropTypes.bool.isRequired, + valid: PropTypes.bool, values: PropTypes.arrayOf(PropTypes.number).isRequired, }).isRequired, percentageValues: PropTypes.arrayOf(PropTypes.number).isRequired, @@ -99,4 +98,13 @@ SliderProvider.propTypes /* remove-proptypes */ = { }).isRequired, } as any; -export { SliderProvider }; +export namespace SliderProvider { + export interface Props { + value: Value; + children: React.ReactNode; + } + + export interface Value extends SliderRoot.Context { + compoundComponentContextValue: CompoundComponentContextValue; + } +} diff --git a/packages/mui-base/src/Slider/Root/SliderRoot.test.tsx b/packages/mui-base/src/Slider/Root/SliderRoot.test.tsx index d106d49fea..4ff4db3ae1 100644 --- a/packages/mui-base/src/Slider/Root/SliderRoot.test.tsx +++ b/packages/mui-base/src/Slider/Root/SliderRoot.test.tsx @@ -4,7 +4,7 @@ import { spy, stub } from 'sinon'; import { act, fireEvent, screen } from '@mui/internal-test-utils'; import * as Slider from '@base_ui/react/Slider'; import { createRenderer, describeConformance } from '#test-utils'; -import type { SliderRootProps } from './SliderRoot.types'; +import type { SliderRoot } from './SliderRoot'; type Touches = Array>; @@ -32,7 +32,7 @@ function createTouches(touches: Touches) { }; } -function TestSlider(props: SliderRootProps) { +function TestSlider(props: SliderRoot.Props) { return ( @@ -46,7 +46,7 @@ function TestSlider(props: SliderRootProps) { ); } -function TestRangeSlider(props: SliderRootProps) { +function TestRangeSlider(props: SliderRoot.Props) { return ( diff --git a/packages/mui-base/src/Slider/Root/SliderRoot.tsx b/packages/mui-base/src/Slider/Root/SliderRoot.tsx index da75649129..9f3e124c29 100644 --- a/packages/mui-base/src/Slider/Root/SliderRoot.tsx +++ b/packages/mui-base/src/Slider/Root/SliderRoot.tsx @@ -1,13 +1,13 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; +import type { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import type { FieldRootOwnerState } from '../../Field/Root/FieldRoot.types'; import { sliderStyleHookMapping } from './styleHooks'; import { useSliderRoot } from './useSliderRoot'; import { SliderProvider } from './SliderProvider'; -import { SliderRootProps, SliderRootOwnerState } from './SliderRoot.types'; import { useFieldRootContext } from '../../Field/Root/FieldRootContext'; - /** * * Demos: @@ -19,7 +19,7 @@ import { useFieldRootContext } from '../../Field/Root/FieldRootContext'; * - [SliderRoot API](https://base-ui.netlify.app/components/react-slider/#api-reference-SliderRoot) */ const SliderRoot = React.forwardRef(function SliderRoot( - props: SliderRootProps, + props: SliderRoot.Props, forwardedRef: React.ForwardedRef, ) { const { @@ -56,7 +56,7 @@ const SliderRoot = React.forwardRef(function SliderRoot( ...otherProps, }); - const ownerState: SliderRootOwnerState = React.useMemo( + const ownerState: SliderRoot.OwnerState = React.useMemo( () => ({ ...fieldOwnerState, activeThumbIndex: slider.active, @@ -136,36 +136,16 @@ SliderRoot.propTypes /* remove-proptypes */ = { * @default false */ disabled: PropTypes.bool, - /** - * The id of the slider element. - */ - id: PropTypes.string, /** * The granularity with which the slider can step through values when using Page Up/Page Down or Shift + Arrow Up/Arrow Down. * @default 10 */ largeStep: PropTypes.number, - /** - * The maximum allowed value of the slider. - * Should not be equal to min. - * @default 100 - */ - max: PropTypes.number, - /** - * The minimum allowed value of the slider. - * Should not be equal to max. - * @default 0 - */ - min: PropTypes.number, /** * The minimum steps between values in a range slider. * @default 0 */ minStepsBetweenValues: PropTypes.number, - /** - * Name attribute of the hidden `input` element. - */ - name: PropTypes.string, /** * Callback function that is fired when the slider's value changed. * @@ -193,17 +173,6 @@ SliderRoot.propTypes /* remove-proptypes */ = { * A function to customize rendering of the component. */ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), - /** - * 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. - * @default 1 - */ - step: PropTypes.number, - /** - * Tab index attribute of the Thumb component's `input` element. - */ - tabIndex: PropTypes.number, /** * The value of the slider. * For ranged sliders, provide an array with two values. @@ -211,4 +180,67 @@ SliderRoot.propTypes /* remove-proptypes */ = { value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]), } as any; +export namespace SliderRoot { + export interface Context + extends Omit { + ownerState: OwnerState; + } + + export interface OwnerState extends FieldRootOwnerState { + /** + * The index of the active thumb. + */ + activeThumbIndex: number; + /** + * If `true`, the component is disabled. + */ + disabled: boolean; + /** + * If `true`, a thumb is being dragged by a pointer. + */ + dragging: boolean; + direction: useSliderRoot.Direction; + max: number; + min: number; + /** + * The minimum steps between values in a range slider. + * @default 0 + */ + minStepsBetweenValues: number; + /** + * The component orientation. + */ + orientation: useSliderRoot.Orientation; + /** + * The step increment of the slider when incrementing or decrementing. It will snap + * to multiples of this value. Decimal values are supported. + * @default 1 + */ + step: number; + /** + * The raw number value of the slider. + */ + values: ReadonlyArray; + } + + export interface Props + extends Omit, + Omit, 'defaultValue' | 'onChange' | 'values'> { + /** + * The default value of the slider. Use when the component is not controlled. + */ + defaultValue?: number | ReadonlyArray; + /** + * If `true`, the component is disabled. + * @default false + */ + disabled?: boolean; + /** + * The value of the slider. + * For ranged sliders, provide an array with two values. + */ + value?: number | ReadonlyArray; + } +} + export { SliderRoot }; diff --git a/packages/mui-base/src/Slider/Root/SliderRoot.types.ts b/packages/mui-base/src/Slider/Root/SliderRoot.types.ts deleted file mode 100644 index de137e0b37..0000000000 --- a/packages/mui-base/src/Slider/Root/SliderRoot.types.ts +++ /dev/null @@ -1,286 +0,0 @@ -import type { BaseUIComponentProps } from '../../utils/types'; -import type { CompoundComponentContextValue } from '../../useCompound'; -import type { FieldRootOwnerState } from '../../Field/Root/FieldRoot.types'; - -export interface SliderThumbMetadata { - inputId: string; - ref: React.RefObject; - inputRef: React.RefObject; -} - -export type SliderContextValue = Omit< - UseSliderReturnValue, - 'compoundComponentContextValue' | 'getRootProps' -> & { - ownerState: SliderRootOwnerState; -}; - -export type SliderProviderValue = SliderContextValue & { - compoundComponentContextValue: CompoundComponentContextValue; -}; - -export type SliderDirection = 'ltr' | 'rtl'; - -export type SliderOrientation = 'horizontal' | 'vertical'; - -export interface SliderRootOwnerState extends FieldRootOwnerState { - /** - * The index of the active thumb. - */ - activeThumbIndex: number; - /** - * If `true`, the component is disabled. - */ - disabled: boolean; - /** - * If `true`, a thumb is being dragged by a pointer. - */ - dragging: boolean; - direction: SliderDirection; - max: number; - min: number; - /** - * The minimum steps between values in a range slider. - * @default 0 - */ - minStepsBetweenValues: number; - /** - * The component orientation. - */ - orientation: SliderOrientation; - /** - * The step increment of the slider when incrementing or decrementing. It will snap - * to multiples of this value. Decimal values are supported. - * @default 1 - */ - step: number; - /** - * The raw number value of the slider. - */ - values: ReadonlyArray; -} - -export interface SliderRootProps - extends Omit, - Omit< - BaseUIComponentProps<'span', SliderRootOwnerState>, - 'defaultValue' | 'onChange' | 'values' - > { - /** - * The default value of the slider. Use when the component is not controlled. - */ - defaultValue?: number | ReadonlyArray; - /** - * If `true`, the component is disabled. - * @default false - */ - disabled?: boolean; - /** - * The value of the slider. - * For ranged sliders, provide an array with two values. - */ - value?: number | ReadonlyArray; -} - -export interface UseSliderParameters { - /** - * The id of the slider element. - */ - id?: string; - /** - * The id of the element containing a label for the slider. - */ - 'aria-labelledby'?: string; - /** - * The default value. Use when the component is not controlled. - */ - defaultValue?: number | ReadonlyArray; - /** - * Sets the direction. For right-to-left languages, the lowest value is on the right-hand side. - * @default 'ltr' - */ - direction?: SliderDirection; - /** - * If `true`, the component is disabled. - * @default false - */ - disabled?: boolean; - /** - * 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; - /** - * The minimum steps between values in a range slider. - * @default 0 - */ - minStepsBetweenValues?: number; - /** - * Name attribute of the hidden `input` element. - */ - name?: string; - /** - * Callback function that is fired when the slider's value changed. - * - * @param {number | number[]} value The new value. - * @param {number} activeThumb Index of the currently moved thumb. - * @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. - */ - onValueChange?: (value: number | number[], activeThumb: number, event: Event) => void; - /** - * Callback function that is fired when the `pointerup` is triggered. - * - * @param {number | number[]} value The new value. - * @param {Event} event The event source of the callback. - * **Warning**: This is a generic event not a change event. - */ - onValueCommitted?: (value: number | number[], event: Event) => void; - /** - * The component orientation. - * @default 'horizontal' - */ - orientation?: SliderOrientation; - /** - * The ref attached to the root of the Slider. - */ - rootRef?: React.Ref; - /** - * The granularity with which the slider can step through values when using Page Up/Page Down or Shift + Arrow Up/Arrow Down. - * @default 10 - */ - largeStep?: 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. - * @default 1 - */ - step?: number; - /** - * Tab index attribute of the Thumb component's `input` element. - */ - tabIndex?: number; - /** - * The value of the slider. - * For ranged sliders, provide an array with two values. - */ - value?: number | ReadonlyArray; -} - -export type Axis = SliderOrientation | 'horizontal-reverse'; - -export interface AxisProps { - offset: ( - percent: number, - ) => T extends 'horizontal' - ? { left: string } - : T extends 'vertical' - ? { bottom: string } - : T extends 'horizontal-reverse' - ? { right: string } - : never; - leap: ( - percent: number, - ) => T extends 'horizontal' | 'horizontal-reverse' - ? { width: string } - : T extends 'vertical' - ? { height: string } - : never; -} - -export interface UseSliderReturnValue { - getRootProps: ( - externalProps?: React.ComponentPropsWithRef<'span'>, - ) => React.ComponentPropsWithRef<'span'>; - /** - * The index of the active thumb. - */ - active: number; - /** - * A function that compares a new value with the internal value of the slider. - * The internal value is potentially unsorted, e.g. to support frozen arrays: https://github.com/mui/material-ui/pull/28472 - */ - areValuesEqual: (newValue: number | ReadonlyArray) => boolean; - 'aria-labelledby'?: string; - /** - * The orientation of the slider. - */ - axis: Axis; - changeValue: ( - valueInput: number, - index: number, - event: React.KeyboardEvent | React.ChangeEvent, - ) => void; - compoundComponentContextValue: CompoundComponentContextValue; - dragging: boolean; - direction: SliderDirection; - disabled: boolean; - getFingerNewValue: (args: { - finger: { x: number; y: number }; - move?: boolean; - offset?: number; - activeIndex?: number; - }) => { newValue: number | number[]; activeIndex: number; newPercentageValue: number } | null; - handleValueChange: ( - value: number | number[], - activeThumb: number, - event: React.SyntheticEvent | Event, - ) => void; - /** - * The large step value of the slider when incrementing or decrementing while the shift key is held, - * or when using Page-Up or Page-Down keys. Snaps to multiples of this value. - * @default 10 - */ - largeStep: number; - /** - * The maximum allowed value of the slider. - */ - max: number; - /** - * The minimum allowed value of the slider. - */ - min: number; - /** - * The minimum steps between values in a range slider. - */ - minStepsBetweenValues: number; - name?: string; - onValueCommitted?: (value: number | number[], event: Event) => void; - /** - * The component orientation. - * @default 'horizontal' - */ - orientation: SliderOrientation; - registerSliderControl: (element: HTMLElement | null) => void; - /** - * The value(s) of the slider as percentages - */ - percentageValues: readonly number[]; - setActive: (activeIndex: number) => void; - setDragging: (isDragging: boolean) => void; - setValueState: (newValue: number | number[]) => void; - /** - * The step increment of the slider when incrementing or decrementing. It will snap - * to multiples of this value. Decimal values are supported. - * @default 1 - */ - step: number; - /** - * A map containing all the Thumb components registered to the slider - */ - subitems: Map; - tabIndex?: number; - /** - * The value(s) of the slider - */ - values: readonly number[]; -} diff --git a/packages/mui-base/src/Slider/Root/styleHooks.ts b/packages/mui-base/src/Slider/Root/styleHooks.ts index e90a3d172c..72b3b2879e 100644 --- a/packages/mui-base/src/Slider/Root/styleHooks.ts +++ b/packages/mui-base/src/Slider/Root/styleHooks.ts @@ -1,7 +1,7 @@ import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; -import type { SliderRootOwnerState } from './SliderRoot.types'; +import type { SliderRoot } from './SliderRoot'; -export const sliderStyleHookMapping: CustomStyleHookMapping = { +export const sliderStyleHookMapping: CustomStyleHookMapping = { activeThumbIndex: () => null, direction: () => null, max: () => null, diff --git a/packages/mui-base/src/Slider/Root/useSliderRoot.ts b/packages/mui-base/src/Slider/Root/useSliderRoot.ts index f7adf45df7..7e371a29bc 100644 --- a/packages/mui-base/src/Slider/Root/useSliderRoot.ts +++ b/packages/mui-base/src/Slider/Root/useSliderRoot.ts @@ -6,10 +6,10 @@ import { mergeReactProps } from '../../utils/mergeReactProps'; import { ownerDocument } from '../../utils/owner'; import { useControlled } from '../../utils/useControlled'; import { useForkRef } from '../../utils/useForkRef'; -import { useCompoundParent } from '../../useCompound'; +import { type CompoundComponentContextValue, useCompoundParent } from '../../useCompound'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { percentToValue, roundValueToStep, valueToPercent } from '../utils'; -import { SliderThumbMetadata, UseSliderParameters, UseSliderReturnValue } from './SliderRoot.types'; +import type { useSliderThumb } from '../Thumb/useSliderThumb'; import { useFieldRootContext } from '../../Field/Root/FieldRootContext'; import { useId } from '../../utils/useId'; import { useFieldControlValidation } from '../../Field/Control/useFieldControlValidation'; @@ -119,7 +119,7 @@ export function trackFinger( * * - [useSliderRoot API](https://mui.com/base-ui/react-slider/hooks-api/#use-slider-root) */ -function useSliderRoot(parameters: UseSliderParameters): UseSliderReturnValue { +export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRoot.ReturnValue { const { 'aria-labelledby': ariaLabelledby, id: idProp, @@ -191,7 +191,7 @@ function useSliderRoot(parameters: UseSliderParameters): UseSliderReturnValue { const { contextValue: compoundComponentContextValue, subitems } = useCompoundParent< string, - SliderThumbMetadata + useSliderThumb.Metadata >(); const handleValueChange = React.useCallback( @@ -389,7 +389,7 @@ function useSliderRoot(parameters: UseSliderParameters): UseSliderReturnValue { setActive(-1); } - const getRootProps: UseSliderReturnValue['getRootProps'] = React.useCallback( + const getRootProps: useSliderRoot.ReturnValue['getRootProps'] = React.useCallback( (externalProps = {}) => mergeReactProps(getValidationProps(externalProps), { 'aria-labelledby': ariaLabelledby, @@ -465,4 +465,190 @@ function useSliderRoot(parameters: UseSliderParameters): UseSliderReturnValue { ); } -export { useSliderRoot }; +export namespace useSliderRoot { + export type Direction = 'ltr' | 'rtl'; + + export type Orientation = 'horizontal' | 'vertical'; + + export interface Parameters { + /** + * The id of the slider element. + */ + id?: string; + /** + * The id of the element containing a label for the slider. + */ + 'aria-labelledby'?: string; + /** + * The default value. Use when the component is not controlled. + */ + defaultValue?: number | ReadonlyArray; + /** + * Sets the direction. For right-to-left languages, the lowest value is on the right-hand side. + * @default 'ltr' + */ + direction?: Direction; + /** + * If `true`, the component is disabled. + * @default false + */ + disabled?: boolean; + /** + * 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; + /** + * The minimum steps between values in a range slider. + * @default 0 + */ + minStepsBetweenValues?: number; + /** + * Name attribute of the hidden `input` element. + */ + name?: string; + /** + * Callback function that is fired when the slider's value changed. + * + * @param {number | number[]} value The new value. + * @param {number} activeThumb Index of the currently moved thumb. + * @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. + */ + onValueChange?: (value: number | number[], activeThumb: number, event: Event) => void; + /** + * Callback function that is fired when the `pointerup` is triggered. + * + * @param {number | number[]} value The new value. + * @param {Event} event The event source of the callback. + * **Warning**: This is a generic event not a change event. + */ + onValueCommitted?: (value: number | number[], event: Event) => void; + /** + * The component orientation. + * @default 'horizontal' + */ + orientation?: Orientation; + /** + * The ref attached to the root of the Slider. + */ + rootRef?: React.Ref; + /** + * The granularity with which the slider can step through values when using Page Up/Page Down or Shift + Arrow Up/Arrow Down. + * @default 10 + */ + largeStep?: 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. + * @default 1 + */ + step?: number; + /** + * Tab index attribute of the Thumb component's `input` element. + */ + tabIndex?: number; + /** + * The value of the slider. + * For ranged sliders, provide an array with two values. + */ + value?: number | ReadonlyArray; + } + + export interface ReturnValue { + getRootProps: ( + externalProps?: React.ComponentPropsWithRef<'span'>, + ) => React.ComponentPropsWithRef<'span'>; + /** + * The index of the active thumb. + */ + active: number; + /** + * A function that compares a new value with the internal value of the slider. + * The internal value is potentially unsorted, e.g. to support frozen arrays: https://github.com/mui/material-ui/pull/28472 + */ + areValuesEqual: (newValue: number | ReadonlyArray) => boolean; + 'aria-labelledby'?: string; + /** + * The orientation of the slider. + */ + axis: Orientation | 'horizontal-reverse'; + changeValue: ( + valueInput: number, + index: number, + event: React.KeyboardEvent | React.ChangeEvent, + ) => void; + compoundComponentContextValue: CompoundComponentContextValue; + dragging: boolean; + direction: Direction; + disabled: boolean; + getFingerNewValue: (args: { + finger: { x: number; y: number }; + move?: boolean; + offset?: number; + activeIndex?: number; + }) => { newValue: number | number[]; activeIndex: number; newPercentageValue: number } | null; + handleValueChange: ( + value: number | number[], + activeThumb: number, + event: React.SyntheticEvent | Event, + ) => void; + /** + * The large step value of the slider when incrementing or decrementing while the shift key is held, + * or when using Page-Up or Page-Down keys. Snaps to multiples of this value. + * @default 10 + */ + largeStep: number; + /** + * The maximum allowed value of the slider. + */ + max: number; + /** + * The minimum allowed value of the slider. + */ + min: number; + /** + * The minimum steps between values in a range slider. + */ + minStepsBetweenValues: number; + name?: string; + onValueCommitted?: (value: number | number[], event: Event) => void; + /** + * The component orientation. + * @default 'horizontal' + */ + orientation: Orientation; + registerSliderControl: (element: HTMLElement | null) => void; + /** + * The value(s) of the slider as percentages + */ + percentageValues: readonly number[]; + setActive: (activeIndex: number) => void; + setDragging: (isDragging: boolean) => void; + setValueState: (newValue: number | number[]) => void; + /** + * The step increment of the slider when incrementing or decrementing. It will snap + * to multiples of this value. Decimal values are supported. + * @default 1 + */ + step: number; + /** + * A map containing all the Thumb components registered to the slider + */ + subitems: Map; + tabIndex?: number; + /** + * The value(s) of the slider + */ + values: readonly number[]; + } +} diff --git a/packages/mui-base/src/Slider/Thumb/SliderThumb.test.tsx b/packages/mui-base/src/Slider/Thumb/SliderThumb.test.tsx index 3d65198534..620e672f91 100644 --- a/packages/mui-base/src/Slider/Thumb/SliderThumb.test.tsx +++ b/packages/mui-base/src/Slider/Thumb/SliderThumb.test.tsx @@ -1,14 +1,14 @@ import * as React from 'react'; import * as Slider from '@base_ui/react/Slider'; -import { SliderProvider, type SliderProviderValue } from '@base_ui/react/Slider'; import { createRenderer, describeConformance } from '#test-utils'; +import { SliderProvider } from '../Root/SliderProvider'; const NOOP = () => {}; describe('', () => { const { render } = createRenderer(); - const testProviderValue: SliderProviderValue = { + const testProviderValue: SliderProvider.Value = { active: -1, areValuesEqual: () => true, axis: 'horizontal', diff --git a/packages/mui-base/src/Slider/Thumb/SliderThumb.tsx b/packages/mui-base/src/Slider/Thumb/SliderThumb.tsx index 867db5bdd8..3339db178c 100644 --- a/packages/mui-base/src/Slider/Thumb/SliderThumb.tsx +++ b/packages/mui-base/src/Slider/Thumb/SliderThumb.tsx @@ -4,9 +4,10 @@ import PropTypes from 'prop-types'; import { getStyleHookProps } from '../../utils/getStyleHookProps'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { resolveClassName } from '../../utils/resolveClassName'; +import { BaseUIComponentProps } from '../../utils/types'; import { useForkRef } from '../../utils/useForkRef'; +import type { SliderRoot } from '../Root/SliderRoot'; import { useSliderContext } from '../Root/SliderProvider'; -import { SliderThumbProps } from './SliderThumb.types'; import { useSliderThumb } from './useSliderThumb'; function defaultRender( @@ -21,7 +22,6 @@ function defaultRender( ); } - /** * * Demos: @@ -33,7 +33,7 @@ function defaultRender( * - [SliderThumb API](https://base-ui.netlify.app/components/react-slider/#api-reference-SliderThumb) */ const SliderThumb = React.forwardRef(function SliderThumb( - props: SliderThumbProps, + props: SliderThumb.Props, forwardedRef: React.ForwardedRef, ) { const { @@ -204,4 +204,28 @@ SliderThumb.propTypes /* remove-proptypes */ = { ]), } as any; +export namespace SliderThumb { + export interface OwnerState extends SliderRoot.OwnerState {} + + export interface Props + extends Partial>, + Omit, 'render'> { + onPointerLeave?: React.PointerEventHandler; + onPointerOver?: React.PointerEventHandler; + onBlur?: React.FocusEventHandler; + onFocus?: React.FocusEventHandler; + onKeyDown?: React.KeyboardEventHandler; + /** + * A function to customize rendering of the component. + */ + render?: + | (( + props: React.ComponentPropsWithRef<'span'>, + inputProps: React.ComponentPropsWithRef<'input'>, + state: OwnerState, + ) => React.ReactElement) + | (React.ReactElement & { ref: React.Ref }); + } +} + export { SliderThumb }; diff --git a/packages/mui-base/src/Slider/Thumb/SliderThumb.types.ts b/packages/mui-base/src/Slider/Thumb/SliderThumb.types.ts deleted file mode 100644 index 84fe617637..0000000000 --- a/packages/mui-base/src/Slider/Thumb/SliderThumb.types.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { BaseUIComponentProps } from '../../utils/types'; -import { SliderRootOwnerState, UseSliderReturnValue } from '../Root/SliderRoot.types'; - -export interface SliderThumbOwnerState extends SliderRootOwnerState {} - -export interface SliderThumbProps - extends Partial>, - Omit, 'render'> { - onPointerLeave?: React.PointerEventHandler; - onPointerOver?: React.PointerEventHandler; - onBlur?: React.FocusEventHandler; - onFocus?: React.FocusEventHandler; - onKeyDown?: React.KeyboardEventHandler; - /** - * A function to customize rendering of the component. - */ - render?: - | (( - props: React.ComponentPropsWithRef<'span'>, - inputProps: React.ComponentPropsWithRef<'input'>, - state: SliderThumbOwnerState, - ) => React.ReactElement) - | (React.ReactElement & { ref: React.Ref }); -} - -export interface UseSliderThumbParameters - extends Pick< - UseSliderReturnValue, - | 'active' - | 'aria-labelledby' - | 'axis' - | 'changeValue' - | 'direction' - | 'largeStep' - | 'max' - | 'min' - | 'minStepsBetweenValues' - | 'name' - | 'orientation' - | 'percentageValues' - | 'step' - | 'tabIndex' - | 'values' - > { - /** - * The label for the input element. - */ - 'aria-label'?: string; - /** - * A string value that provides a user-friendly name for the current value of the slider. - */ - 'aria-valuetext'?: string; - /** - * Accepts a function which returns a string value that provides a user-friendly name for the input associated with the thumb - * @param {number} index The index of the input - * @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; - id?: string; - disabled?: boolean; - onBlur?: React.FocusEventHandler; - onFocus?: React.FocusEventHandler; - onKeyDown?: React.KeyboardEventHandler; - rootRef?: React.Ref; -} - -export interface UseSliderThumbReturnValue { - getRootProps: ( - externalProps?: React.ComponentPropsWithRef, - ) => React.ComponentPropsWithRef; - getThumbInputProps: ( - externalProps?: React.ComponentPropsWithRef<'input'>, - ) => React.ComponentPropsWithRef<'input'>; - /** - * Resolver for the thumb slot's style prop. - * @param index of the currently moved thumb - * @returns props that should be spread on the style prop of thumb slot - */ - getThumbStyle: (index: number) => Record; - index: number; -} diff --git a/packages/mui-base/src/Slider/Thumb/useSliderThumb.ts b/packages/mui-base/src/Slider/Thumb/useSliderThumb.ts index a14a728403..db93ce898a 100644 --- a/packages/mui-base/src/Slider/Thumb/useSliderThumb.ts +++ b/packages/mui-base/src/Slider/Thumb/useSliderThumb.ts @@ -1,12 +1,12 @@ 'use client'; import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; +import { GenericHTMLProps } from '../../utils/types'; import { useForkRef } from '../../utils/useForkRef'; import { useId } from '../../utils/useId'; import { visuallyHidden } from '../../utils/visuallyHidden'; import { useCompoundItem } from '../../useCompound'; -import { SliderThumbMetadata } from '../Root/SliderRoot.types'; -import { UseSliderThumbParameters, UseSliderThumbReturnValue } from './SliderThumb.types'; +import type { useSliderRoot } from '../Root/useSliderRoot'; import { useFieldControlValidation } from '../../Field/Control/useFieldControlValidation'; import { useFieldRootContext } from '../../Field/Root/FieldRootContext'; import { getSliderValue } from '../utils/getSliderValue'; @@ -41,7 +41,7 @@ function getDefaultAriaValueText(values: readonly number[], index: number): stri return undefined; } -export function useSliderThumb(parameters: UseSliderThumbParameters) { +export function useSliderThumb(parameters: useSliderThumb.Parameters): useSliderThumb.ReturnValue { const { active: activeIndex, 'aria-label': ariaLabel, @@ -81,7 +81,7 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) { const mergedInputRef = useForkRef(inputRef, inputValidationRef); const handleRef = useForkRef(externalRef, thumbRef); - const thumbMetadata: SliderThumbMetadata = React.useMemo( + const thumbMetadata: useSliderThumb.Metadata = React.useMemo( () => ({ inputId: thumbId ? `${thumbId}-input` : '', ref: thumbRef, inputRef }), [thumbId], ); @@ -120,7 +120,7 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) { }; }, [activeIndex, axis, isRtl, orientation, percent, index]); - const getRootProps: UseSliderThumbReturnValue['getRootProps'] = React.useCallback( + const getRootProps: useSliderThumb.ReturnValue['getRootProps'] = React.useCallback( (externalProps = {}) => { return mergeReactProps(externalProps, { 'data-index': index, @@ -230,7 +230,7 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) { ], ); - const getThumbInputProps: UseSliderThumbReturnValue['getThumbInputProps'] = React.useCallback( + const getThumbInputProps: useSliderThumb.ReturnValue['getThumbInputProps'] = React.useCallback( (externalProps = {}) => { return mergeReactProps(getInputValidationProps(externalProps), { 'aria-label': getAriaLabel ? getAriaLabel(index) : ariaLabel, @@ -299,3 +299,69 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) { [getRootProps, getThumbInputProps, index, disabled], ); } + +export namespace useSliderThumb { + export interface Metadata { + inputId: string; + ref: React.RefObject; + inputRef: React.RefObject; + } + + export interface Parameters + extends Pick< + useSliderRoot.ReturnValue, + | 'active' + | 'aria-labelledby' + | 'axis' + | 'changeValue' + | 'direction' + | 'largeStep' + | 'max' + | 'min' + | 'minStepsBetweenValues' + | 'name' + | 'orientation' + | 'percentageValues' + | 'step' + | 'tabIndex' + | 'values' + > { + /** + * The label for the input element. + */ + 'aria-label'?: string; + /** + * A string value that provides a user-friendly name for the current value of the slider. + */ + 'aria-valuetext'?: string; + /** + * Accepts a function which returns a string value that provides a user-friendly name for the input associated with the thumb + * @param {number} index The index of the input + * @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; + id?: string; + disabled: boolean; + onBlur?: React.FocusEventHandler; + onFocus?: React.FocusEventHandler; + onKeyDown?: React.KeyboardEventHandler; + rootRef?: React.Ref; + } + + export interface ReturnValue { + getRootProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + getThumbInputProps: ( + externalProps?: React.ComponentPropsWithRef<'input'>, + ) => React.ComponentPropsWithRef<'input'>; + index: number; + disabled: boolean; + } +} diff --git a/packages/mui-base/src/Slider/Track/SliderTrack.test.tsx b/packages/mui-base/src/Slider/Track/SliderTrack.test.tsx index a28829b12e..37f3c8a71c 100644 --- a/packages/mui-base/src/Slider/Track/SliderTrack.test.tsx +++ b/packages/mui-base/src/Slider/Track/SliderTrack.test.tsx @@ -1,14 +1,14 @@ import * as React from 'react'; import * as Slider from '@base_ui/react/Slider'; -import { SliderProvider, type SliderProviderValue } from '@base_ui/react/Slider'; import { createRenderer, describeConformance } from '#test-utils'; +import { SliderProvider } from '../Root/SliderProvider'; const NOOP = () => {}; describe('', () => { const { render } = createRenderer(); - const testProviderValue: SliderProviderValue = { + const testProviderValue: SliderProvider.Value = { active: -1, areValuesEqual: () => true, axis: 'horizontal', diff --git a/packages/mui-base/src/Slider/Track/SliderTrack.tsx b/packages/mui-base/src/Slider/Track/SliderTrack.tsx index 33726a84a3..4447e07294 100644 --- a/packages/mui-base/src/Slider/Track/SliderTrack.tsx +++ b/packages/mui-base/src/Slider/Track/SliderTrack.tsx @@ -1,11 +1,11 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; +import { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useSliderContext } from '../Root/SliderProvider'; +import type { SliderRoot } from '../Root/SliderRoot'; import { sliderStyleHookMapping } from '../Root/styleHooks'; -import { SliderTrackProps } from './SliderTrack.types'; - /** * * Demos: @@ -17,8 +17,8 @@ import { SliderTrackProps } from './SliderTrack.types'; * - [SliderTrack API](https://base-ui.netlify.app/components/react-slider/#api-reference-SliderTrack) */ const SliderTrack = React.forwardRef(function SliderTrack( - props: SliderTrackProps, - forwardedRef: React.ForwardedRef, + props: SliderTrack.Props, + forwardedRef: React.ForwardedRef, ) { const { render, className, ...otherProps } = props; @@ -55,4 +55,8 @@ SliderTrack.propTypes /* remove-proptypes */ = { render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), } as any; +export namespace SliderTrack { + export interface Props extends BaseUIComponentProps<'span', SliderRoot.OwnerState> {} +} + export { SliderTrack }; diff --git a/packages/mui-base/src/Slider/Track/SliderTrack.types.ts b/packages/mui-base/src/Slider/Track/SliderTrack.types.ts deleted file mode 100644 index edda2fcbf6..0000000000 --- a/packages/mui-base/src/Slider/Track/SliderTrack.types.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { BaseUIComponentProps } from '../../utils/types'; -import { SliderRootOwnerState } from '../Root/SliderRoot.types'; - -export interface SliderTrackProps extends BaseUIComponentProps<'span', SliderRootOwnerState> {} diff --git a/packages/mui-base/src/Slider/index.barrel.ts b/packages/mui-base/src/Slider/index.barrel.ts index 79a640ba70..89d007d908 100644 --- a/packages/mui-base/src/Slider/index.barrel.ts +++ b/packages/mui-base/src/Slider/index.barrel.ts @@ -1,40 +1,17 @@ export { SliderRoot } from './Root/SliderRoot'; -export type * from './Root/SliderRoot.types'; export { useSliderRoot } from './Root/useSliderRoot'; -export * from './Root/SliderProvider'; +export { SliderContext, SliderProvider, useSliderContext } from './Root/SliderProvider'; export { SliderOutput } from './Output/SliderOutput'; -export type { - SliderOutputProps as OutputProps, - UseSliderOutputParameters, - UseSliderOutputReturnValue, -} from './Output/SliderOutput.types'; export { useSliderOutput } from './Output/useSliderOutput'; export { SliderControl } from './Control/SliderControl'; -export type { - SliderControlProps as ControlProps, - UseSliderControlParameters, - UseSliderControlReturnValue, -} from './Control/SliderControl.types'; export { useSliderControl } from './Control/useSliderControl'; export { SliderTrack } from './Track/SliderTrack'; -export type { SliderTrackProps } from './Track/SliderTrack.types'; export { SliderThumb } from './Thumb/SliderThumb'; -export type { - SliderThumbOwnerState, - SliderThumbProps, - UseSliderThumbParameters, - UseSliderThumbReturnValue, -} from './Thumb/SliderThumb.types'; export { useSliderThumb } from './Thumb/useSliderThumb'; export { SliderIndicator } from './Indicator/SliderIndicator'; -export type { - SliderIndicatorProps as IndicatorProps, - UseSliderIndicatorParameters, - UseSliderIndicatorReturnValue, -} from './Indicator/SliderIndicator.types'; export { useSliderIndicator } from './Indicator/useSliderIndicator'; diff --git a/packages/mui-base/src/Slider/index.ts b/packages/mui-base/src/Slider/index.ts index be5d9c2dc2..55014dff4a 100644 --- a/packages/mui-base/src/Slider/index.ts +++ b/packages/mui-base/src/Slider/index.ts @@ -1,49 +1,17 @@ export { SliderRoot as Root } from './Root/SliderRoot'; -export type { - SliderRootOwnerState as SliderOwnerState, - SliderRootProps as RootProps, - UseSliderParameters, - UseSliderReturnValue, - SliderContextValue, - SliderProviderValue, - SliderThumbMetadata, - Axis, -} from './Root/SliderRoot.types'; export { useSliderRoot } from './Root/useSliderRoot'; -export * from './Root/SliderProvider'; +export { SliderContext, SliderProvider, useSliderContext } from './Root/SliderProvider'; export { SliderOutput as Output } from './Output/SliderOutput'; -export type { - SliderOutputProps as OutputProps, - UseSliderOutputParameters, - UseSliderOutputReturnValue, -} from './Output/SliderOutput.types'; export { useSliderOutput } from './Output/useSliderOutput'; export { SliderControl as Control } from './Control/SliderControl'; -export type { - SliderControlProps as ControlProps, - UseSliderControlParameters, - UseSliderControlReturnValue, -} from './Control/SliderControl.types'; export { useSliderControl } from './Control/useSliderControl'; export { SliderTrack as Track } from './Track/SliderTrack'; -export type { SliderTrackProps as TrackProps } from './Track/SliderTrack.types'; export { SliderThumb as Thumb } from './Thumb/SliderThumb'; -export type { - SliderThumbOwnerState as ThumbOwnerState, - SliderThumbProps as ThumbProps, - UseSliderThumbParameters, - UseSliderThumbReturnValue, -} from './Thumb/SliderThumb.types'; export { useSliderThumb } from './Thumb/useSliderThumb'; export { SliderIndicator as Indicator } from './Indicator/SliderIndicator'; -export type { - SliderIndicatorProps as IndicatorProps, - UseSliderIndicatorParameters, - UseSliderIndicatorReturnValue, -} from './Indicator/SliderIndicator.types'; export { useSliderIndicator } from './Indicator/useSliderIndicator'; From e47af47ad16b1989b500f936df9bdcd7d4e9c4e7 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 11 Sep 2024 00:34:20 +0800 Subject: [PATCH 02/12] Replace useCompound with CompositeList --- docs/app/experiments/slider.tsx | 10 +- docs/data/components/slider/RtlSlider.js | 8 +- docs/data/components/slider/RtlSlider.tsx | 8 +- .../css-modules/index.js | 10 +- .../css-modules/index.tsx | 10 +- .../system/index.js | 8 +- .../system/index.tsx | 8 +- .../tailwind/index.js | 10 +- .../tailwind/index.tsx | 10 +- docs/data/components/slider/VerticalSlider.js | 8 +- .../data/components/slider/VerticalSlider.tsx | 8 +- .../src/Slider/Control/SliderControl.test.tsx | 95 +++++++-------- .../src/Slider/Control/SliderControl.tsx | 6 +- .../src/Slider/Control/useSliderControl.ts | 13 +-- .../Slider/Indicator/SliderIndicator.test.tsx | 95 +++++++-------- .../src/Slider/Indicator/SliderIndicator.tsx | 2 +- .../src/Slider/Output/SliderOutput.test.tsx | 95 +++++++-------- .../src/Slider/Output/SliderOutput.tsx | 6 +- .../src/Slider/Output/useSliderOutput.ts | 32 ++--- .../src/Slider/Root/SliderContext.tsx | 20 ++++ .../src/Slider/Root/SliderProvider.tsx | 110 ------------------ .../mui-base/src/Slider/Root/SliderRoot.tsx | 9 +- .../mui-base/src/Slider/Root/useSliderRoot.ts | 62 +++++++--- .../src/Slider/Thumb/SliderThumb.test.tsx | 95 +++++++-------- .../mui-base/src/Slider/Thumb/SliderThumb.tsx | 4 +- .../src/Slider/Thumb/useSliderThumb.ts | 76 ++++++------ .../src/Slider/Track/SliderTrack.test.tsx | 95 +++++++-------- .../mui-base/src/Slider/Track/SliderTrack.tsx | 2 +- packages/mui-base/src/Slider/index.barrel.ts | 2 +- packages/mui-base/src/Slider/index.ts | 2 +- 30 files changed, 408 insertions(+), 511 deletions(-) create mode 100644 packages/mui-base/src/Slider/Root/SliderContext.tsx delete mode 100644 packages/mui-base/src/Slider/Root/SliderProvider.tsx diff --git a/docs/app/experiments/slider.tsx b/docs/app/experiments/slider.tsx index 449930434d..89661a7871 100644 --- a/docs/app/experiments/slider.tsx +++ b/docs/app/experiments/slider.tsx @@ -233,15 +233,11 @@ function Label(props: any) { const defaultId = React.useId(); const labelId = idProp ?? defaultId; - const { subitems } = useSliderContext(); + const { inputIdMap } = useSliderContext(); - const htmlFor = Array.from(subitems.values()) - .reduce((acc, item) => { - return `${acc} ${item.inputId}`; - }, '') - .trim(); + const inputId = inputIdMap.get(0); - return - ); } -function Styles() { - return ( - - ); -} - function Label(props) { const { id, ...otherProps } = props; const { inputIdMap, disabled } = Slider.useSliderContext(); @@ -155,16 +50,3 @@ function useIsDarkMode() { const theme = useTheme(); return theme.palette.mode === 'dark'; } - -const grey = { - 50: '#F3F6F9', - 100: '#E5EAF2', - 200: '#DAE2ED', - 300: '#C7D0DD', - 400: '#B0B8C4', - 500: '#9DA8B7', - 600: '#6B7A90', - 700: '#434D5B', - 800: '#303740', - 900: '#1C2025', -}; diff --git a/docs/data/components/slider/VerticalSlider.tsx b/docs/data/components/slider/VerticalSlider.tsx index 3785c5fe29..059ef44a37 100644 --- a/docs/data/components/slider/VerticalSlider.tsx +++ b/docs/data/components/slider/VerticalSlider.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import * as Slider from '@base_ui/react/Slider'; import { useTheme } from '@mui/system'; +import classes from './vertical.module.css'; export default function VerticalSlider() { // Replace this with your app logic for determining dark mode @@ -12,129 +13,23 @@ export default function VerticalSlider() { defaultValue={50} orientation="vertical" aria-labelledby="VolumeSliderLabel" - className="VerticalSlider" + className={classes.slider} > - - ); } -function Styles() { - return ( - - ); -} - function Label(props: React.HTMLAttributes) { const { id, ...otherProps } = props; const { inputIdMap, disabled } = Slider.useSliderContext(); @@ -150,16 +45,3 @@ function useIsDarkMode() { const theme = useTheme(); return theme.palette.mode === 'dark'; } - -const grey = { - 50: '#F3F6F9', - 100: '#E5EAF2', - 200: '#DAE2ED', - 300: '#C7D0DD', - 400: '#B0B8C4', - 500: '#9DA8B7', - 600: '#6B7A90', - 700: '#434D5B', - 800: '#303740', - 900: '#1C2025', -}; diff --git a/docs/data/components/slider/styles.module.css b/docs/data/components/slider/styles.module.css new file mode 100644 index 0000000000..99aecc36a3 --- /dev/null +++ b/docs/data/components/slider/styles.module.css @@ -0,0 +1,99 @@ +.slider { + font-family: + IBM Plex Sans, + sans-serif; + font-size: 1rem; + width: 100%; + align-items: center; + position: relative; + -webkit-tap-highlight-color: transparent; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.output { + text-align: right; +} + +.control { + grid-column: 1/3; + display: flex; + align-items: center; + position: relative; + width: 100%; + height: 16px; + border-radius: 9999px; + touch-action: none; +} + +.track { + width: 100%; + height: 2px; + border-radius: 9999px; + background-color: var(--gray-400); + touch-action: none; +} + +.dark .track { + background-color: var(--gray-700); +} + +.control[data-disabled] { + cursor: not-allowed; +} + +.indicator { + height: 2px; + border-radius: 9999px; + background-color: var(--gray-900); +} + +.dark .indicator { + background-color: var(--gray-100); +} + +.thumb { + width: 16px; + height: 16px; + box-sizing: border-box; + border-radius: 50%; + background-color: var(--gray-900); + touch-action: none; + + &:focus-visible { + outline: 2px solid var(--gray-900); + outline-offset: 2px; + } + + &[data-dragging] { + background-color: pink; + } + + &[data-disabled] { + background-color: var(--gray-300); + } +} + +.dark .thumb { + background-color: var(--gray-100); + + &:focus-visible { + outline-color: var(--gray-300); + outline-width: 1px; + outline-offset: 3px; + } + + &[data-disabled] { + background-color: var(--gray-600); + } +} + +.label { + cursor: unset; + font-weight: bold; +} + +.label[data-disabled='true'] { + color: var(--gray-600); +} diff --git a/docs/data/components/slider/vertical.module.css b/docs/data/components/slider/vertical.module.css new file mode 100644 index 0000000000..e4b21c8497 --- /dev/null +++ b/docs/data/components/slider/vertical.module.css @@ -0,0 +1,106 @@ +.slider { + font-family: 'IBM Plex Sans', sans-serif; + font-size: 1rem; + height: 100%; + width: 5rem; + align-items: center; + position: relative; + -webkit-tap-highlight-color: transparent; + display: flex; + flex-flow: column-reverse nowrap; + gap: 1rem; +} + +.output { + font-size: 1.125rem; +} + +.control { + display: flex; + flex-flow: column nowrap; + align-items: center; + position: relative; + width: 16px; + height: 300px; + border-radius: 9999px; + touch-action: none; +} + +.track { + height: 100%; + width: 2px; + border-radius: 9999px; + background-color: var(--gray-400); + touch-action: none; +} + +.dark .track { + background-color: var(--gray-700); +} + +.control[data-disabled] { + cursor: not-allowed; +} + +.indicator { + border-radius: 9999px; + background-color: var(--gray-900); +} + +.dark .indicator { + position: absolute; + width: 16px; + height: 16px; + box-sizing: border-box; + border-radius: 50%; + background-color: var(--gray-900); + transform: translateY(50%); + touch-action: none; +} + +.thumb { + width: 16px; + height: 16px; + box-sizing: border-box; + border-radius: 50%; + background-color: var(--gray-900); + touch-action: none; + + &:focus-visible { + outline: 2px solid var(--gray-900); + outline-offset: 2px; + } + + &[data-dragging] { + background-color: pink; + } + + &[data-disabled] { + background-color: var(--gray-300); + } +} + +.dark .thumb { + background-color: var(--gray-100); + + &:focus-visible { + outline-color: var(--gray-300); + outline-width: 1px; + outline-offset: 3px; + } + + &[data-disabled] { + background-color: var(--gray-600); + } +} + +.label { + cursor: unset; + font-weight: 700; + font-size: 1rem; + color: inherit; +} + +.label[data-disabled='true'] { + color: var(--gray-600); +} From ee7e3fc7dc1179a27a465b4674bed8ff4db2264f Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 26 Sep 2024 09:53:21 +0200 Subject: [PATCH 04/12] Apply CSS writing mode on input element --- docs/data/components/slider/slider.mdx | 12 ++++++++++++ packages/mui-base/src/Slider/Thumb/useSliderThumb.ts | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/docs/data/components/slider/slider.mdx b/docs/data/components/slider/slider.mdx index f9c106aff6..3d60a072f5 100644 --- a/docs/data/components/slider/slider.mdx +++ b/docs/data/components/slider/slider.mdx @@ -171,6 +171,18 @@ To create vertical sliders, set the `orientation` prop to `"vertical"`. This wil + + Chrome versions below 124 does not implement `aria-orientation` correctly for vertical sliders ([Chromium issue #40736841](https://issues.chromium.org/issues/40736841)), and exposes them in the accessibility tree as `horizontal`. + + The `-webkit-appearance: slider-vertical` CSS property can be used to correct this, though it will trigger a console warning in newer Chrome versions: + ```css + .MySlider-thumb > input { + -webkit-appearance: slider-vertical; + } + ``` + The `Slider.Thumb` subcomponent automatically sets the CSS [`writing-mode`](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_writing_modes/Vertical_controls#range_sliders_meters_and_progress_bars) property that fixes this bug for Chrome 124 and newer. + + ## RTL Set the `direction` prop to `'rtl'` to change the slider's direction for right-to-left languages: diff --git a/packages/mui-base/src/Slider/Thumb/useSliderThumb.ts b/packages/mui-base/src/Slider/Thumb/useSliderThumb.ts index 9cb1f4c160..1caaf8d11e 100644 --- a/packages/mui-base/src/Slider/Thumb/useSliderThumb.ts +++ b/packages/mui-base/src/Slider/Thumb/useSliderThumb.ts @@ -233,6 +233,10 @@ export function useSliderThumb(parameters: useSliderThumb.Parameters): useSlider const getThumbInputProps: useSliderThumb.ReturnValue['getThumbInputProps'] = React.useCallback( (externalProps = {}) => { + let cssWritingMode; + if (orientation === 'vertical') { + cssWritingMode = isRtl ? 'vertical-rl' : 'vertical-lr'; + } return mergeReactProps(getInputValidationProps(externalProps), { 'aria-label': getAriaLabel ? getAriaLabel(index) : ariaLabel, 'aria-labelledby': ariaLabelledby, @@ -261,6 +265,7 @@ export function useSliderThumb(parameters: useSliderThumb.Parameters): useSlider // So that VoiceOver's focus indicator matches the thumb's dimensions width: '100%', height: '100%', + writingMode: cssWritingMode, }, tabIndex: -1, type: 'range', From 6ba298350e688f418d0fe5f552ca90af90c7cddc Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 27 Sep 2024 09:23:48 +0200 Subject: [PATCH 05/12] Clean up experiments --- .../slider-change-committed-lag.tsx | 15 +- docs/app/experiments/slider-template.tsx | 19 - docs/app/experiments/slider.module.css | 99 ++++ docs/app/experiments/slider.tsx | 433 ------------------ 4 files changed, 106 insertions(+), 460 deletions(-) delete mode 100644 docs/app/experiments/slider-template.tsx create mode 100644 docs/app/experiments/slider.module.css delete mode 100644 docs/app/experiments/slider.tsx diff --git a/docs/app/experiments/slider-change-committed-lag.tsx b/docs/app/experiments/slider-change-committed-lag.tsx index 8ced35a40a..10c664f6ef 100644 --- a/docs/app/experiments/slider-change-committed-lag.tsx +++ b/docs/app/experiments/slider-change-committed-lag.tsx @@ -3,7 +3,7 @@ // to cross check whether this issue would still occur in the new API import * as React from 'react'; import * as Slider from '@base_ui/react/Slider'; -import { Styles } from './slider'; +import classes from './slider.module.css'; export default function App() { const [val1, setVal1] = React.useState(80); @@ -17,23 +17,22 @@ export default function App() { }} > setVal1(newValue as number)} onValueCommitted={(newValue) => setVal2(newValue as number)} > - - - - - + + + + +
onValueChange value: {val1}
onValueCommitted value: {val2}
- ); } diff --git a/docs/app/experiments/slider-template.tsx b/docs/app/experiments/slider-template.tsx deleted file mode 100644 index 33bee84cf1..0000000000 --- a/docs/app/experiments/slider-template.tsx +++ /dev/null @@ -1,19 +0,0 @@ -'use client'; -import * as React from 'react'; -import * as Slider from '@base_ui/react/Slider'; -import { Styles } from './slider'; - -export default function App() { - return ( -
- - - - - - - - -
- ); -} diff --git a/docs/app/experiments/slider.module.css b/docs/app/experiments/slider.module.css new file mode 100644 index 0000000000..99aecc36a3 --- /dev/null +++ b/docs/app/experiments/slider.module.css @@ -0,0 +1,99 @@ +.slider { + font-family: + IBM Plex Sans, + sans-serif; + font-size: 1rem; + width: 100%; + align-items: center; + position: relative; + -webkit-tap-highlight-color: transparent; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.output { + text-align: right; +} + +.control { + grid-column: 1/3; + display: flex; + align-items: center; + position: relative; + width: 100%; + height: 16px; + border-radius: 9999px; + touch-action: none; +} + +.track { + width: 100%; + height: 2px; + border-radius: 9999px; + background-color: var(--gray-400); + touch-action: none; +} + +.dark .track { + background-color: var(--gray-700); +} + +.control[data-disabled] { + cursor: not-allowed; +} + +.indicator { + height: 2px; + border-radius: 9999px; + background-color: var(--gray-900); +} + +.dark .indicator { + background-color: var(--gray-100); +} + +.thumb { + width: 16px; + height: 16px; + box-sizing: border-box; + border-radius: 50%; + background-color: var(--gray-900); + touch-action: none; + + &:focus-visible { + outline: 2px solid var(--gray-900); + outline-offset: 2px; + } + + &[data-dragging] { + background-color: pink; + } + + &[data-disabled] { + background-color: var(--gray-300); + } +} + +.dark .thumb { + background-color: var(--gray-100); + + &:focus-visible { + outline-color: var(--gray-300); + outline-width: 1px; + outline-offset: 3px; + } + + &[data-disabled] { + background-color: var(--gray-600); + } +} + +.label { + cursor: unset; + font-weight: bold; +} + +.label[data-disabled='true'] { + color: var(--gray-600); +} diff --git a/docs/app/experiments/slider.tsx b/docs/app/experiments/slider.tsx deleted file mode 100644 index 89661a7871..0000000000 --- a/docs/app/experiments/slider.tsx +++ /dev/null @@ -1,433 +0,0 @@ -'use client'; -import * as React from 'react'; -import { useTheme } from '@mui/system'; -import * as Slider from '@base_ui/react/Slider'; -import { useSliderContext } from '@base_ui/react/Slider'; - -export default function App() { - const [val1, setVal1] = React.useState(50); - const [val2, setVal2] = React.useState([40, 60]); - const [val3, setVal3] = React.useState([20, 40, 60, 80]); - return ( -
-

Uncontrolled

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- Controlled -

- { - setVal1(newValue as number); - }} - > - - - - - - - - - - { - setVal2(newValue as number[]); - }} - > - - - - - - - - - - - { - setVal3(newValue as number[]); - }} - > - - - - {val3.map((_val, idx) => ( - - ))} - - - - -

- With custom labels -

- - - - - - - - - - - - - - Volume Range - - - - - - - - - - - -

- Vertical -

- - - - - - - - - - - - - - - - - - - - - -

- RTL -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Price Range - - - -
- ); -} - -function Label(props: any) { - const { id: idProp, ...otherProps } = props; - const defaultId = React.useId(); - const labelId = idProp ?? defaultId; - - const { inputIdMap } = useSliderContext(); - - const inputId = inputIdMap.get(0); - - return