diff --git a/apps/vr-tests/src/stories/RangedSliderConverged.stories.tsx b/apps/vr-tests/src/stories/RangedSliderConverged.stories.tsx deleted file mode 100644 index a5d9271d38d4d7..00000000000000 --- a/apps/vr-tests/src/stories/RangedSliderConverged.stories.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import * as React from 'react'; -import Screener, { Steps } from 'screener-storybook/src/screener'; -import { storiesOf } from '@storybook/react'; -import { RangedSlider } from '@fluentui/react-slider'; - -const LabelComponent = () =>
; - -const MarkComponent = () => ( -
-); - -storiesOf('RangedSlider Converged', module) - .addDecorator(story => ( - - {story()} - - )) - .addStory('Root', () => , { - includeRtl: true, - includeHighContrast: true, - includeDarkMode: true, - }) - .addStory('Vertical', () => , { - includeRtl: true, - }) - .addStory( - 'Disabled', - () => , - { includeHighContrast: true, includeDarkMode: true }, - ) - .addStory('Disabled Vertical', () => ( - - )) - .addStory( - 'Marks', - () => , - { - includeRtl: true, - includeHighContrast: true, - includeDarkMode: true, - }, - ) - .addStory( - 'Marks Vertical', - () => , - { - includeRtl: true, - }, - ) - .addStory( - 'Marks Custom', - () => ( - - ), - { - includeRtl: true, - }, - ) - .addStory( - 'Marks Custom Vertical', - () => ( - - ), - { - includeRtl: true, - }, - ) - .addStory( - 'Marks Custom Label Value', - () => ( - , - }, - { value: 4, label: 'world' }, - 8, - ]} - /> - ), - { - includeRtl: true, - }, - ) - .addStory( - 'Marks Custom Label Vertical', - () => ( - , - }, - { value: 4, label: 'world' }, - 8, - ]} - /> - ), - { - includeRtl: true, - }, - ) - .addStory( - 'Custom Marks', - () => ( - , - mark: , - }, - { value: 4, label: 'world' }, - 8, - ]} - /> - ), - { - includeRtl: true, - }, - ) - .addStory( - 'Custom Marks Vertical', - () => ( - , - mark: , - }, - { value: 4, label: 'world' }, - 8, - ]} - /> - ), - { - includeRtl: true, - }, - ); diff --git a/apps/vr-tests/src/stories/SliderConverged.stories.tsx b/apps/vr-tests/src/stories/SliderConverged.stories.tsx index e4f714d1ace557..dcdc2dff50d1c3 100644 --- a/apps/vr-tests/src/stories/SliderConverged.stories.tsx +++ b/apps/vr-tests/src/stories/SliderConverged.stories.tsx @@ -3,18 +3,6 @@ import Screener, { Steps } from 'screener-storybook/src/screener'; import { storiesOf } from '@storybook/react'; import { Slider } from '@fluentui/react-slider'; -const LabelComponent = () =>
; - -const MarkComponent = () => ( -
-); - storiesOf('Slider Converged', module) .addDecorator(story => ( ) .addStory('Origin Vertical (max)', () => ( - )) - .addStory('Marks', () => , { - includeRtl: true, - includeHighContrast: true, - includeDarkMode: true, - }) - .addStory( - 'Marks Vertical', - () => , - { - includeRtl: true, - }, - ) - .addStory( - 'Marks Custom Value', - () => , - { - includeRtl: true, - }, - ) - .addStory( - 'Marks Custom Value Vertical', - () => ( - - ), - { - includeRtl: true, - }, - ) - .addStory( - 'Marks Label Value', - () => ( - - ), - { - includeRtl: true, - }, - ) - .addStory( - 'Marks Label Vertical', - () => ( - - ), - { - includeRtl: true, - }, - ) - .addStory('Marks Label Disabled', () => ( - - )) - .addStory( - 'Marks Custom Label Value', - () => ( - , - }, - { value: 4, label: 'world' }, - 8, - ]} - /> - ), - { - includeRtl: true, - }, - ) - .addStory( - 'Marks Custom Label Vertical', - () => ( - , - }, - { value: 4, label: 'world' }, - 8, - ]} - /> - ), - { - includeRtl: true, - }, - ) - .addStory( - 'Custom Marks', - () => ( - , - mark: , - }, - { value: 4, label: 'world' }, - 8, - ]} - /> - ), - { - includeRtl: true, - }, - ) - .addStory( - 'Custom Marks Vertical', - () => ( - , - mark: , - }, - { value: 4, label: 'world' }, - 8, - ]} - /> - ), - { - includeRtl: true, - }, - ); + )); diff --git a/change/@fluentui-react-slider-03252d64-5321-44f5-a760-9bbdadf077a3.json b/change/@fluentui-react-slider-03252d64-5321-44f5-a760-9bbdadf077a3.json new file mode 100644 index 00000000000000..ab7754a06449e2 --- /dev/null +++ b/change/@fluentui-react-slider-03252d64-5321-44f5-a760-9bbdadf077a3.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Refactor slider component: removed marks support, RangedSlider is removed", + "packageName": "@fluentui/react-slider", + "email": "mgodbolt@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-slider/bundle-size/RangedSlider.fixture.js b/packages/react-slider/bundle-size/RangedSlider.fixture.js deleted file mode 100644 index 720b28a2d24c83..00000000000000 --- a/packages/react-slider/bundle-size/RangedSlider.fixture.js +++ /dev/null @@ -1,7 +0,0 @@ -import { RangedSlider } from '@fluentui/react-slider'; - -console.log(RangedSlider); - -export default { - name: 'RangedSlider', -}; diff --git a/packages/react-slider/etc/react-slider.api.md b/packages/react-slider/etc/react-slider.api.md index 64fcd72eb93eb9..95df15d00a6eb4 100644 --- a/packages/react-slider/etc/react-slider.api.md +++ b/packages/react-slider/etc/react-slider.api.md @@ -10,42 +10,6 @@ import type { ForwardRefComponent } from '@fluentui/react-utilities'; import { IntrinsicShorthandProps } from '@fluentui/react-utilities'; import * as React_2 from 'react'; -// @public -export const RangedSlider: ForwardRefComponent; - -// @public (undocumented) -export const rangedSliderClassName = "fui-RangedSlider"; - -// @public (undocumented) -export interface RangedSliderCommons extends Omit { - defaultValue?: [number, number]; - onChange?: (ev: React_2.PointerEvent | React_2.KeyboardEvent, data: { - value: [number, number]; - }) => void; - value?: [number, number]; -} - -// @public (undocumented) -export interface RangedSliderProps extends Omit, 'onChange' | 'defaultValue'>, RangedSliderCommons { -} - -// @public (undocumented) -export type RangedSliderSlots = Omit & { - lowerThumb: IntrinsicShorthandProps<'div'>; - lowerThumbWrapper: IntrinsicShorthandProps<'div'>; - upperThumb: IntrinsicShorthandProps<'div'>; - upperThumbWrapper: IntrinsicShorthandProps<'div'>; - lowerInput: IntrinsicShorthandProps<'input'>; - upperInput: IntrinsicShorthandProps<'input'>; -}; - -// @public (undocumented) -export interface RangedSliderState extends ComponentState, RangedSliderCommons { -} - -// @public -export const renderRangedSlider: (state: RangedSliderState) => JSX.Element; - // @public export const renderSlider: (state: SliderState) => JSX.Element; @@ -62,24 +26,21 @@ export type SliderCommons = { min?: number; max?: number; step?: number; - keyboardStep?: number; disabled?: boolean; vertical?: boolean; - marks?: boolean | (number | { - value: number; - label?: string | JSX.Element; - mark?: JSX.Element; - })[]; origin?: number; size?: 'small' | 'medium'; - onChange?: (ev: React_2.PointerEvent | React_2.KeyboardEvent, data: { - value: number; - }) => void; - ariaValueText?: (value: number) => string; + onChange?: (ev: React_2.ChangeEvent, data: SliderOnChangeData) => void; + getAriaValueText?: (value: number) => string; +}; + +// @public (undocumented) +export type SliderOnChangeData = { + value: number; }; // @public (undocumented) -export type SliderProps = Omit, 'onChange' | 'defaultValue'> & SliderCommons; +export type SliderProps = Omit, 'defaultValue' | 'onChange' | 'size' | 'value'> & SliderCommons; // @public export const sliderShorthandProps: (keyof SliderSlots)[]; @@ -88,13 +49,7 @@ export const sliderShorthandProps: (keyof SliderSlots)[]; export type SliderSlots = { root: IntrinsicShorthandProps<'div'>; rail: IntrinsicShorthandProps<'div'>; - sliderWrapper: IntrinsicShorthandProps<'div'>; - trackWrapper: IntrinsicShorthandProps<'div'>; - track: IntrinsicShorthandProps<'div'>; - marksWrapper: IntrinsicShorthandProps<'div'>; - thumbWrapper: IntrinsicShorthandProps<'div'>; thumb: IntrinsicShorthandProps<'div'>; - activeRail: IntrinsicShorthandProps<'div'>; input: IntrinsicShorthandProps<'input'>; }; @@ -102,16 +57,7 @@ export type SliderSlots = { export type SliderState = ComponentState & SliderCommons; // @public -export const useRangedSlider: (props: RangedSliderProps, ref: React_2.Ref) => RangedSliderState; - -// @public (undocumented) -export const useRangedSliderState: (state: RangedSliderState) => RangedSliderState; - -// @public (undocumented) -export const useRangedSliderStyles: (state: RangedSliderState) => RangedSliderState; - -// @public -export const useSlider: (props: SliderProps, ref: React_2.Ref) => SliderState; +export const useSlider: (props: SliderProps, ref: React_2.Ref) => SliderState; // @public (undocumented) export const useSliderState: (state: SliderState) => SliderState; diff --git a/packages/react-slider/package.json b/packages/react-slider/package.json index 9d195a9a6b3237..a3655d9fde1d6c 100644 --- a/packages/react-slider/package.json +++ b/packages/react-slider/package.json @@ -4,6 +4,7 @@ "description": "Fluent UI React Slider component.", "main": "lib-commonjs/index.js", "module": "lib/index.js", + "private": "true", "typings": "lib/index.d.ts", "sideEffects": false, "repository": { diff --git a/packages/react-slider/src/RangedSlider.stories.tsx b/packages/react-slider/src/RangedSlider.stories.tsx deleted file mode 100644 index f6c3376c07e5c4..00000000000000 --- a/packages/react-slider/src/RangedSlider.stories.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import * as React from 'react'; -import { makeStyles, shorthands } from '@fluentui/react-make-styles'; -import { RangedSlider } from './index'; -import { Label } from '@fluentui/react-label'; -import type { RangedSliderProps } from './index'; -import type { Meta } from '@storybook/react'; - -const useStyles = makeStyles({ - root: { - display: 'flex', - flexDirection: 'column', - ...shorthands.gap('10px'), - width: '400px', - }, - slider: { - width: '500px', - '--slider-thumb-size': '50px', - '--slider-rail-size': '8px', - }, - verticalWrapper: { - display: 'flex', - ...shorthands.gap('10px'), - }, -}); - -const CustomLabel = () => ( -
-); - -export const BasicRangedSliderExample = (props: RangedSliderProps) => { - const [rangedSliderValue, setRangedSliderValue] = React.useState<[number, number]>([10, 20]); - const styles = useStyles(); - - const onSliderChange = ( - ev: React.PointerEvent | React.KeyboardEvent, - data: { value: [number, number] }, - ) => setRangedSliderValue(data.value); - - return ( -
- - - - - -
- - -
- - - - , - }, - 9, - ]} - max={10} - /> -
- ); -}; - -export default { - title: 'Components/RangedSlider', - component: RangedSlider, -} as Meta; diff --git a/packages/react-slider/src/RangedSlider.ts b/packages/react-slider/src/RangedSlider.ts deleted file mode 100644 index 37bcc031d44637..00000000000000 --- a/packages/react-slider/src/RangedSlider.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './components/RangedSlider/index'; diff --git a/packages/react-slider/src/Slider.stories.tsx b/packages/react-slider/src/Slider.stories.tsx deleted file mode 100644 index bfcc7a0bb53720..00000000000000 --- a/packages/react-slider/src/Slider.stories.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import * as React from 'react'; -import { makeStyles, shorthands } from '@fluentui/react-make-styles'; -import { Slider } from './index'; -import { Label } from '@fluentui/react-label'; -import type { SliderProps } from './index'; -import type { Meta } from '@storybook/react'; - -const useStyles = makeStyles({ - root: { - display: 'flex', - flexDirection: 'column', - ...shorthands.gap('10px'), - width: '400px', - }, - slider: { - width: '500px', - '--slider-thumb-size': '50px', - '--slider-rail-size': '8px', - }, - verticalWrapper: { - display: 'flex', - ...shorthands.gap('10px'), - }, -}); - -const CustomMark = () => ( -
-); - -const CustomLabel = () => ( -
-); - -export const BasicSliderExample = (props: SliderProps) => { - const [sliderValue, setSliderValue] = React.useState(160); - const onSliderChange = ( - ev: React.PointerEvent | React.KeyboardEvent, - data: { value: number }, - ) => setSliderValue(data.value); - - const styles = useStyles(); - - return ( -
- - - - - - - - - - -
- ); -}; - -export const MarkedSliderExample = (props: SliderProps) => { - const styles = useStyles(); - - return ( -
- - - - - - , - }, - { value: 9, label: '9 oz' }, - ]} - max={10} - /> - - - - , - }, - { - value: 8, - label: '8', - mark: , - }, - { - value: 9, - mark: , - }, - ]} - max={10} - /> - -
- - - - , - }, - 9, - ]} - max={10} - /> -
-
- ); -}; - -export const VerticalSliderExample = (props: SliderProps) => { - const [sliderValue, setSliderValue] = React.useState(160); - const onSliderChange = ( - ev: React.PointerEvent | React.KeyboardEvent, - data: { value: number }, - ) => setSliderValue(data.value); - - const styles = useStyles(); - - return ( -
- - - - - - - - -
- ); -}; - -export const CustomSliderExample = (props: SliderProps) => { - const styles = useStyles(); - - return ( -
- - - - -
- ); -}; - -export default { - title: 'Components/Slider', - component: Slider, -} as Meta; diff --git a/packages/react-slider/src/components/RangedSlider/RangedSlider.test.tsx b/packages/react-slider/src/components/RangedSlider/RangedSlider.test.tsx deleted file mode 100644 index ed3dfa6c4a864a..00000000000000 --- a/packages/react-slider/src/components/RangedSlider/RangedSlider.test.tsx +++ /dev/null @@ -1,604 +0,0 @@ -import * as React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; -import { resetIdsForTests } from '@fluentui/react-utilities'; -// TODO: Find a way to use pointer events with testing-library and remove enzyme. -// https: github.com/microsoft/fluentui/issues/19977 -import { mount, ReactWrapper } from 'enzyme'; -import { RangedSlider } from './RangedSlider'; -import { isConformant } from '../../common/isConformant'; - -describe('RangedSlider', () => { - isConformant({ - Component: RangedSlider, - displayName: 'RangedSlider', - // consistent-callback-args throws error when given a tuple type. - // https://github.com/microsoft/fluentui/issues/19978 - disabledTests: ['kebab-aria-attributes', 'consistent-callback-args'], - }); - - afterEach(() => { - resetIdsForTests(); - }); - - describe('Snapshot Tests', () => { - it('renders horizontal RangedSlider correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('renders vertical RangedSlider correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('renders disabled RangedSlider correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('renders RangedSlider with marks correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('renders horizontal Slider with (custom marks) correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('renders vertical Slider with (custom marks) correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - }); - - describe('Unit Tests', () => { - it('handles id prop', () => { - render(); - const sliderRoot = screen.getByTestId('test'); - expect(sliderRoot.getAttribute('id')).toEqual('test_id'); - }); - - it('slides to the correct position when dragged in-between steps', () => { - const inputRef = React.createRef(); - const onChange = jest.fn(); - - const wrapper: ReactWrapper = mount( - , - ); - - const activeRail = wrapper.find('.active-rail'); - - activeRail.getDOMNode().getBoundingClientRect = () => - ({ left: 0, top: 0, right: 100, bottom: 40, width: 100, height: 40 } as DOMRect); - - wrapper.simulate('pointerdown', { type: 'pointermove', clientX: 45, clientY: 0 }); - expect(onChange).toBeCalledTimes(1); - expect(onChange.mock.calls[0][1]).toEqual({ - value: [50, 100], - }); - expect(inputRef.current?.value).toEqual('50'); - expect(wrapper.find('.fui-Slider-track').props().style?.width).toEqual('55%'); - expect(wrapper.find('.fui-Slider-track').props().style?.left).toEqual('45%'); - - wrapper.simulate('pointerdown', { type: 'pointermove', clientX: 24, clientY: 0 }); - expect(onChange).toBeCalledTimes(2); - expect(onChange.mock.calls[1][1]).toEqual({ - value: [20, 100], - }); - expect(inputRef.current?.value).toEqual('20'); - expect(wrapper.find('.fui-Slider-track').props().style?.width).toEqual('76%'); - expect(wrapper.find('.fui-Slider-track').props().style?.left).toEqual('24%'); - }); - - it('calls onChange when pointerDown', () => { - const onChange = jest.fn(); - - render(); - - const sliderRoot = screen.getByTestId('test'); - expect(onChange).toBeCalledTimes(0); - fireEvent.pointerDown(sliderRoot, { clientX: 0, clientY: 0 }); - expect(onChange).toBeCalledTimes(1); - expect(onChange.mock.calls[0][1]).toEqual({ value: [0, 10] }); - }); - - it('applies the defaultValue prop', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - render( - , - ); - expect(lowerInputRef.current?.value).toEqual('50'); - expect(upperInputRef.current?.value).toEqual('100'); - }); - - it('applies the value prop', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - render(); - expect(lowerInputRef.current?.value).toEqual('20'); - expect(upperInputRef.current?.value).toEqual('80'); - }); - - it('applies the disabled prop', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - render(); - expect(lowerInputRef.current?.disabled).toEqual(true); - expect(upperInputRef.current?.disabled).toEqual(true); - }); - - it('applies the min prop', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - render(); - expect(lowerInputRef.current?.min).toEqual('11'); - expect(upperInputRef.current?.min).toEqual('11'); - }); - - it('applies the max prop', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - render(); - expect(lowerInputRef.current?.max).toEqual('11'); - expect(upperInputRef.current?.max).toEqual('11'); - }); - - it('applies the step prop', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - render(); - expect(lowerInputRef.current?.step).toEqual('11'); - expect(upperInputRef.current?.step).toEqual('11'); - }); - - it('clamps an initial defaultValue that is out of bounds', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - render( - , - ); - expect(lowerInputRef.current?.value).toEqual('0'); - expect(upperInputRef.current?.value).toEqual('100'); - }); - }); - - it('sorts an unsorted set of defaultValues', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - render( - , - ); - expect(lowerInputRef.current?.value).toEqual('50'); - expect(upperInputRef.current?.value).toEqual('100'); - }); - - it('slides to min/max and executes onChange', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - const onChange = jest.fn(); - - const wrapper: ReactWrapper = mount( - , - ); - - const activeRail = wrapper.find('.active-rail'); - - activeRail.getDOMNode().getBoundingClientRect = () => - ({ left: 0, top: 0, right: 100, bottom: 40, width: 100, height: 40 } as DOMRect); - - expect(onChange).toBeCalledTimes(0); - - activeRail.simulate('pointerdown', { type: 'pointermove', clientX: 110, clientY: 0 }); - expect(onChange).toBeCalledTimes(1); - expect(onChange.mock.calls[0][1]).toEqual({ value: [50, 100] }); - expect(lowerInputRef.current?.value).toEqual('50'); - expect(upperInputRef.current?.value).toEqual('100'); - expect(wrapper.find('.fui-Slider-track').props().style?.width).toEqual('50%'); - expect(wrapper.find('.fui-Slider-track').props().style?.left).toEqual('50%'); - - activeRail.simulate('pointerdown', { type: 'pointermove', clientX: -10, clientY: 0 }); - expect(onChange).toBeCalledTimes(2); - expect(onChange.mock.calls[1][1]).toEqual({ value: [0, 100] }); - expect(lowerInputRef.current?.value).toEqual('0'); - expect(upperInputRef.current?.value).toEqual('100'); - expect(wrapper.find('.fui-Slider-track').props().style?.width).toEqual('100%'); - expect(wrapper.find('.fui-Slider-track').props().style?.left).toEqual('0%'); - - wrapper.unmount(); - }); - - it('clamps to the correct value when dragged in-between steps', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - const onChange = jest.fn(); - - const wrapper: ReactWrapper = mount( - , - ); - - const activeRail = wrapper.find('.active-rail'); - - activeRail.getDOMNode().getBoundingClientRect = () => - ({ left: 0, top: 0, right: 100, bottom: 40, width: 100, height: 40 } as DOMRect); - - wrapper.simulate('pointerdown', { type: 'pointermove', clientX: 45, clientY: 0 }, { type: 'pointerup' }); - expect(onChange).toBeCalledTimes(1); - expect(onChange.mock.calls[0][1]).toEqual({ value: [50, 100] }); - expect(lowerInputRef.current?.value).toEqual('50'); - expect(upperInputRef.current?.value).toEqual('100'); - expect(wrapper.find('.fui-Slider-track').props().style?.width).toEqual('55%'); - expect(wrapper.find('.fui-Slider-track').props().style?.left).toEqual('45%'); - - wrapper.simulate('pointerdown', { type: 'pointermove', clientX: 84, clientY: 0 }, { type: 'pointerup' }); - expect(onChange).toBeCalledTimes(2); - expect(onChange.mock.calls[1][1]).toEqual({ value: [50, 80] }); - expect(lowerInputRef.current?.value).toEqual('50'); - expect(upperInputRef.current?.value).toEqual('80'); - expect(wrapper.find('.fui-Slider-track').props().style?.width).toEqual('34%'); - expect(wrapper.find('.fui-Slider-track').props().style?.left).toEqual('50%'); - }); - - it('handles a keyboardStep prop', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - const onChange = jest.fn(); - - render( - , - ); - - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowUp' }); - expect(onChange.mock.calls[0][1]).toEqual({ value: [20, 25] }); - expect(lowerInputRef.current?.value).toBe('20'); - expect(upperInputRef.current?.value).toBe('25'); - - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowDown' }); - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowDown' }); - expect(onChange.mock.calls[2][1]).toEqual({ value: [15, 20] }); - expect(lowerInputRef.current?.value).toBe('15'); - expect(upperInputRef.current?.value).toBe('20'); - }); - - it('handles a negative step prop', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - const onChange = jest.fn(); - - render( - , - ); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'ArrowUp' }); - expect(onChange.mock.calls[0][1]).toEqual({ value: [17, 20] }); - expect(lowerInputRef.current?.value).toBe('17'); - expect(upperInputRef.current?.value).toBe('20'); - - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowUp' }); - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowUp' }); - expect(onChange.mock.calls[2][1]).toEqual({ value: [14, 17] }); - expect(lowerInputRef.current?.value).toBe('14'); - expect(upperInputRef.current?.value).toBe('17'); - - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowDown' }); - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowDown' }); - expect(onChange.mock.calls[4][1]).toEqual({ value: [14, 23] }); - expect(lowerInputRef.current?.value).toBe('14'); - expect(upperInputRef.current?.value).toBe('23'); - }); - - it('handles a decimal step prop', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - const onChange = jest.fn(); - - render( - , - ); - - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowUp' }); - expect(onChange.mock.calls[0][1]).toEqual({ value: [20, 20.0001] }); - expect(lowerInputRef.current?.value).toBe('20'); - expect(upperInputRef.current?.value).toBe('20.0001'); - - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowDown' }); - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowDown' }); - expect(onChange.mock.calls[2][1]).toEqual({ value: [19.9999, 20] }); - expect(lowerInputRef.current?.value).toBe('19.9999'); - expect(upperInputRef.current?.value).toBe('20'); - }); - - it('applies focus to lowerInput', () => { - const inputRef = React.createRef(); - render(); - inputRef?.current?.focus(); - expect(document.activeElement).toEqual(inputRef.current); - }); - - it('applies focus to upperInput', () => { - const inputRef = React.createRef(); - render(); - inputRef?.current?.focus(); - expect(document.activeElement).toEqual(inputRef.current); - }); - - it('does not allow focus on disabled RangedSlider', () => { - const sliderRef = React.createRef(); - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - - render( - , - ); - - expect(document.activeElement).toEqual(document.body); - sliderRef?.current?.focus(); - expect(document.activeElement).toEqual(document.body); - lowerInputRef?.current?.focus(); - expect(document.activeElement).toEqual(document.body); - upperInputRef?.current?.focus(); - expect(document.activeElement).toEqual(document.body); - }); - - it('switches to the opposite thumb when its value is surpassed', () => { - const onChange = jest.fn(); - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - - render( - , - ); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'ArrowUp' }); - expect(onChange.mock.calls[0][1]).toEqual({ value: [49, 50] }); - expect(document.activeElement).toEqual(lowerInputRef.current); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'ArrowUp' }); - expect(onChange.mock.calls[1][1]).toEqual({ value: [50, 50] }); - expect(document.activeElement).toEqual(lowerInputRef.current); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'ArrowUp' }); - expect(onChange.mock.calls[2][1]).toEqual({ value: [50, 51] }); - expect(document.activeElement).toEqual(upperInputRef.current); - - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowDown' }); - expect(onChange.mock.calls[3][1]).toEqual({ value: [50, 50] }); - expect(document.activeElement).toEqual(upperInputRef.current); - - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowDown' }); - expect(onChange.mock.calls[4][1]).toEqual({ value: [49, 50] }); - expect(document.activeElement).toEqual(lowerInputRef.current); - }); - - it('handles keydown events', () => { - const onChange = jest.fn(); - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - - render( - , - ); - - expect(onChange).toBeCalledTimes(0); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'ArrowDown' }); - expect(onChange.mock.calls[0][1]).toEqual({ value: [49, 50] }); - expect(lowerInputRef.current?.value).toEqual('49'); - expect(upperInputRef.current?.value).toEqual('50'); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'ArrowUp' }); - expect(onChange.mock.calls[1][1]).toEqual({ value: [50, 50] }); - expect(lowerInputRef.current?.value).toEqual('50'); - expect(upperInputRef.current?.value).toEqual('50'); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'ArrowLeft' }); - expect(onChange.mock.calls[2][1]).toEqual({ value: [49, 50] }); - expect(lowerInputRef.current?.value).toEqual('49'); - expect(upperInputRef.current?.value).toEqual('50'); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'ArrowRight' }); - expect(onChange.mock.calls[3][1]).toEqual({ value: [50, 50] }); - expect(lowerInputRef.current?.value).toEqual('50'); - expect(upperInputRef.current?.value).toEqual('50'); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'PageUp' }); - expect(onChange.mock.calls[4][1]).toEqual({ value: [50, 60] }); - expect(lowerInputRef.current?.value).toEqual('50'); - expect(upperInputRef.current?.value).toEqual('60'); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'PageDown' }); - expect(onChange.mock.calls[5][1]).toEqual({ value: [40, 60] }); - expect(lowerInputRef.current?.value).toEqual('40'); - expect(upperInputRef.current?.value).toEqual('60'); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'Home' }); - expect(onChange.mock.calls[6][1]).toEqual({ value: [0, 60] }); - expect(lowerInputRef.current?.value).toEqual('0'); - expect(upperInputRef.current?.value).toEqual('60'); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'End' }); - expect(onChange.mock.calls[7][1]).toEqual({ value: [0, 100] }); - expect(lowerInputRef.current?.value).toEqual('0'); - expect(upperInputRef.current?.value).toEqual('100'); - - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowLeft', shiftKey: true }); - expect(onChange.mock.calls[8][1]).toEqual({ value: [0, 90] }); - expect(lowerInputRef.current?.value).toEqual('0'); - expect(upperInputRef.current?.value).toEqual('90'); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'ArrowRight', shiftKey: true }); - expect(onChange.mock.calls[9][1]).toEqual({ value: [10, 90] }); - expect(lowerInputRef.current?.value).toEqual('10'); - expect(upperInputRef.current?.value).toEqual('90'); - - expect(onChange).toBeCalledTimes(10); - }); - - it('does not allow change on disabled Slider', () => { - const onChange = jest.fn(); - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - - render( - , - ); - - expect(onChange).toBeCalledTimes(0); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'ArrowUp' }); - expect(onChange).toBeCalledTimes(0); - - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowUp' }); - expect(onChange).toBeCalledTimes(0); - }); - - it('handles onKeyDown callback', () => { - const onKeyDown = jest.fn(); - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - - render( - , - ); - - expect(onKeyDown).toBeCalledTimes(0); - - fireEvent.keyDown(lowerInputRef.current!, { key: 'ArrowUp' }); - expect(onKeyDown).toBeCalledTimes(1); - - fireEvent.keyDown(upperInputRef.current!, { key: 'ArrowUp' }); - expect(onKeyDown).toBeCalledTimes(2); - }); - - it('handles onPointerDown callback', () => { - const onPointerDown = jest.fn(); - - const wrapper: ReactWrapper = mount(); - const sliderRoot = wrapper.first(); - - expect(onPointerDown).toBeCalledTimes(0); - - sliderRoot.simulate('pointerdown', { type: 'pointerMove', clientX: 87, clientY: 32 }); - expect(onPointerDown).toBeCalledTimes(1); - - wrapper.unmount(); - }); -}); - -describe('Accessibility Tests', () => { - it('handles role prop', () => { - render(); - const sliderRoot = screen.getByTestId('test'); - expect(sliderRoot.getAttribute('role')).toEqual('test'); - }); - - it('renders the lower input slot as input', () => { - const { container } = render(); - const inputElement = container.querySelector('.test'); - expect(inputElement?.tagName).toEqual('INPUT'); - }); - - it('provides the lower input slot with a type of range', () => { - const { container } = render(); - const inputElement = container.querySelector('.test'); - expect(inputElement?.getAttribute('type')).toEqual('range'); - }); - - it('renders the upper input slot as input', () => { - const { container } = render(); - const inputElement = container.querySelector('.test'); - expect(inputElement?.tagName).toEqual('INPUT'); - }); - - it('provides the upper input slot with a type of range', () => { - const { container } = render(); - const inputElement = container.querySelector('.test'); - expect(inputElement?.getAttribute('type')).toEqual('range'); - }); - - it('applies ariaValueText', () => { - const lowerInputRef = React.createRef(); - const upperInputRef = React.createRef(); - - const values = ['small', 'medium', 'large']; - const defaultValue: [number, number] = [1, 2]; - const getTextValue = (value: number) => values[value]; - - render( - , - ); - - expect(lowerInputRef?.current?.getAttribute('aria-valuetext')).toEqual(values[defaultValue[0]]); - expect(upperInputRef?.current?.getAttribute('aria-valuetext')).toEqual(values[defaultValue[1]]); - }); -}); diff --git a/packages/react-slider/src/components/RangedSlider/RangedSlider.tsx b/packages/react-slider/src/components/RangedSlider/RangedSlider.tsx deleted file mode 100644 index 8154ba0d4b90cb..00000000000000 --- a/packages/react-slider/src/components/RangedSlider/RangedSlider.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { renderRangedSlider } from './renderRangedSlider'; -import { useRangedSlider } from './useRangedSlider'; -import { useRangedSliderStyles } from './useRangedSliderStyles'; -import type { RangedSliderProps } from './RangedSlider.types'; -import type { ForwardRefComponent } from '@fluentui/react-utilities'; - -/** - * The RangedSlider component allows users to quickly select a range by dragging a lower or upper thumb across a rail. - */ -export const RangedSlider: ForwardRefComponent = React.forwardRef((props, ref) => { - const state = useRangedSlider(props, ref); - - useRangedSliderStyles(state); - - return renderRangedSlider(state); -}); -RangedSlider.displayName = 'RangedSlider'; diff --git a/packages/react-slider/src/components/RangedSlider/RangedSlider.types.ts b/packages/react-slider/src/components/RangedSlider/RangedSlider.types.ts deleted file mode 100644 index 54c4f1a57c5f45..00000000000000 --- a/packages/react-slider/src/components/RangedSlider/RangedSlider.types.ts +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from 'react'; -import { ComponentState, ComponentProps, IntrinsicShorthandProps } from '@fluentui/react-utilities'; -import type { SliderSlots, SliderCommons } from '../Slider/Slider.types'; - -export type RangedSliderSlots = Omit & { - /** - * The lower draggable icon used to select a given value in the RangedSlider. - */ - lowerThumb: IntrinsicShorthandProps<'div'>; - - /** - * The wrapper around the RangedSlider's lower thumb. - * It is primarily used to handle the dragging animation from translateX. - */ - lowerThumbWrapper: IntrinsicShorthandProps<'div'>; - - /** - * The upper draggable icon used to select a given value in the RangedSlider. - */ - upperThumb: IntrinsicShorthandProps<'div'>; - - /** - * The wrapper around the RangedSlider's upper thumb. - * It is primarily used to handle the dragging animation from translateX. - */ - upperThumbWrapper: IntrinsicShorthandProps<'div'>; - - /** - * The hidden input for the Slider's lower thumb. - */ - lowerInput: IntrinsicShorthandProps<'input'>; - - /** - * The hidden input for the Slider's upper thumb. - */ - upperInput: IntrinsicShorthandProps<'input'>; -}; - -export interface RangedSliderCommons extends Omit { - /** - * The starting value for an uncontrolled RangedSlider. The first value is always lower than the second value. - * Mutually exclusive with `value` prop. - */ - defaultValue?: [number, number]; - - /** - * The current value of the controlled RangedSlider. The first value is always lower than the second value. - * Mutually exclusive with `defaultValue` prop. - */ - value?: [number, number]; - - /** - * Triggers a callback when the value has been changed. This will be called on every individual step. - */ - onChange?: ( - ev: React.PointerEvent | React.KeyboardEvent, - data: { value: [number, number] }, - ) => void; -} - -export interface RangedSliderProps - extends Omit, 'onChange' | 'defaultValue'>, - RangedSliderCommons {} - -export interface RangedSliderState extends ComponentState, RangedSliderCommons {} diff --git a/packages/react-slider/src/components/RangedSlider/Spec.md b/packages/react-slider/src/components/RangedSlider/Spec.md deleted file mode 100644 index 8f426e220fb0a4..00000000000000 --- a/packages/react-slider/src/components/RangedSlider/Spec.md +++ /dev/null @@ -1,175 +0,0 @@ -# RangedSlider - -## Sample Code - -```jsx= -export const BasicRangedSliderExample = (props: RangedSliderProps) => { - const [rangedSliderValue, setRangedSliderValue] = React.useState([10, 20]); - - const sliderOnChange = ( - ev: React.PointerEvent | React.KeyboardEvent, - data: { value: [number, number] }, - ) => setRangedSliderValue(data.value); - - return ( -
- - -
- ); -}; -``` - -## API - -The API is the same as **Slider** with two differences: - -1. The value changes to a tuple. -2. The `origin` prop is omitted from the api. - -| Name | V0 | V8 | Description | -| ------------ | --- | --- | -------------------------------------------------------------------------------------------------- | --- | -| defaultValue | - | - | The starting value for an uncontrolled RangedSlider. Mutually exclusive with `value` prop. | -| value | - | - | The current value of the controlled RangedSlider. Mutually exclusive with `defaultValue` prop. | | -| onChange | - | - | Triggers a callback when the value has been changed. This will be called on every individual step. | - -## Migration - -drawing - -| v8 `Slider` | Converged `RangedSlider` | -| ------------------- | ------------------------ | -| `lowerValue` | `value` | -| `defaultLowerValue` | `defaultValue` | -| `ranged` | X | - -drawing - -| v0 `Slider` | Converged `RangedSlider` | -| ----------- | ------------------------ | -| - | - | - -### v8 - -```jsx -export const SliderRangedExample: React.FunctionComponent = () => { - const [sliderLowerValue, setSliderLowerValue] = React.useState(0); - const [sliderValue, setSliderValue] = React.useState(10); - - const onChange = (_: unknown, range: [number, number]) => { - setSliderLowerValue(range[0]); - setSliderValue(range[1]); - }; - - return ( - <> - - - - ); -}; -``` - -### Converged - -```jsx -export const BasicRangedSliderExample = (props: RangedSliderProps) => { - const [rangedSliderValue, setRangedSliderValue] = React.useState([0, 10]); - - const sliderOnChange = ( - ev: React.PointerEvent | React.KeyboardEvent, - data: { value: [number, number] }, - ) => setRangedSliderValue(data.value); - - return ( -
- - -
- ); -}; -``` - -## Structure - -- _**Public**_ - ```jsx - - ``` -- _**Internal**_ - - ```jsx - - {state.marks && } - - - - - - - // The hidden input element is moved inside the thumbWrapper for styling purposes regarding focus - - - - - - - - - - - ``` - -- _**DOM** - how the component will be rendered as HTML elements_ - -```jsx -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-``` - -## Behaviors - -_Explain how the component will behave in use, including:_ - -- _Component States_ - - - **Disabled** - - When disabled, all touch and mouse events are ignored, and the RangedSlider's value never updates. - - Does not allow focus and is read only. - - **Focused** - - Focus indicators only appear when keyboard tabbing/directional keystrokes and disappears when the mouse/touch interactions occur. The individual thumb hidden input elements maintain focus. - -- _Interaction_ - - _Keyboard_ - Handles the same keyboard increment/decrement values from the `Slider` component. When the active thumb surpasses the lower or upper thumb, focus and the set value both switch to the other thumb. - - _Touch_ - When the _rail_ is pressed the nearest thumb will be selected - - _Cursor_ - `pointerdown` Finds the nearest thumb and sets the current value immediately. - `pointermove` is attached to the window element on `pointerdown` and watches for move events. The selected thumb's value is updated accordingly. When the active thumb surpasses the lower or upper thumb, the set value switches to the other thumb. - `pointerup` removes the `mousemove` event. - - _Touch_ - Handles the same events as the _Cursor_ - - _Screen readers_ - Functions the same as `Slider` diff --git a/packages/react-slider/src/components/RangedSlider/__snapshots__/RangedSlider.test.tsx.snap b/packages/react-slider/src/components/RangedSlider/__snapshots__/RangedSlider.test.tsx.snap deleted file mode 100644 index 868354e61fcaed..00000000000000 --- a/packages/react-slider/src/components/RangedSlider/__snapshots__/RangedSlider.test.tsx.snap +++ /dev/null @@ -1,679 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RangedSlider Snapshot Tests renders RangedSlider with marks correctly 1`] = ` -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-`; - -exports[`RangedSlider Snapshot Tests renders disabled RangedSlider correctly 1`] = ` -
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-`; - -exports[`RangedSlider Snapshot Tests renders horizontal RangedSlider correctly 1`] = ` -
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-`; - -exports[`RangedSlider Snapshot Tests renders horizontal Slider with (custom marks) correctly 1`] = ` -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-`; - -exports[`RangedSlider Snapshot Tests renders vertical RangedSlider correctly 1`] = ` -
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-`; - -exports[`RangedSlider Snapshot Tests renders vertical Slider with (custom marks) correctly 1`] = ` -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-`; diff --git a/packages/react-slider/src/components/RangedSlider/index.ts b/packages/react-slider/src/components/RangedSlider/index.ts deleted file mode 100644 index 4713556473a24d..00000000000000 --- a/packages/react-slider/src/components/RangedSlider/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './RangedSlider'; -export * from './RangedSlider.types'; -export * from './renderRangedSlider'; -export * from './useRangedSlider'; -export * from './useRangedSliderState'; -export { rangedSliderClassName, useRangedSliderStyles } from './useRangedSliderStyles'; diff --git a/packages/react-slider/src/components/RangedSlider/renderRangedSlider.tsx b/packages/react-slider/src/components/RangedSlider/renderRangedSlider.tsx deleted file mode 100644 index fa2ed3dedd3773..00000000000000 --- a/packages/react-slider/src/components/RangedSlider/renderRangedSlider.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from 'react'; -import { getSlots } from '@fluentui/react-utilities'; -import type { RangedSliderState, RangedSliderSlots } from './RangedSlider.types'; - -/** - * Array of all shorthand properties listed in RangedSliderSlots. - */ -const rangedSliderShorthandProps: (keyof RangedSliderSlots)[] = [ - 'activeRail', - 'lowerInput', - 'lowerThumb', - 'lowerThumbWrapper', - 'marksWrapper', - 'rail', - 'root', - 'sliderWrapper', - 'track', - 'trackWrapper', - 'upperInput', - 'upperThumb', - 'upperThumbWrapper', -]; - -/** - * Render the final JSX of RangedSlider - */ -export const renderRangedSlider = (state: RangedSliderState) => { - const { slots, slotProps } = getSlots(state, rangedSliderShorthandProps); - - return ( - - {state.marks && } - - - - - - - - - - - - - - - - - ); -}; diff --git a/packages/react-slider/src/components/RangedSlider/useRangedSlider.ts b/packages/react-slider/src/components/RangedSlider/useRangedSlider.ts deleted file mode 100644 index bc2413b3a420af..00000000000000 --- a/packages/react-slider/src/components/RangedSlider/useRangedSlider.ts +++ /dev/null @@ -1,100 +0,0 @@ -import * as React from 'react'; -import { getNativeElementProps, resolveShorthand, useId } from '@fluentui/react-utilities'; -import { useRangedSliderState } from './useRangedSliderState'; -import type { RangedSliderProps, RangedSliderState } from './RangedSlider.types'; - -/** - * Given user props, returns state and render function for a RangedSlider. - */ -export const useRangedSlider = (props: RangedSliderProps, ref: React.Ref): RangedSliderState => { - const { - // Props - ariaValueText, - defaultValue, - disabled, - keyboardStep, - marks, - max, - min, - onChange, - size = 'medium', - step = 1, - value, - vertical, - - // Slots - activeRail, - lowerInput, - lowerThumb, - lowerThumbWrapper, - marksWrapper, - rail, - sliderWrapper, - track, - trackWrapper, - upperInput, - upperThumb, - upperThumbWrapper, - } = props; - - const state: RangedSliderState = { - ariaValueText, - defaultValue, - disabled, - keyboardStep, - marks, - max, - min, - onChange, - size, - step, - value, - vertical, - components: { - activeRail: 'div', - lowerInput: 'input', - lowerThumb: 'div', - lowerThumbWrapper: 'div', - marksWrapper: 'div', - rail: 'div', - root: 'div', - track: 'div', - trackWrapper: 'div', - sliderWrapper: 'div', - upperInput: 'input', - upperThumb: 'div', - upperThumbWrapper: 'div', - }, - root: getNativeElementProps('span', { - ref, - ...props, - id: useId('ranged-slider-', props.id), - }), - activeRail: resolveShorthand(activeRail, { required: true }), - lowerInput: resolveShorthand(lowerInput, { - required: true, - defaultProps: { - type: 'range', - }, - }), - upperInput: resolveShorthand(upperInput, { - required: true, - defaultProps: { - type: 'range', - }, - }), - lowerThumb: resolveShorthand(lowerThumb, { required: true }), - lowerThumbWrapper: resolveShorthand(lowerThumbWrapper, { required: true }), - marksWrapper: resolveShorthand(marksWrapper, { required: true }), - rail: resolveShorthand(rail, { required: true }), - track: resolveShorthand(track, { required: true }), - trackWrapper: resolveShorthand(trackWrapper, { required: true }), - sliderWrapper: resolveShorthand(sliderWrapper, { required: true }), - upperThumb: resolveShorthand(upperThumb, { required: true }), - upperThumbWrapper: resolveShorthand(upperThumbWrapper, { required: true }), - }; - - useRangedSliderState(state); - - return state; -}; diff --git a/packages/react-slider/src/components/RangedSlider/useRangedSliderState.tsx b/packages/react-slider/src/components/RangedSlider/useRangedSliderState.tsx deleted file mode 100644 index f14310b0b574fa..00000000000000 --- a/packages/react-slider/src/components/RangedSlider/useRangedSliderState.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import * as React from 'react'; -import { useFluent } from '@fluentui/react-shared-contexts'; -import { - clamp, - useBoolean, - useControllableState, - useEventCallback, - useUnmount, - useMergedRefs, -} from '@fluentui/react-utilities'; -import { - calculateSteps, - findClosestThumb, - getKeydownValue, - getMarkPercent, - getMarkValue, - getPercent, - on, - renderMarks, - validateRangedThumbValues, -} from '../../utils/index'; -import { animationTime } from '../Slider/useSliderState'; -import type { RangedSliderState } from './RangedSlider.types'; - -type RangedSliderInternalState = { - /** - * The current selected thumb index of the RangedSlider. - */ - activeThumb: 'lowerValue' | 'upperValue'; - - /** - * Disposable events for the RangedSlider. - */ - disposables: (() => void)[]; - - /** - * The internal rendered value of the RangedSlider. - */ - internalValue: [number, number]; - - /** - * The locked value of the non-moving thumb. Used to ensure that the active thumb updates correctly when changed. - * If the mouse moves quickly it would re evaluate both positions allowing for unintended movement. This locks it. - */ - lockedValue: number; -}; - -export const useRangedSliderState = (state: RangedSliderState) => { - const { max = 100, min = 0, step = 1 } = state; - const { - ariaValueText, - defaultValue = [min, max], - disabled = false, - keyboardStep = step, - marks, - onChange, - value, - vertical = false, - } = state; - - const { onKeyDown: onKeyDownCallback, onPointerDown: onPointerDownCallback } = state.root; - - const { dir } = useFluent(); - - const lowerInputRef = React.useRef(null); - const upperInputRef = React.useRef(null); - const railRef = React.useRef(null); - const internalState = React.useRef({ - internalValue: value - ? validateRangedThumbValues(value, min, max) - : validateRangedThumbValues(defaultValue, min, max), - lockedValue: 0, - activeThumb: 'lowerValue', - disposables: [], - }); - - const [stepAnimation, { setTrue: showStepAnimation, setFalse: hideStepAnimation }] = useBoolean(false); - const [renderedPosition, setRenderedPosition] = React.useState<[number, number] | undefined>( - value ? validateRangedThumbValues(value, min, max) : validateRangedThumbValues(defaultValue, min, max), - ); - const [currentValue, setCurrentValue] = useControllableState({ - state: value && validateRangedThumbValues(value, min, max), - defaultState: validateRangedThumbValues(defaultValue, min, max), - initialState: [min, max], - }); - - /** - * Updates the active thumb of the RangedSlider - */ - const updateActiveThumb = React.useCallback((incomingValue: number) => { - switch (internalState.current.activeThumb) { - case 'lowerValue': - if (incomingValue > internalState.current.internalValue[1]) { - internalState.current.activeThumb = 'upperValue'; - } - break; - case 'upperValue': - if (incomingValue < internalState.current.internalValue[0]) { - internalState.current.activeThumb = 'lowerValue'; - } - break; - } - }, []); - - /** - * Updates the controlled `currentValue` to the new `incomingValue` and clamps it. - */ - const updateValue = useEventCallback( - (incomingValue: number, ev: React.PointerEvent | React.KeyboardEvent): void => { - const clampedValue = clamp(incomingValue, min, max); - - const newValue: [number, number] = [ - internalState.current.activeThumb === 'lowerValue' ? clampedValue : internalState.current.lockedValue, - internalState.current.activeThumb === 'upperValue' ? clampedValue : internalState.current.lockedValue, - ]; - - if (clampedValue !== min && clampedValue !== max) { - ev.stopPropagation(); - if (ev.type === 'keydown') { - ev.preventDefault(); - } - } - - internalState.current.internalValue = newValue; - onChange?.(ev, { value: newValue }); - setCurrentValue(newValue); - }, - ); - - /** - * Updates the controlled `currentValue` and `renderedPosition` of the RangedSlider. - */ - const updatePosition = React.useCallback( - (incomingValue: number, ev) => { - updateActiveThumb(clamp(incomingValue, min, max)); - - internalState.current.internalValue = [ - internalState.current.activeThumb === 'lowerValue' - ? clamp(incomingValue, min, max) - : internalState.current.internalValue[0], - internalState.current.activeThumb === 'upperValue' - ? clamp(incomingValue, min, max) - : internalState.current.internalValue[1], - ]; - - internalState.current.lockedValue = - internalState.current.activeThumb === 'lowerValue' - ? internalState.current.internalValue[1] - : internalState.current.internalValue[0]; - - if (internalState.current.activeThumb === 'lowerValue') { - lowerInputRef.current!.focus(); - } else { - upperInputRef.current!.focus(); - } - - setRenderedPosition(internalState.current.internalValue); - updateValue(incomingValue, ev); - }, - [max, min, updateActiveThumb, updateValue], - ); - - /** - * Updates the internal `renderedPosition` of the RangedSlider. - */ - const updatedRenderedPosition = React.useCallback((incomingValue: number) => { - setRenderedPosition([ - internalState.current.activeThumb === 'lowerValue' ? incomingValue : internalState.current.internalValue[0], - internalState.current.activeThumb === 'upperValue' ? incomingValue : internalState.current.internalValue[1], - ]); - }, []); - - const onPointerMove = React.useCallback( - (ev: React.PointerEvent): void => { - const position = calculateSteps(ev, railRef, min, max, step, vertical, dir); - const currentStepPosition = Math.round(position / step) * step; - - updateActiveThumb(currentStepPosition); - updatedRenderedPosition(position); - updateValue(currentStepPosition, ev); - }, - [dir, max, min, step, updateActiveThumb, updateValue, updatedRenderedPosition, vertical], - ); - - const onPointerUp = React.useCallback( - (ev: React.PointerEvent): void => { - internalState.current.disposables.forEach(dispose => dispose()); - internalState.current.disposables = []; - showStepAnimation(); - // When undefined, the position falls back to the currentValue state. - setRenderedPosition(undefined); - if (internalState.current.activeThumb === 'lowerValue') { - lowerInputRef.current!.focus(); - } else { - upperInputRef.current!.focus(); - } - }, - [showStepAnimation], - ); - - const onPointerDown = React.useCallback( - (ev: React.PointerEvent): void => { - const { pointerId } = ev; - const target = ev.target as HTMLElement; - - target.setPointerCapture?.(pointerId); - onPointerDownCallback?.(ev); - hideStepAnimation(); - internalState.current.activeThumb = findClosestThumb( - internalState.current.internalValue, - calculateSteps(ev, railRef, min, max, step, vertical, dir), - ); - - internalState.current.lockedValue = - internalState.current.activeThumb === 'lowerValue' - ? internalState.current.internalValue[1] - : internalState.current.internalValue[0]; - - internalState.current.disposables.push( - on(target, 'pointermove', onPointerMove), - on(target, 'pointerup', onPointerUp), - () => { - target.releasePointerCapture?.(pointerId); - }, - ); - - onPointerMove(ev); - }, - [dir, hideStepAnimation, max, min, onPointerDownCallback, onPointerMove, onPointerUp, step, vertical], - ); - - const onInputChange = React.useCallback( - (ev: React.ChangeEvent) => { - updatePosition(Number(ev.target.value), ev); - }, - [updatePosition], - ); - - const onKeyDown = React.useCallback( - (ev: React.KeyboardEvent) => { - const activeThumbIndex = internalState.current.activeThumb === 'lowerValue' ? 0 : 1; - hideStepAnimation(); - - const incomingValue = getKeydownValue(ev, currentValue[activeThumbIndex], min, max, dir, keyboardStep); - - if (incomingValue !== min && incomingValue !== max) { - ev.stopPropagation(); - } - onKeyDownCallback?.(ev); - - if (currentValue[activeThumbIndex] !== incomingValue) { - updatePosition(incomingValue, ev); - } - }, - [currentValue, dir, hideStepAnimation, keyboardStep, max, min, onKeyDownCallback, updatePosition], - ); - - const onKeyDownLower = React.useCallback( - (ev: React.KeyboardEvent): void => { - internalState.current.activeThumb = 'lowerValue'; - onKeyDown(ev); - }, - [onKeyDown], - ); - - const onKeyDownUpper = React.useCallback( - (ev: React.KeyboardEvent): void => { - internalState.current.activeThumb = 'upperValue'; - onKeyDown(ev); - }, - [onKeyDown], - ); - - useUnmount(() => { - internalState.current.disposables.forEach(dispose => dispose()); - internalState.current.disposables = []; - }); - - const lowerValuePercent = getPercent( - renderedPosition !== undefined ? renderedPosition[0] : currentValue[0], - min, - max, - ); - - const upperValuePercent = getPercent( - renderedPosition !== undefined ? renderedPosition[1] : currentValue[1], - min, - max, - ); - - const markValues = React.useMemo((): number[] => getMarkValue(marks, min, max, step), [marks, max, min, step]); - const markPercent = React.useMemo((): string[] => getMarkPercent(markValues), [markValues]); - - const lowerThumbWrapperStyles = { - transform: vertical - ? `translateY(${lowerValuePercent}%)` - : `translateX(${dir === 'rtl' ? -lowerValuePercent : lowerValuePercent}%)`, - transition: stepAnimation ? `transform ease-in-out ${animationTime}` : 'none', - ...state.lowerThumbWrapper.style, - }; - - const upperThumbWrapperStyles = { - transform: vertical - ? `translateY(${upperValuePercent}%)` - : `translateX(${dir === 'rtl' ? -upperValuePercent : upperValuePercent}%)`, - transition: stepAnimation ? `transform ease-in-out ${animationTime}` : 'none', - ...state.upperThumbWrapper.style, - }; - - const marksWrapperStyles = marks - ? { - [vertical ? 'gridTemplateRows' : 'gridTemplateColumns']: markPercent.join(''), - ...state.marksWrapper.style, - } - : {}; - - const trackStyles = { - [vertical ? 'top' : dir === 'rtl' ? 'right' : 'left']: `${Math.min(lowerValuePercent, upperValuePercent)}%`, - [vertical ? 'height' : 'width']: `${Math.max( - upperValuePercent - lowerValuePercent, - lowerValuePercent - upperValuePercent, - )}%`, - transition: stepAnimation - ? `${vertical ? 'height' : 'width'} ease-in-out ${animationTime}${ - ', ' + vertical ? 'top' : dir === 'rtl' ? 'right' : 'left' + 'ease-in-out ' + animationTime - }` - : 'none', - ...state.track.style, - }; - - // Root props - if (!disabled) { - state.root.onPointerDown = onPointerDown; - } - - // Track Props - state.track.style = trackStyles; - - // Mark props - if (marks) { - state.marksWrapper.children = renderMarks(markValues, marks); - state.marksWrapper.style = marksWrapperStyles; - } - - // Lower Thumb Wrapper Props - state.lowerThumbWrapper.style = lowerThumbWrapperStyles; - - // Upper Thumb Wrapper Props - state.upperThumbWrapper.style = upperThumbWrapperStyles; - - // Active Rail Props - state.activeRail.ref = railRef; - - // Lower Input Props - state.lowerInput.ref = useMergedRefs(state.lowerInput.ref, lowerInputRef); - state.lowerInput.value = currentValue[0]; - state.lowerInput.min = min; - state.lowerInput.max = max; - ariaValueText && (state.lowerInput['aria-valuetext'] = ariaValueText(currentValue[0])); - state.lowerInput.disabled = disabled; - state.lowerInput.step = step; - if (!disabled) { - state.lowerInput.onKeyDown = onKeyDownLower; - state.lowerInput.onChange = onInputChange; - } - - // Upper Input Props - state.upperInput.ref = useMergedRefs(state.upperInput.ref, upperInputRef); - state.upperInput.value = currentValue[1]; - state.upperInput.min = min; - state.upperInput.max = max; - ariaValueText && (state.upperInput['aria-valuetext'] = ariaValueText(currentValue[1])); - state.upperInput.disabled = disabled; - state.upperInput.step = step; - if (!disabled) { - state.upperInput.onKeyDown = onKeyDownUpper; - state.upperInput.onChange = onInputChange; - } - - return state; -}; diff --git a/packages/react-slider/src/components/RangedSlider/useRangedSliderStyles.ts b/packages/react-slider/src/components/RangedSlider/useRangedSliderStyles.ts deleted file mode 100644 index ece75935918942..00000000000000 --- a/packages/react-slider/src/components/RangedSlider/useRangedSliderStyles.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { shorthands, mergeClasses, makeStyles } from '@fluentui/react-make-styles'; -import { - thumbClassName, - trackClassName, - useActiveRailStyles, - useMarksWrapperStyles, - useRailStyles, - useRootStyles, - useSliderWrapper, - useThumbStyles, - useThumbWrapperStyles, - useTrackStyles, - useTrackWrapperStyles, -} from '../Slider/useSliderStyles'; -import { createCustomFocusIndicatorStyle } from '@fluentui/react-tabster'; -import type { RangedSliderState } from './RangedSlider.types'; - -export const rangedSliderClassName = 'fui-RangedSlider'; - -export const lowerThumbClassName = `${thumbClassName} ${thumbClassName + '-lower'}`; -export const upperThumbClassName = `${thumbClassName} ${thumbClassName + '-upper'}`; - -/** - * Styles for the Input slot - */ -const useInputStyles = makeStyles({ - input: { - opacity: 0, - position: 'absolute', - ...shorthands.padding(0), - ...shorthands.margin(0), - width: '0px', - height: '0px', - pointerEvents: 'none', - }, - - lowerInputFocusIndicator: createCustomFocusIndicatorStyle( - theme => ({ - // TODO: Update this to [`& + .${lowerThumbClassName}`] - '& + .fui-Slider-thumb-lower': { - ':before': { - outlineStyle: 'none', - boxSizing: 'border-box', - ...shorthands.border('calc(var(--slider-thumb-size) * .05)', 'solid', 'black'), - }, - }, - }), - { selector: 'focus' }, - ), - - upperInputFocusIndicator: createCustomFocusIndicatorStyle( - theme => ({ - // TODO: Update this to [`& + .${upperThumbClassName}`] - '& + .fui-Slider-thumb-upper': { - ':before': { - outlineStyle: 'none', - boxSizing: 'border-box', - ...shorthands.border('calc(var(--slider-thumb-size) * .05)', 'solid', 'black'), - }, - }, - }), - { selector: 'focus' }, - ), -}); - -export const useRangedSliderStyles = (state: RangedSliderState): RangedSliderState => { - const rootStyles = useRootStyles(); - const sliderWrapperStyles = useSliderWrapper(); - const railStyles = useRailStyles(); - const trackWrapperStyles = useTrackWrapperStyles(); - const trackStyles = useTrackStyles(); - const marksWrapperStyles = useMarksWrapperStyles(); - const thumbWrapperStyles = useThumbWrapperStyles(); - const thumbStyles = useThumbStyles(); - const activeRailStyles = useActiveRailStyles(); - const inputStyles = useInputStyles(); - - state.root.className = mergeClasses( - rangedSliderClassName, - rootStyles.root, - rootStyles[state.size!], - state.vertical ? rootStyles.vertical : rootStyles.horizontal, - state.disabled ? rootStyles.disabled : rootStyles.enabled, - state.root.className, - ); - - state.sliderWrapper.className = mergeClasses( - sliderWrapperStyles.sliderWrapper, - state.vertical ? sliderWrapperStyles.vertical : sliderWrapperStyles.horizontal, - state.sliderWrapper.className, - ); - - state.rail.className = mergeClasses( - railStyles.rail, - state.vertical ? railStyles.vertical : railStyles.horizontal, - state.disabled ? railStyles.disabled : railStyles.enabled, - state.rail.className, - ); - - state.sliderWrapper.className = mergeClasses( - sliderWrapperStyles.sliderWrapper, - state.vertical ? sliderWrapperStyles.vertical : sliderWrapperStyles.horizontal, - state.sliderWrapper.className, - ); - - state.trackWrapper.className = mergeClasses( - trackWrapperStyles.trackWrapper, - state.vertical ? trackWrapperStyles.vertical : trackWrapperStyles.horizontal, - state.trackWrapper.className, - ); - - state.track.className = mergeClasses( - trackClassName, - trackStyles.track, - state.vertical ? trackStyles.vertical : trackStyles.horizontal, - state.disabled ? trackStyles.disabled : trackStyles.enabled, - state.track.className, - ); - - state.marksWrapper.className = mergeClasses( - marksWrapperStyles.marksWrapper, - state.vertical ? marksWrapperStyles.vertical : marksWrapperStyles.horizontal, - state.marksWrapper.className, - ); - - state.lowerThumbWrapper.className = mergeClasses( - thumbWrapperStyles.thumbWrapper, - state.vertical ? thumbWrapperStyles.vertical : thumbWrapperStyles.horizontal, - state.lowerThumbWrapper.className, - ); - - state.lowerThumb.className = mergeClasses( - lowerThumbClassName, - thumbStyles.thumb, - !state.vertical && thumbStyles.horizontal, - state.disabled ? thumbStyles.disabled : thumbStyles.enabled, - state.lowerThumb.className, - ); - - state.upperThumbWrapper.className = mergeClasses( - thumbWrapperStyles.thumbWrapper, - state.vertical ? thumbWrapperStyles.vertical : thumbWrapperStyles.horizontal, - state.upperThumbWrapper.className, - ); - - state.upperThumb.className = mergeClasses( - upperThumbClassName, - thumbStyles.thumb, - !state.vertical && thumbStyles.horizontal, - state.disabled ? thumbStyles.disabled : thumbStyles.enabled, - state.upperThumb.className, - ); - - state.activeRail.className = mergeClasses( - activeRailStyles.activeRail, - state.vertical ? activeRailStyles.vertical : activeRailStyles.horizontal, - state.activeRail.className, - ); - - state.lowerInput.className = mergeClasses( - inputStyles.input, - inputStyles.lowerInputFocusIndicator, - state.lowerInput.className, - ); - - state.upperInput.className = mergeClasses( - inputStyles.input, - inputStyles.upperInputFocusIndicator, - state.upperInput.className, - ); - - return state; -}; diff --git a/packages/react-slider/src/components/Slider/Slider.stories.tsx b/packages/react-slider/src/components/Slider/Slider.stories.tsx new file mode 100644 index 00000000000000..6f911e80056df7 --- /dev/null +++ b/packages/react-slider/src/components/Slider/Slider.stories.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { Slider } from '../../index'; +import type { Meta } from '@storybook/react'; + +export * from './stories/SliderDefault.stories'; +export * from './stories/SliderSize.stories'; +export * from './stories/SliderControlled.stories'; +export * from './stories/SliderStep.stories'; +export * from './stories/SliderOrigin.stories'; +export * from './stories/SliderVertical.stories'; +export * from './stories/SliderDisabled.stories'; + +export default { + title: 'Components/Slider', + component: Slider, + decorators: [ + Story => ( +
+ +
+ ), + ], +} as Meta; diff --git a/packages/react-slider/src/components/Slider/Slider.test.tsx b/packages/react-slider/src/components/Slider/Slider.test.tsx index 3c476acfeddd50..6613fbd310a8ee 100644 --- a/packages/react-slider/src/components/Slider/Slider.test.tsx +++ b/packages/react-slider/src/components/Slider/Slider.test.tsx @@ -1,18 +1,14 @@ import * as React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { resetIdsForTests } from '@fluentui/react-utilities'; -// TODO: Find a way to use pointer events with testing-library and remove enzyme. -// github.com/microsoft/fluentui/issues/19977 -import { mount, ReactWrapper } from 'enzyme'; import { Slider } from './Slider'; import { isConformant } from '../../common/isConformant'; -/* eslint-disable @typescript-eslint/no-explicit-any */ - describe('Slider', () => { isConformant({ Component: Slider, displayName: 'Slider', + primarySlot: 'input', disabledTests: ['kebab-aria-attributes'], }); @@ -20,536 +16,118 @@ describe('Slider', () => { resetIdsForTests(); }); - describe('Snapshot Tests', () => { - it('renders horizontal Slider correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('renders vertical Slider correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('renders disabled Slider correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('renders horizontal origin Slider correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('renders vertical origin Slider correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('renders Slider with marks correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('renders horizontal Slider with unique mark values correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('renders vertical Slider with unique mark values correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('renders horizontal Slider with mark labels correctly', () => { - const { container } = render( - , - ); - expect(container).toMatchSnapshot(); - }); - - it('renders vertical Slider with mark labels correctly', () => { - const { container } = render( - , - ); - expect(container).toMatchSnapshot(); - }); - - it('renders disabled Slider with mark labels correctly', () => { - const { container } = render( - , - ); - expect(container).toMatchSnapshot(); - }); - - it('renders Slider with custom mark labels correctly', () => { - const { container } = render( - }, - { value: 40, label: 'world' }, - 80, - ]} - />, - ); - expect(container).toMatchSnapshot(); - }); - - it('renders Slider with custom marks correctly', () => { - const { container } = render( - , - mark: ( -
- ), - }, - { value: 40, label: 'world' }, - 80, - ]} - />, - ); - expect(container).toMatchSnapshot(); - }); + // Snapshot tests + it('renders horizontal Slider correctly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); }); - describe('Unit Tests', () => { - it('handles id prop', () => { - render(); - const sliderRoot = screen.getByTestId('test'); - expect(sliderRoot.getAttribute('id')).toEqual('test_id'); - }); - - it('slides to the correct position when dragged in-between steps', () => { - const inputRef = React.createRef(); - const onChange = jest.fn(); - - const wrapper: ReactWrapper = mount( - , - ); - - const activeRail = wrapper.find('.active-rail'); - - activeRail.getDOMNode().getBoundingClientRect = () => - ({ left: 0, top: 0, right: 100, bottom: 40, width: 100, height: 40 } as DOMRect); - - wrapper.simulate('pointerdown', { type: 'pointermove', clientX: 45, clientY: 0 }); - expect(onChange).toBeCalledTimes(1); - expect(onChange.mock.calls[0][1]).toEqual({ value: 50 }); - expect(inputRef.current?.value).toEqual('50'); - expect(wrapper.find('.fui-Slider-track').props().style?.width).toEqual('45%'); - - wrapper.simulate('pointerdown', { type: 'pointermove', clientX: 24, clientY: 0 }); - expect(onChange).toBeCalledTimes(2); - expect(onChange.mock.calls[1][1]).toEqual({ value: 20 }); - expect(inputRef.current?.value).toEqual('20'); - expect(wrapper.find('.fui-Slider-track').props().style?.width).toEqual('24%'); - }); - - it('calls onChange when pointerDown', () => { - const onChange = jest.fn(); - - render(); - - const sliderRoot = screen.getByTestId('test'); - expect(onChange).toBeCalledTimes(0); - fireEvent.pointerDown(sliderRoot, { clientX: 0, clientY: 0 }); - expect(onChange).toBeCalledTimes(1); - expect(onChange.mock.calls[0][1]).toEqual({ value: 0 }); - }); - - it('applies the defaultValue prop', () => { - const inputRef = React.createRef(); - render(); - expect(inputRef.current?.value).toEqual('10'); - }); - - it('applies the value prop', () => { - const inputRef = React.createRef(); - render(); - expect(inputRef.current?.value).toEqual('10'); - }); - - it('applies the disabled prop', () => { - const inputRef = React.createRef(); - render(); - expect(inputRef.current?.disabled).toEqual(true); - }); - - it('applies the min prop', () => { - const inputRef = React.createRef(); - render(); - expect(inputRef.current?.min).toEqual('11'); - }); - - it('applies the max prop', () => { - const inputRef = React.createRef(); - render(); - expect(inputRef.current?.max).toEqual('11'); - }); - - it('applies the step prop', () => { - const inputRef = React.createRef(); - render(); - expect(inputRef.current?.step).toEqual('11'); - }); - - it('clamps an initial defaultValue that is out of bounds', () => { - const inputRef = React.createRef(); - render(); - expect(inputRef.current?.value).toEqual('0'); - }); - - it('slides to min/max and executes onChange', () => { - const inputRef = React.createRef(); - const onChange = jest.fn(); - - const wrapper: ReactWrapper = mount(); - const sliderRoot = wrapper.first(); - - expect(onChange).toBeCalledTimes(0); - - sliderRoot.getDOMNode().getBoundingClientRect = () => - ({ left: 0, top: 0, right: 100, bottom: 40, width: 100, height: 40 } as DOMRect); - - sliderRoot.simulate('pointerdown', { type: 'pointermove', clientX: 110, clientY: 0 }); - expect(onChange).toBeCalledTimes(1); - expect(onChange.mock.calls[0][1]).toEqual({ value: 100 }); - expect(inputRef.current?.value).toEqual('100'); - expect(wrapper.find('.fui-Slider-track').props().style?.width).toEqual('100%'); - - sliderRoot.simulate('pointerdown', { type: 'pointermove', clientX: -10, clientY: 0 }); - expect(onChange).toBeCalledTimes(2); - expect(onChange.mock.calls[1][1]).toEqual({ value: 0 }); - expect(inputRef.current?.value).toEqual('0'); - expect(wrapper.find('.fui-Slider-track').props().style?.width).toEqual('0%'); - - wrapper.unmount(); - }); - - it('clamps to the correct value when dragged in-between steps', () => { - const inputRef = React.createRef(); - const onChange = jest.fn(); - const wrapper: ReactWrapper = mount( - , - ); - - const activeRail = wrapper.find('.active-rail'); - - activeRail.getDOMNode().getBoundingClientRect = () => - ({ left: 0, top: 0, right: 100, bottom: 40, width: 100, height: 40 } as DOMRect); - - wrapper.simulate('pointerdown', { type: 'pointermove', clientX: 45, clientY: 0 }, { type: 'pointerup' }); - expect(onChange).toBeCalledTimes(1); - expect(onChange.mock.calls[0][1]).toEqual({ value: 50 }); - expect(inputRef.current?.value).toEqual('50'); - expect(wrapper.find('.fui-Slider-track').props().style?.width).toEqual('45%'); - - wrapper.simulate('pointerdown', { type: 'pointermove', clientX: 24, clientY: 0 }, { type: 'pointerup' }); - expect(onChange).toBeCalledTimes(2); - expect(onChange.mock.calls[1][1]).toEqual({ value: 20 }); - expect(inputRef.current?.value).toEqual('20'); - expect(wrapper.find('.fui-Slider-track').props().style?.width).toEqual('24%'); - }); - - it('handles keydown events', () => { - const inputRef = React.createRef(); - const onChange = jest.fn(); - - render( - , - ); - - const sliderRoot = screen.getByTestId('test'); - expect(onChange).toBeCalledTimes(0); - - fireEvent.keyDown(sliderRoot, { key: 'ArrowDown' }); - expect(onChange.mock.calls[0][1]).toEqual({ value: 49 }); - expect(inputRef.current?.value).toEqual('49'); - - fireEvent.keyDown(sliderRoot, { key: 'ArrowUp' }); - expect(onChange.mock.calls[1][1]).toEqual({ value: 50 }); - expect(inputRef.current?.value).toEqual('50'); - - fireEvent.keyDown(sliderRoot, { key: 'ArrowLeft' }); - expect(onChange.mock.calls[2][1]).toEqual({ value: 49 }); - expect(inputRef.current?.value).toEqual('49'); - - fireEvent.keyDown(sliderRoot, { key: 'ArrowRight' }); - expect(onChange.mock.calls[3][1]).toEqual({ value: 50 }); - expect(inputRef.current?.value).toEqual('50'); - - fireEvent.keyDown(sliderRoot, { key: 'PageUp' }); - expect(onChange.mock.calls[4][1]).toEqual({ value: 60 }); - expect(inputRef.current?.value).toEqual('60'); - - fireEvent.keyDown(sliderRoot, { key: 'PageDown' }); - expect(onChange.mock.calls[5][1]).toEqual({ value: 50 }); - expect(inputRef.current?.value).toEqual('50'); - - fireEvent.keyDown(sliderRoot, { key: 'Home' }); - expect(onChange.mock.calls[6][1]).toEqual({ value: 0 }); - expect(inputRef.current?.value).toEqual('0'); - - fireEvent.keyDown(sliderRoot, { key: 'End' }); - expect(onChange.mock.calls[7][1]).toEqual({ value: 100 }); - expect(inputRef.current?.value).toEqual('100'); - - fireEvent.keyDown(sliderRoot, { key: 'ArrowLeft', shiftKey: true }); - expect(onChange.mock.calls[8][1]).toEqual({ value: 90 }); - expect(inputRef.current?.value).toEqual('90'); - - fireEvent.keyDown(sliderRoot, { key: 'ArrowRight', shiftKey: true }); - expect(onChange.mock.calls[9][1]).toEqual({ value: 100 }); - expect(inputRef.current?.value).toEqual('100'); - - expect(onChange).toBeCalledTimes(10); - }); - - it('does not update when the controlled value prop is provided', () => { - const inputRef = React.createRef(); - render(); - const sliderRoot = screen.getByTestId('test'); - - fireEvent.keyDown(sliderRoot, { key: 'ArrowUp' }); - expect(inputRef.current?.value).toBe('50'); - }); - - it('calls onChange with the correct value', () => { - const onChange = jest.fn(); - - render(); - - const sliderRoot = screen.getByTestId('test'); - - fireEvent.keyDown(sliderRoot, { key: 'ArrowUp' }); - fireEvent.keyDown(sliderRoot, { key: 'ArrowUp' }); - fireEvent.keyDown(sliderRoot, { key: 'ArrowUp' }); - - expect(onChange.mock.calls[2][1]).toEqual({ value: 51 }); - }); - - it('correctly calculates the horizontal origin border-radius', () => { - const { container } = render(); - - const sliderRoot = screen.getByTestId('test'); - const sliderTrack = container.querySelector('.fui-Slider-track'); - - fireEvent.keyDown(sliderRoot, { key: 'ArrowUp' }); - expect(sliderTrack?.getAttribute('style')).toContain('0px 99px 99px 0px'); - fireEvent.keyDown(sliderRoot, { key: 'ArrowDown' }); - fireEvent.keyDown(sliderRoot, { key: 'ArrowDown' }); - expect(sliderTrack?.getAttribute('style')).toContain('99px 0px 0px 99px'); - }); - - it('correctly calculates the vertical origin border-radius', () => { - const { container } = render(); - - const sliderRoot = screen.getByTestId('test'); - const sliderTrack = container.querySelector('.fui-Slider-track'); - - fireEvent.keyDown(sliderRoot, { key: 'ArrowUp' }); - expect(sliderTrack?.getAttribute('style')).toContain('0px 0px 99px 99px'); - fireEvent.keyDown(sliderRoot, { key: 'ArrowDown' }); - fireEvent.keyDown(sliderRoot, { key: 'ArrowDown' }); - expect(sliderTrack?.getAttribute('style')).toContain('99px 99px 0px 0px'); - }); - - it('correctly calculates marks for each step', () => { - const { container } = render( - , - ); - const sliderWrapper = container.querySelector('.test-class'); - expect(sliderWrapper?.getAttribute('style')).toContain('grid-template-columns: 0% 20% 20% 20% 20% 20%'); - }); - - it('correctly calculates marks for custom values', () => { - const { container } = render( - , - ); - const sliderWrapper = container.querySelector('.test-class'); - expect(sliderWrapper?.getAttribute('style')).toContain('grid-template-columns: 10% 30% 30% 20%'); - }); - - it('correctly calculates marks position at a single custom value', () => { - const { container } = render(); - const sliderWrapper = container.querySelector('.test-class'); - expect(sliderWrapper?.getAttribute('style')).toContain('grid-template-columns: 40%'); - }); - - it('correctly defines the first and last marks', () => { - const { container } = render(); - const sliderWrapper = container.querySelector('.test-class'); - expect(sliderWrapper?.getAttribute('style')).toContain('grid-template-columns: 0% 100%'); - expect(container.querySelector('.fui-Slider-firstMark')).toBeTruthy(); - expect(container.querySelector('.fui-Slider-lastMark')).toBeTruthy; - }); - - it('correctly calculates the origin border-radius when given min as the origin', () => { - const { container } = render(); - const sliderTrack = container.querySelector('.fui-Slider-track'); - expect(sliderTrack?.getAttribute('style')).toContain('99px'); - }); - - it('correctly calculates the origin border-radius when given max as the origin', () => { - const { container } = render(); - const sliderTrack = container.querySelector('.fui-Slider-track'); - expect(sliderTrack?.getAttribute('style')).toContain('99px'); - }); - - it('handles a keyboardStep prop', () => { - const inputRef = React.createRef(); - const onChange = jest.fn(); - - render( - , - ); - const sliderRoot = screen.getByTestId('test'); - - fireEvent.keyDown(sliderRoot, { key: 'ArrowUp' }); - expect(onChange.mock.calls[0][1]).toEqual({ value: 25 }); - expect(inputRef.current?.value).toBe('25'); - }); - - it('handles a negative step prop', () => { - const inputRef = React.createRef(); - const onChange = jest.fn(); - - render(); - const sliderRoot = screen.getByTestId('test'); - - fireEvent.keyDown(sliderRoot, { key: 'ArrowUp' }); - expect(onChange.mock.calls[0][1]).toEqual({ value: 17 }); - expect(inputRef.current?.value).toBe('17'); - }); - - it('handles a decimal step prop', () => { - const inputRef = React.createRef(); - const onChange = jest.fn(); - - render(); - const sliderRoot = screen.getByTestId('test'); - - fireEvent.keyDown(sliderRoot, { key: 'ArrowUp' }); - expect(onChange.mock.calls[0][1]).toEqual({ value: 0.001 }); - expect(inputRef.current?.value).toBe('0.001'); - }); - - it('applies focus to the hidden input', () => { - const inputRef = React.createRef(); - render(); - inputRef?.current?.focus(); - expect(document.activeElement).toEqual(inputRef.current); - }); - - it('does not allow focus on disabled Slider', () => { - const sliderRef = React.createRef(); - const inputRef = React.createRef(); + it('renders vertical Slider correctly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); - render(); + it('renders disabled Slider correctly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); - expect(document.activeElement).toEqual(document.body); - sliderRef?.current?.focus(); - expect(document.activeElement).toEqual(document.body); - inputRef?.current?.focus(); - expect(document.activeElement).toEqual(document.body); - }); + it('renders horizontal origin Slider correctly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); - it('does not allow change on disabled Slider', () => { - const eventHandler = jest.fn(); + it('renders vertical origin Slider correctly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); - render(); + // Unit tests + it('handles id prop', () => { + const testId = 'test-id'; + render(); + expect(screen.getByRole('slider').getAttribute('id')).toEqual(testId); + }); - const sliderRoot = screen.getByTestId('test'); + it('applies the defaultValue prop', () => { + render(); + expect(screen.getByRole('slider').getAttribute('value')).toEqual('10'); + }); - expect(eventHandler).toBeCalledTimes(0); + it('applies the value prop', () => { + render(); + expect(screen.getByRole('slider').getAttribute('value')).toEqual('10'); + }); - fireEvent.keyDown(sliderRoot, { key: 'ArrowUp' }); - expect(eventHandler).toBeCalledTimes(0); - }); + it('applies the correct value prop when min is set', () => { + render(); + expect(screen.getByRole('slider').getAttribute('value')).toEqual('20'); + }); - it('handles onKeyDown callback', () => { - const eventHandler = jest.fn(); + it('applies the correct value prop when max is set', () => { + render(); + expect(screen.getByRole('slider').getAttribute('value')).toEqual('20'); + }); - render(); - const sliderRoot = screen.getByTestId('test'); + it('applies the disabled prop', () => { + render(); + expect(screen.getByRole('slider').getAttribute('disabled')).toBeDefined(); + }); - expect(eventHandler).toBeCalledTimes(0); + it('applies the min prop', () => { + render(); + expect(screen.getByRole('slider').getAttribute('min')).toEqual('11'); + }); - fireEvent.keyDown(sliderRoot, { key: 'ArrowUp' }); - expect(eventHandler).toBeCalledTimes(1); - }); + it('applies the max prop', () => { + render(); + expect(screen.getByRole('slider').getAttribute('max')).toEqual('11'); + }); - it('handles onPointerDown callback', () => { - const eventHandler = jest.fn(); + it('applies the step prop', () => { + render(); + expect(screen.getByRole('slider').getAttribute('step')).toEqual('11'); + }); - const wrapper: ReactWrapper = mount(); - const sliderRoot = wrapper.first(); + it('clamps an initial defaultValue that is out of bounds', () => { + render(); + expect(screen.getByRole('slider').getAttribute('value')).toEqual('0'); + }); - expect(eventHandler).toBeCalledTimes(0); - sliderRoot.simulate('pointerdown', { type: 'pointerMove', clientX: 87, clientY: 32 }); - expect(eventHandler).toBeCalledTimes(1); + it('applies focus to the hidden input', () => { + render(); + const input = screen.getByRole('slider'); - wrapper.unmount(); - }); + input.focus(); + expect(document.activeElement).toEqual(input); }); - describe('Accessibility Tests', () => { - it('handles role prop', () => { - render(); - const sliderRoot = screen.getByTestId('test'); - expect(sliderRoot.getAttribute('role')).toEqual('test'); - }); + it('does not allow focus on disabled Slider', () => { + render(); + const slider = screen.getByRole('slider'); + expect(document.activeElement).toEqual(document.body); + slider.focus(); + expect(document.activeElement).toEqual(document.body); + }); - it('renders the input slot as input', () => { - const { container } = render(); - const inputElement = container.querySelector('.test'); - expect(inputElement?.tagName).toEqual('INPUT'); - }); + // Accessibility tests + it('handles role prop', () => { + render(); + const customRole = screen.getByRole('test'); + expect(customRole).toBeDefined(); + }); - it('provides the input slot with a type of range', () => { - const { container } = render(); - const inputElement = container.querySelector('.test'); - expect(inputElement?.getAttribute('type')).toEqual('range'); - }); + it('provides the input slot with a type of range', () => { + render(); + expect(screen.getByRole('slider').getAttribute('type')).toEqual('range'); + }); - it('applies ariaValueText', () => { - const values = ['small', 'medium', 'large']; - const defaultValue = 1; - const getTextValue = (value: number) => values[value]; + it('applies ariaValueText', () => { + const testValue = 'test-value'; + const getTextValue = () => testValue; - render(); - const sliderInput = screen.getByRole('slider'); + render(); - expect(sliderInput.getAttribute('aria-valuetext')).toEqual(values[defaultValue]); - }); + expect(screen.getByRole('slider').getAttribute('aria-valuetext')).toEqual(testValue); }); }); diff --git a/packages/react-slider/src/components/Slider/Slider.types.ts b/packages/react-slider/src/components/Slider/Slider.types.ts index ffe4f4c9d54d52..dd6f07a5e73a82 100644 --- a/packages/react-slider/src/components/Slider/Slider.types.ts +++ b/packages/react-slider/src/components/Slider/Slider.types.ts @@ -4,6 +4,8 @@ import { ComponentState, ComponentProps, IntrinsicShorthandProps } from '@fluent export type SliderSlots = { /** * The root of the Slider. + * The root slot receives the `className` and `style` specified directly on the ``. + * All other native props will be applied to the primary slot, `input`. */ root: IntrinsicShorthandProps<'div'>; @@ -12,44 +14,16 @@ export type SliderSlots = { */ rail: IntrinsicShorthandProps<'div'>; - /** - * The wrapper around the Slider component. - */ - sliderWrapper: IntrinsicShorthandProps<'div'>; - - /** - * The wrapper around the Slider's track. It is primarily used to handle the positioning of the track. - */ - trackWrapper: IntrinsicShorthandProps<'div'>; - - /** - * The bar showing the current selected area adjacent to the Slider's thumb. - */ - track: IntrinsicShorthandProps<'div'>; - - /** - * The wrapper holding the marks and mark labels for the Slider. - */ - marksWrapper: IntrinsicShorthandProps<'div'>; - - /** - * The wrapper around the Slider's thumb. It is primarily used to handle the dragging animation from translateX. - */ - thumbWrapper: IntrinsicShorthandProps<'div'>; - /** * The draggable icon used to select a given value from the Slider. * This is the element containing `role = 'slider'`. */ thumb: IntrinsicShorthandProps<'div'>; - /** - * The area in which the Slider's rail allows for the thumb to be dragged. - */ - activeRail: IntrinsicShorthandProps<'div'>; - /** * The hidden input for the Slider. + * This is the PRIMARY slot: all native properties specified directly on `` will be applied to this slot, + * except `className` and `style`, which remain on the root slot. */ input: IntrinsicShorthandProps<'input'>; }; @@ -86,15 +60,6 @@ export type SliderCommons = { */ step?: number; - /** - * The number of steps that the Slider's value will change by during a key press. When provided, the `keyboardSteps` - * will be separated from the pointer `steps` allowing for the value to go outside of pointer related - * snapping values. - * - * @default `step` or 1 - */ - keyboardStep?: number; - /** * Whether to render the Slider as disabled. * @@ -108,15 +73,6 @@ export type SliderCommons = { */ vertical?: boolean; - /** - * When enabled, small marks are displayed across the Slider, showing potential steps. - * - * - If `true`, marks are visible at each `step`. - * - If `number[]`, marks will be displayed at each provided number. Numbers must be in ascending order. - * - If `{}[]`, mark is shown at the value location and displays any provided custom labels and marks. - */ - marks?: boolean | (number | { value: number; label?: string | JSX.Element; mark?: JSX.Element })[]; - /** * The starting origin point for the Slider. * @default min @@ -132,17 +88,19 @@ export type SliderCommons = { /** * Triggers a callback when the value has been changed. This will be called on every individual step. */ - onChange?: ( - ev: React.PointerEvent | React.KeyboardEvent, - data: { value: number }, - ) => void; + onChange?: (ev: React.ChangeEvent, data: SliderOnChangeData) => void; /** * The Slider's current value label to be read by the screen reader. */ - ariaValueText?: (value: number) => string; + getAriaValueText?: (value: number) => string; +}; + +export type SliderOnChangeData = { + value: number; }; -export type SliderProps = Omit, 'onChange' | 'defaultValue'> & SliderCommons; +export type SliderProps = Omit, 'defaultValue' | 'onChange' | 'size' | 'value'> & + SliderCommons; export type SliderState = ComponentState & SliderCommons; diff --git a/packages/react-slider/src/components/Slider/Spec.md b/packages/react-slider/src/components/Slider/Spec.md index 4cbbfd8020950a..eb26168c0f78b8 100644 --- a/packages/react-slider/src/components/Slider/Spec.md +++ b/packages/react-slider/src/components/Slider/Spec.md @@ -23,10 +23,10 @@ https://open-ui.org/components/slider.research #### Research Summary -**Marks**: Amongst other component libraries marks/ticks/notches are used to help visibly differ the current location of the thumb. Marks are also used to create custom steps through providing an array of values to jump too. +**Marks**: Amongst other component libraries marks/ticks/notches are used to help visibly differ the current location of the thumb. Marks are also used to create custom steps through providing an array of values to jump too. Marks will be excluded from initial release of the Slider to reduce complexity and await guidance for marks from design and partners. **Ranged Slider** -Since the `RangedSlider` and `Slider` have very different use cases and accessibility concerns they are planned to be separated into different components. +Since the `RangedSlider` and `Slider` have very different use cases and accessibility concerns they are planned to be separated into different components. Slider component will be the focus of initial release with the multi-thumb slider being a focus after launch. ## Sample Code @@ -37,22 +37,6 @@ Since the `RangedSlider` and `Slider` have very different use cases and accessib // Slider can be controlled -// Marks can be a Boolean (default marks) - - -// Marks can be a number array (specific marks) - - -// Marks can be an array of mark definitions - - -// Marks can be used as the step values using `step` - ``` ## Variants @@ -85,12 +69,11 @@ https://hackmd.io/VUpPADJ7Ry-ZXTrtffD7Sg ### Visual behavior props -| Name | drawing | drawing | Description | -| -------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | -| disabled | ✓ | ✓ | Whether to render the **Slider** as disabled. @defaultvalue `false` (render enabled) | -| vertical | ✓ | ✓ | Whether to render the **Slider** vertically. @default `false` (render horizontally) | -| marks | x | x | Whether the **Slider** will have marks to visibly display its steps. @default `false` (renders without marks) | -| size | x | x | The size of the Slider. | +| Name | drawing | drawing | Description | +| -------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| disabled | ✓ | ✓ | Whether to render the **Slider** as disabled. @defaultvalue `false` (render enabled) | +| vertical | ✓ | ✓ | Whether to render the **Slider** vertically. @default `false` (render horizontally) | +| size | x | x | The size of the Slider. | ### Event handlers props @@ -136,18 +119,10 @@ https://hackmd.io/VUpPADJ7Ry-ZXTrtffD7Sg ```jsx - {state.marks && } - - - - - - - - - - - + + + + ``` @@ -155,23 +130,10 @@ https://hackmd.io/VUpPADJ7Ry-ZXTrtffD7Sg ```jsx
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
``` @@ -206,7 +168,6 @@ _Explain how the component will behave in use, including:_ - _Screen readers_ - **`root`:** - renders `as` div - - handles native props expected from the element type in `as` - **`hidden input element`:** - Handles aria for the Slider. diff --git a/packages/react-slider/src/components/Slider/__snapshots__/Slider.test.tsx.snap b/packages/react-slider/src/components/Slider/__snapshots__/Slider.test.tsx.snap index dc317176e479e5..8ea26c58674813 100644 --- a/packages/react-slider/src/components/Slider/__snapshots__/Slider.test.tsx.snap +++ b/packages/react-slider/src/components/Slider/__snapshots__/Slider.test.tsx.snap @@ -1,1110 +1,127 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Slider Snapshot Tests renders Slider with custom mark labels correctly 1`] = ` +exports[`Slider renders disabled Slider correctly 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
- world -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-`; - -exports[`Slider Snapshot Tests renders Slider with custom marks correctly 1`] = ` -
-
+ disabled="" + id="slider-1" + max="10" + min="0" + orient="" + type="range" + value="5" + />
-
-
-
-
-
-
-
-
-
-
-
-
- world -
-
-
-
-
-
+ />
-
-
-
-
-
-
-
-
- -
+ />
`; -exports[`Slider Snapshot Tests renders Slider with marks correctly 1`] = ` +exports[`Slider renders horizontal Slider correctly 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ id="slider-1" + max="10" + min="0" + orient="" + type="range" + value="5" + />
-
-
-
-
-
-
-
-
- -
-
-
-`; - -exports[`Slider Snapshot Tests renders disabled Slider correctly 1`] = ` -
-
+ />
-
-
-
-
-
-
-
-
- -
+ />
`; -exports[`Slider Snapshot Tests renders disabled Slider with mark labels correctly 1`] = ` +exports[`Slider renders horizontal origin Slider correctly 1`] = `
-
-
-
-
-
-
-
- hello -
-
-
-
-
- world -
-
-
-
-
-
+ id="slider-1" + max="10" + min="0" + orient="" + type="range" + value="5" + />
-
-
-
-
-
-
-
-
- -
-
-
-`; - -exports[`Slider Snapshot Tests renders horizontal Slider correctly 1`] = ` -
-
+ />
-
-
-
-
-
-
-
-
- -
+ />
`; -exports[`Slider Snapshot Tests renders horizontal Slider with mark labels correctly 1`] = ` +exports[`Slider renders vertical Slider correctly 1`] = `
-
-
-
-
-
-
-
- hello -
-
-
-
-
- world -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-`; - -exports[`Slider Snapshot Tests renders horizontal Slider with unique mark values correctly 1`] = ` -
-
+ id="slider-1" + max="10" + min="0" + orient="vertical" + type="range" + value="5" + />
-
-
-
-
-
-
-
-
-
-
-
-
-
+ />
-
-
-
-
-
-
-
-
- -
+ />
`; -exports[`Slider Snapshot Tests renders horizontal origin Slider correctly 1`] = ` +exports[`Slider renders vertical origin Slider correctly 1`] = `
-
-
-
-
-
-
-
-
-
- -
-
-
-`; - -exports[`Slider Snapshot Tests renders vertical Slider correctly 1`] = ` -
-
-
-
-
-
-
-
-
-
-
- -
-
-
-`; - -exports[`Slider Snapshot Tests renders vertical Slider with mark labels correctly 1`] = ` -
-
-
-
-
-
-
-
-
- hello -
-
-
-
-
- world -
-
-
-
-
-
+ id="slider-1" + max="10" + min="0" + orient="vertical" + type="range" + value="5" + />
-
-
-
-
-
-
-
-
- -
-
-
-`; - -exports[`Slider Snapshot Tests renders vertical Slider with unique mark values correctly 1`] = ` -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-`; - -exports[`Slider Snapshot Tests renders vertical origin Slider correctly 1`] = ` -
-
+ />
-
-
-
-
-
-
-
-
- -
+ />
`; diff --git a/packages/react-slider/src/components/Slider/renderSlider.tsx b/packages/react-slider/src/components/Slider/renderSlider.tsx index f0cca69d2e07c8..245fd351b86f8b 100644 --- a/packages/react-slider/src/components/Slider/renderSlider.tsx +++ b/packages/react-slider/src/components/Slider/renderSlider.tsx @@ -11,18 +11,9 @@ export const renderSlider = (state: SliderState) => { return ( - {state.marks && } - - - - - - - - - - - + + + ); }; diff --git a/packages/react-slider/src/components/Slider/stories/SliderControlled.stories.tsx b/packages/react-slider/src/components/Slider/stories/SliderControlled.stories.tsx new file mode 100644 index 00000000000000..4df7c43ecc412d --- /dev/null +++ b/packages/react-slider/src/components/Slider/stories/SliderControlled.stories.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { Label } from '@fluentui/react-label'; +import { useId } from '@fluentui/react-utilities'; +import { Button } from '@fluentui/react-button'; +import { Slider, SliderProps } from '../../../index'; + +export const Controlled = () => { + const id = useId(); + const [sliderValue, setSliderValue] = React.useState(160); + const onSliderChange: SliderProps['onChange'] = (_, data) => setSliderValue(data.value); + const resetSlider = () => setSliderValue(0); + return ( + <> + + + + + ); +}; + +Controlled.parameters = { + docs: { + description: { + story: + 'A slider can be a controlled input where the slider value is stored in state and updated with `onChange`.', + }, + }, +}; diff --git a/packages/react-slider/src/components/Slider/stories/SliderDefault.stories.tsx b/packages/react-slider/src/components/Slider/stories/SliderDefault.stories.tsx new file mode 100644 index 00000000000000..525af940fe6f59 --- /dev/null +++ b/packages/react-slider/src/components/Slider/stories/SliderDefault.stories.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { Label } from '@fluentui/react-label'; +import { useId } from '@fluentui/react-utilities'; +import { Slider } from '../../../index'; + +export const Default = () => { + const id = useId(); + return ( + <> + + + + ); +}; + +Default.parameters = { + docs: { + description: { + story: 'A default slider', + }, + }, +}; diff --git a/packages/react-slider/src/components/Slider/stories/SliderDisabled.stories.tsx b/packages/react-slider/src/components/Slider/stories/SliderDisabled.stories.tsx new file mode 100644 index 00000000000000..b726a3dfa71764 --- /dev/null +++ b/packages/react-slider/src/components/Slider/stories/SliderDisabled.stories.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { Label } from '@fluentui/react-label'; +import { useId } from '@fluentui/react-utilities'; +import { Slider } from '../../../index'; + +export const Disabled = () => { + const id = useId(); + return ( + <> + + + + ); +}; + +Disabled.parameters = { + docs: { + description: { + story: 'A disabled slider will not change or fire events on click or keyboard press.', + }, + }, +}; diff --git a/packages/react-slider/src/components/Slider/stories/SliderOrigin.stories.tsx b/packages/react-slider/src/components/Slider/stories/SliderOrigin.stories.tsx new file mode 100644 index 00000000000000..dcf93c7f39fbec --- /dev/null +++ b/packages/react-slider/src/components/Slider/stories/SliderOrigin.stories.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { Label } from '@fluentui/react-label'; +import { useId } from '@fluentui/react-utilities'; +import { Slider } from '../../../index'; + +export const Origin = () => { + const id = useId(); + return ( + <> + + + + ); +}; + +Origin.parameters = { + docs: { + description: { + story: `A slider's progress can be represented with an origin so that values below the + origin will have negative progress and those above will have positive progress. + Origin, however, has no effect on the actual value of the slider.`, + }, + }, +}; diff --git a/packages/react-slider/src/components/Slider/stories/SliderSize.stories.tsx b/packages/react-slider/src/components/Slider/stories/SliderSize.stories.tsx new file mode 100644 index 00000000000000..fcb5d9e5bf6d08 --- /dev/null +++ b/packages/react-slider/src/components/Slider/stories/SliderSize.stories.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { Label } from '@fluentui/react-label'; +import { useId } from '@fluentui/react-utilities'; +import { Slider } from '../../../index'; + +export const Size = () => { + const smallId = useId('small'); + const mediumId = useId('medium'); + return ( + <> + + + + + + + ); +}; + +Size.parameters = { + docs: { + description: { + story: `A slider comes in both medium and small size. Medium is the default.`, + }, + }, +}; diff --git a/packages/react-slider/src/components/Slider/stories/SliderStep.stories.tsx b/packages/react-slider/src/components/Slider/stories/SliderStep.stories.tsx new file mode 100644 index 00000000000000..e62402543089ec --- /dev/null +++ b/packages/react-slider/src/components/Slider/stories/SliderStep.stories.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { Label } from '@fluentui/react-label'; +import { useId } from '@fluentui/react-utilities'; +import { Slider } from '../../../index'; + +export const Step = () => { + const id = useId(); + return ( + <> + + + + ); +}; + +Step.parameters = { + docs: { + description: { + story: `You can define the step value of a slider so that the value will always be a mutiple of that step`, + }, + }, +}; diff --git a/packages/react-slider/src/components/Slider/stories/SliderVertical.stories.tsx b/packages/react-slider/src/components/Slider/stories/SliderVertical.stories.tsx new file mode 100644 index 00000000000000..8c40af814017fb --- /dev/null +++ b/packages/react-slider/src/components/Slider/stories/SliderVertical.stories.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { Label } from '@fluentui/react-label'; +import { useId } from '@fluentui/react-utilities'; +import { Slider } from '../../../index'; + +export const Vertical = () => { + const id = useId(); + return ( + <> + + + + ); +}; + +Vertical.parameters = { + docs: { + description: { + story: `A slider can be rendered vertically where the max value is at the top of the slider.`, + }, + }, +}; diff --git a/packages/react-slider/src/components/Slider/useSlider.ts b/packages/react-slider/src/components/Slider/useSlider.ts index ec51a0d6de1c6c..c180c011a6ae00 100644 --- a/packages/react-slider/src/components/Slider/useSlider.ts +++ b/packages/react-slider/src/components/Slider/useSlider.ts @@ -1,101 +1,79 @@ import * as React from 'react'; -import { getNativeElementProps, resolveShorthand, useId } from '@fluentui/react-utilities'; +import { getPartitionedNativeProps, resolveShorthand, useId } from '@fluentui/react-utilities'; import { useSliderState } from './useSliderState'; import { SliderProps, SliderSlots, SliderState } from './Slider.types'; /** * Array of all shorthand properties listed in sliderShorthandProps */ -export const sliderShorthandProps: (keyof SliderSlots)[] = [ - 'root', - 'activeRail', - 'input', - 'rail', - 'sliderWrapper', - 'thumb', - 'thumbWrapper', - 'track', - 'trackWrapper', - 'marksWrapper', -]; +export const sliderShorthandProps: (keyof SliderSlots)[] = ['root', 'input', 'rail', 'thumb']; /** * Given user props, returns state and render function for a Slider. */ -export const useSlider = (props: SliderProps, ref: React.Ref): SliderState => { +export const useSlider = (props: SliderProps, ref: React.Ref): SliderState => { + const nativeProps = getPartitionedNativeProps({ + props, + primarySlotTagName: 'input', + excludedPropNames: ['onChange'], + }); + const { // Props value, defaultValue, - min, - max, - step = 1, - keyboardStep, + min = 0, + max = 100, + step, disabled, - ariaValueText, - onChange, - marks, + getAriaValueText, vertical, size = 'medium', origin, - + onChange, // Slots - activeRail, + root, input, - marksWrapper, rail, - sliderWrapper, thumb, - thumbWrapper, - track, - trackWrapper, } = props; const state: SliderState = { - ariaValueText, + getAriaValueText, defaultValue, disabled, - keyboardStep, - marks, max, min, - onChange, origin, size, step, vertical, value, + onChange, components: { - activeRail: 'div', input: 'input', - marksWrapper: 'div', rail: 'div', root: 'div', - sliderWrapper: 'div', thumb: 'div', - thumbWrapper: 'div', - track: 'div', - trackWrapper: 'div', }, - root: getNativeElementProps('span', { - ref, - ...props, - id: useId('slider-', props.id), + root: resolveShorthand(root, { + required: true, + defaultProps: { + ...nativeProps.root, + }, }), - activeRail: resolveShorthand(activeRail, { required: true }), input: resolveShorthand(input, { required: true, defaultProps: { + id: useId('slider-', props.id), + ref, + ...nativeProps.primary, type: 'range', - }, + orient: vertical ? 'vertical' : '', + } as SliderSlots['input'], }), - marksWrapper: resolveShorthand(marksWrapper, { required: true }), rail: resolveShorthand(rail, { required: true }), - sliderWrapper: resolveShorthand(sliderWrapper, { required: true }), thumb: resolveShorthand(thumb, { required: true }), - thumbWrapper: resolveShorthand(thumbWrapper, { required: true }), - track: resolveShorthand(track, { required: true }), - trackWrapper: resolveShorthand(trackWrapper, { required: true }), }; useSliderState(state); diff --git a/packages/react-slider/src/components/Slider/useSliderState.tsx b/packages/react-slider/src/components/Slider/useSliderState.tsx index 8768af712256e5..cc00acb0a67923 100644 --- a/packages/react-slider/src/components/Slider/useSliderState.tsx +++ b/packages/react-slider/src/components/Slider/useSliderState.tsx @@ -1,244 +1,68 @@ import * as React from 'react'; +import { clamp, useControllableState, useEventCallback } from '@fluentui/react-utilities'; import { useFluent } from '@fluentui/react-shared-contexts'; import { - clamp, - useBoolean, - useControllableState, - useEventCallback, - useUnmount, - useMergedRefs, -} from '@fluentui/react-utilities'; -import { - calculateSteps, - getKeydownValue, - getMarkPercent, - getMarkValue, - getPercent, - on, - renderMarks, -} from '../../utils/index'; + railOffsetVar, + railStepsPercentVar, + railProgressVar, + thumbPositionVar, + railDirectionVar, +} from './useSliderStyles'; import type { SliderState } from './Slider.types'; -// TODO: Awaiting animation time from design spec. -export const animationTime = '0.1s'; +const getPercent = (value: number, min: number, max: number) => { + return max === min ? 0 : ((value - min) / (max - min)) * 100; +}; export const useSliderState = (state: SliderState) => { - const { - value, - defaultValue = 0, - min = 0, - max = 100, - step = 1, - keyboardStep = state.step || 1, - disabled = false, - ariaValueText, - onChange, - marks, - vertical = false, - origin, - } = state; - const { onPointerDown: onPointerDownCallback, onKeyDown: onKeyDownCallback } = state.root; - + const { value, defaultValue = 0, min = 0, max = 100, step = 1, getAriaValueText, origin } = state; const { dir } = useFluent(); - - const [stepAnimation, { setTrue: showStepAnimation, setFalse: hideStepAnimation }] = useBoolean(false); - const [renderedPosition, setRenderedPosition] = React.useState(value ? value : defaultValue); const [currentValue, setCurrentValue] = useControllableState({ - state: value && clamp(value, min, max), + state: value !== undefined ? clamp(value, min, max) : undefined, defaultState: clamp(defaultValue, min, max), initialState: 0, }); - const railRef = React.useRef(null); - const inputRef = React.useRef(null); - const disposables = React.useRef<(() => void)[]>([]); - - /** - * Updates the controlled `currentValue` to the new `incomingValue` and clamps it. - */ - const updateValue = useEventCallback( - (incomingValue: number, ev: React.PointerEvent | React.KeyboardEvent): void => { - const clampedValue = clamp(incomingValue, min, max); - - if (clampedValue !== min && clampedValue !== max) { - ev.stopPropagation(); - if (ev.type === 'keydown') { - ev.preventDefault(); - } - } - - onChange?.(ev, { value: clampedValue }); - setCurrentValue(clampedValue); - }, - ); - - /** - * Updates the controlled `currentValue` and `renderedPosition` of the Slider. - */ - const updatePosition = React.useCallback( - (incomingValue: number, ev) => { - setRenderedPosition(clamp(incomingValue, min, max)); - updateValue(incomingValue, ev); - }, - [max, min, updateValue], - ); - - const onInputChange = (ev: React.ChangeEvent) => { - updatePosition(Number(ev.target.value), ev); - }; - - const onPointerMove = React.useCallback( - (ev: React.PointerEvent): void => { - const position = calculateSteps(ev, railRef, min, max, step, vertical, dir); - const currentStepPosition = Math.round(position / step) * step; - - setRenderedPosition(position); - updateValue(currentStepPosition, ev); - }, - [dir, max, min, step, updateValue, vertical], - ); - - const onPointerUp = React.useCallback( - (ev: React.PointerEvent): void => { - disposables.current.forEach(dispose => dispose()); - disposables.current = []; - showStepAnimation(); - // When undefined, the position fallbacks to the currentValue state. - setRenderedPosition(undefined); - inputRef.current!.focus(); - }, - [showStepAnimation], - ); - - const onPointerDown = React.useCallback( - (ev: React.PointerEvent): void => { - const { pointerId } = ev; - const target = ev.target as HTMLElement; - - target.setPointerCapture?.(pointerId); - - hideStepAnimation(); - onPointerDownCallback?.(ev); - - // eslint-disable-next-line deprecation/deprecation -- Should be remove an replaced with a useEvent hook. - disposables.current.push(on(target, 'pointermove', onPointerMove), on(target, 'pointerup', onPointerUp), () => { - target.releasePointerCapture?.(pointerId); - }); - - onPointerMove(ev); - }, - [hideStepAnimation, onPointerDownCallback, onPointerMove, onPointerUp], - ); - - const onKeyDown = React.useCallback( - (ev: React.KeyboardEvent): void => { - hideStepAnimation(); - onKeyDownCallback?.(ev); - const incomingValue = getKeydownValue(ev, currentValue, min, max, dir, keyboardStep); - - if (currentValue !== incomingValue) { - updatePosition(incomingValue, ev); - } - }, - [currentValue, dir, hideStepAnimation, keyboardStep, max, min, onKeyDownCallback, updatePosition], - ); - - const getTrackBorderRadius = () => { - if (origin !== undefined && origin !== (max || min)) { - if (vertical) { - return originPercent > valuePercent ? '99px 99px 0px 0px' : '0px 0px 99px 99px'; - } else { - return (dir === 'rtl' ? valuePercent > originPercent : valuePercent < originPercent) - ? '99px 0px 0px 99px' - : '0px 99px 99px 0px'; - } - } - return '99px'; - }; - - useUnmount(() => { - disposables.current.forEach(dispose => dispose()); - disposables.current = []; - }); - - const valuePercent = getPercent(renderedPosition !== undefined ? renderedPosition : currentValue, min, max); - + const valuePercent = getPercent(currentValue, min, max); const originPercent = React.useMemo(() => { return origin !== undefined ? getPercent(origin, min, max) : 0; }, [max, min, origin]); - const markValues = React.useMemo((): number[] => getMarkValue(marks, min, max, step), [marks, max, min, step]); + const inputOnChange = state.input.onChange; + const propsOnChange = state.onChange; - const markPercent = React.useMemo((): string[] => getMarkPercent(markValues), [markValues]); + const onChange: React.ChangeEventHandler = useEventCallback(ev => { + const newValue = Number(ev.target.value); + setCurrentValue(clamp(newValue, min, max)); - const thumbWrapperStyles = { - transform: vertical - ? `translateY(${valuePercent}%)` - : `translateX(${dir === 'rtl' ? -valuePercent : valuePercent}%)`, - transition: stepAnimation ? `transform ease-in-out ${animationTime}` : 'none', - ...state.thumbWrapper.style, - }; + if (inputOnChange && inputOnChange !== propsOnChange) { + inputOnChange(ev); + } else if (propsOnChange) { + propsOnChange(ev, { value: newValue }); + } + }); - const trackStyles = { - [vertical ? 'top' : dir === 'rtl' ? 'right' : 'left']: - origin !== undefined ? `${Math.min(valuePercent, originPercent)}%` : 0, - [vertical ? 'height' : 'width']: + const rootVariables = { + [railDirectionVar]: state.vertical ? '0deg' : dir === 'ltr' ? '90deg' : '270deg', + [thumbPositionVar]: valuePercent + '%', + [railStepsPercentVar]: state.step && state.step > 0 ? `${(step * 100) / (max - min)}%` : '', + [railOffsetVar]: origin !== undefined ? `${Math.min(valuePercent, originPercent)}%` : '0%', + [railProgressVar]: origin !== undefined ? `${Math.max(originPercent - valuePercent, valuePercent - originPercent)}%` : `${valuePercent}%`, - borderRadius: getTrackBorderRadius(), - // When a transition is applied with the origin, a visible animation plays when it goes below the min. - transition: stepAnimation - ? `${vertical ? 'height' : 'width'} ease-in-out ${animationTime}${ - origin !== undefined - ? ', ' + vertical - ? 'top' - : dir === 'rtl' - ? 'right' - : 'left' + 'ease-in-out ' + animationTime - : '' - }` - : 'none', - ...state.track.style, }; - const marksWrapperStyles = marks - ? { - [vertical ? 'gridTemplateRows' : 'gridTemplateColumns']: markPercent.join(''), - ...state.marksWrapper.style, - } - : {}; - // Root props - if (!disabled) { - state.root.onPointerDown = onPointerDown; - state.root.onKeyDown = onKeyDown; - } - - // Track Props - state.track.style = trackStyles; - - // Mark props - if (marks) { - state.marksWrapper.children = renderMarks(markValues, marks); - state.marksWrapper.style = marksWrapperStyles; - } - - // Thumb Wrapper Props - state.thumbWrapper.style = thumbWrapperStyles; - - // Active Rail Props - state.activeRail.ref = railRef; + state.root.style = { + ...rootVariables, + ...state.root.style, + }; // Input Props - state.input.ref = useMergedRefs(state.input.ref, inputRef); state.input.value = currentValue; - state.input.min = min; - state.input.max = max; - ariaValueText && (state.input['aria-valuetext'] = ariaValueText(currentValue!)); - state.input.disabled = disabled; - state.input.step = step; - state.input.onChange = onInputChange; + getAriaValueText && (state.input['aria-valuetext'] = getAriaValueText(currentValue!)); + state.input.onChange = onChange; return state; }; diff --git a/packages/react-slider/src/components/Slider/useSliderStyles.ts b/packages/react-slider/src/components/Slider/useSliderStyles.ts index e37473eae21c8d..8f004583c89f24 100644 --- a/packages/react-slider/src/components/Slider/useSliderStyles.ts +++ b/packages/react-slider/src/components/Slider/useSliderStyles.ts @@ -4,102 +4,83 @@ import type { SliderState } from './Slider.types'; export const sliderClassName = 'fui-Slider'; -export const thumbClassName = `${sliderClassName}-thumb`; -export const trackClassName = `${sliderClassName}-track`; -export const markContainerClassName = `${sliderClassName}-markItemContainer`; -export const firstMarkClassName = `${sliderClassName}-firstMark`; -export const lastMarkClassName = `${sliderClassName}-lastMark`; -export const markClassName = `${sliderClassName}-mark`; -export const markLabelClassName = `${sliderClassName}-label`; +const thumbSizeVar = `--fui-slider-thumb-size`; +const railSizeVar = `--fui-slider-rail-size`; +const railColorVar = `--fui-slider-rail-color`; +const progressColorVar = `--fui-slider-progress-color`; +const thumbColorVar = `--fui-slider-thumb-color`; + +export const railDirectionVar = `--fui-slider-rail-direction`; +export const railOffsetVar = `--fui-slider-rail-offset`; +export const railProgressVar = `--fui-slider-rail-progress`; +export const railStepsPercentVar = `--fui-slider-rail-steps-percent`; +export const thumbPositionVar = `--fui-slider-thumb-position`; /** * Styles for the root slot */ export const useRootStyles = makeStyles({ - root: theme => ({ + root: { position: 'relative', - display: 'inline-flex', + display: 'inline-grid', + gridTemplateAreas: '"slider"', userSelect: 'none', touchAction: 'none', - verticalAlign: 'bottom', - }), + alignItems: 'center', + justifyItems: 'center', + }, small: { - '--slider-thumb-size': '10px', - '--slider-rail-size': '2px', - '--slider-mark-size': '2px', + [thumbSizeVar]: '16px', + [railSizeVar]: '2px', }, medium: { - '--slider-thumb-size': '20px', - '--slider-rail-size': '4px', - '--slider-mark-size': '4px', + [thumbSizeVar]: '20px', + [railSizeVar]: '4px', }, - horizontal: theme => ({ + horizontal: { minWidth: '120px', - minHeight: 'var(--slider-thumb-size)', - flexDirection: 'column', - }), + height: `var(${thumbSizeVar})`, + }, - vertical: theme => ({ - transform: 'scaleY(-1)', - minWidth: 'var(--slider-thumb-size)', + vertical: { + width: `var(${thumbSizeVar})`, minHeight: '120px', - flexDirection: 'row', - }), + gridTemplateColumns: `var(${thumbSizeVar})`, + }, enabled: theme => ({ - cursor: 'grab', + [railColorVar]: theme.colorNeutralStrokeAccessible, + [progressColorVar]: theme.colorCompoundBrandBackground, + [thumbColorVar]: theme.colorCompoundBrandBackground, ':hover': { - [`& .${thumbClassName}`]: { - background: theme.colorBrandBackgroundHover, - }, - [`& .${trackClassName}`]: { - background: theme.colorBrandBackgroundHover, - }, + [thumbColorVar]: theme.colorBrandBackgroundHover, + [progressColorVar]: theme.colorBrandBackgroundHover, }, ':active': { - cursor: 'grabbing', - [`& .${thumbClassName}`]: { - background: theme.colorBrandBackgroundPressed, - }, - [`& .${trackClassName}`]: { - background: theme.colorBrandBackgroundPressed, - }, + [thumbColorVar]: theme.colorBrandBackgroundPressed, + [progressColorVar]: theme.colorBrandBackgroundPressed, }, }), disabled: theme => ({ - cursor: 'not-allowed', - }), - - focusIndicator: theme => - createFocusOutlineStyle(theme, { selector: 'focus-within', style: { outlineOffset: '6px' } }), -}); - -/** - * Styles for the slider wrapper slot - */ -export const useSliderWrapper = makeStyles({ - sliderWrapper: theme => ({ - position: 'absolute', - ...shorthands.overflow('hidden'), - }), - - horizontal: theme => ({ - left: '0px', - right: '0px', - top: '0px', - minHeight: 'var(--slider-thumb-size)', - }), - - vertical: theme => ({ - top: '0px', - bottom: '0px', - left: '0px', - minWidth: 'var(--slider-thumb-size)', - }), + [thumbColorVar]: theme.colorNeutralForegroundDisabled, + [railColorVar]: theme.colorNeutralBackgroundDisabled, + [progressColorVar]: theme.colorNeutralForegroundDisabled, + }), + + focusIndicatorHorizontal: theme => + createFocusOutlineStyle(theme, { + selector: 'focus-within', + style: { outlineOffset: { top: '6px', bottom: '6px', left: '10px', right: '10px' } }, + }), + focusIndicatorVertical: theme => + createFocusOutlineStyle(theme, { + selector: 'focus-within', + style: { outlineOffset: { top: '10px', bottom: '10px', left: '6px', right: '6px' } }, + }), }); /** @@ -107,194 +88,56 @@ export const useSliderWrapper = makeStyles({ */ export const useRailStyles = makeStyles({ rail: theme => ({ - position: 'absolute', ...shorthands.borderRadius(theme.borderRadiusXLarge), - boxSizing: 'border-box', pointerEvents: 'none', - }), - - enabled: theme => ({ - backgroundColor: theme.colorNeutralStrokeAccessible, - }), - - disabled: theme => ({ - backgroundColor: theme.colorNeutralBackgroundDisabled, - ...shorthands.border('1px', 'solid', theme.colorTransparentStrokeDisabled), - }), - - horizontal: theme => ({ - height: 'var(--slider-rail-size)', - top: '50%', - left: 'calc(var(--slider-thumb-size) * .5)', - right: 'calc(var(--slider-thumb-size) * .5)', - transform: 'translateY(-50%)', - }), - - vertical: theme => ({ - width: 'var(--slider-rail-size)', - left: '50%', - top: 'calc(var(--slider-thumb-size) * .5)', - bottom: 'calc(var(--slider-thumb-size) * .5)', - transform: 'translateX(-50%)', - }), -}); - -/** - * Styles for the trackWrapper slot - */ -export const useTrackWrapperStyles = makeStyles({ - trackWrapper: theme => ({ - position: 'absolute', - }), - - horizontal: theme => ({ - top: '50%', - left: 'calc(var(--slider-thumb-size) * .5)', - right: 'calc(var(--slider-thumb-size) * .5)', - }), - - vertical: theme => ({ - left: '50%', - top: 'calc(var(--slider-thumb-size) * .5)', - bottom: 'calc(var(--slider-thumb-size) * .5)', - }), -}); - -/** - * Styles for the track slot - */ -export const useTrackStyles = makeStyles({ - track: theme => ({ - position: 'absolute', - ...shorthands.borderRadius(theme.borderRadiusXLarge), - }), - - horizontal: theme => ({ - height: 'var(--slider-rail-size)', - top: '50%', - transform: 'translateY(-50%)', - minWidth: 'calc(var(--slider-thumb-size) / 4)', - }), - - vertical: theme => ({ - width: 'var(--slider-rail-size)', - left: '50%', - transform: 'translateX(-50%)', - minHeight: 'calc(var(--slider-thumb-size) / 4)', - }), - - enabled: theme => ({ - backgroundColor: theme.colorCompoundBrandBackground, - }), - - disabled: theme => ({ - backgroundColor: theme.colorNeutralForegroundDisabled, - }), -}); - -/** - * Styles for the mark slot - */ -export const useMarksWrapperStyles = makeStyles({ - marksWrapper: theme => ({ + gridRowStart: 'slider', + gridRowEnd: 'slider', + gridColumnStart: 'slider', + gridColumnEnd: 'slider', position: 'relative', - display: 'grid', - outlineStyle: 'none', - zIndex: 1, - whiteSpace: 'nowrap', - [`& .${markClassName}`]: { - backgroundColor: theme.colorNeutralBackground1, - }, - - [`& .${markLabelClassName}`]: { - ...shorthands.padding('2px'), - fontSize: '12px', - }, - - [`& .${firstMarkClassName}, .${lastMarkClassName}`]: { - opacity: '0', - }, - }), - - horizontal: theme => ({ - marginTop: 'calc(var(--slider-rail-size) + var(--slider-mark-size))', - marginLeft: 'calc(var(--slider-thumb-size) / 2)', - marginRight: 'calc(var(--slider-thumb-size) / 2)', - justifyItems: 'end', - - [`& .${markContainerClassName}`]: { - display: 'flex', - flexDirection: 'column', - transform: 'translateX(50%)', - alignItems: 'center', - }, - - [`& .${markLabelClassName}`]: { - fontFamily: theme.fontFamilyBase, - color: theme.colorNeutralForeground1, - paddingTop: 'calc(var(--slider-thumb-size) /2 )', - }, - - [`& .${markClassName}`]: { - height: '4px', - width: '1px', + backgroundImage: `linear-gradient( + var(${railDirectionVar}), + var(${railColorVar}) 0%, + var(${railColorVar}) var(${railOffsetVar}), + var(${progressColorVar}) var(${railOffsetVar}), + var(${progressColorVar}) calc(var(${railOffsetVar}) + var(${railProgressVar})), + var(${railColorVar}) calc(var(${railOffsetVar}) + var(${railProgressVar})) + )`, + outlineWidth: '1px', + outlineStyle: 'solid', + outlineColor: theme.colorTransparentStroke, + ':before': { + content: "''", + position: 'absolute', + backgroundImage: `repeating-linear-gradient( + var(${railDirectionVar}), + #0000 0%, + #0000 calc(var(${railStepsPercentVar}) - 1px), + ${theme.colorNeutralBackground1} calc(var(${railStepsPercentVar}) - 1px), + ${theme.colorNeutralBackground1} var(${railStepsPercentVar}) + )`, }, }), - vertical: theme => ({ - marginTop: 'calc(var(--slider-thumb-size) / 2)', - marginBottom: 'calc(var(--slider-thumb-size) / 2)', - marginLeft: 'calc(var(--slider-rail-size) + var(--slider-mark-size))', - justifyItems: 'start', - - [`& .${markContainerClassName}`]: { - display: 'flex', - flexDirection: 'row', - transform: 'translateY(50%)', - alignItems: 'center', - maxWidth: '100%', - maxHeight: '100%', - }, - - [`& .${markLabelClassName}`]: { - paddingLeft: 'calc(var(--slider-thumb-size) /2 )', - transform: 'scaleY(-1)', - }, - - [`& .${markClassName}`]: { - height: '1px', - width: 'var(--slider-mark-size)', + horizontal: { + width: '100%', + height: `var(${railSizeVar})`, + ':before': { + left: '-1px', + right: '-1px', + height: `var(${railSizeVar})`, }, - }), + }, - disabled: theme => ({ - [`& .${markLabelClassName}`]: { - color: theme.colorNeutralForegroundDisabled, + vertical: { + width: `var(${railSizeVar})`, + height: '100%', + ':before': { + width: `var(${railSizeVar})`, + top: '-1px', + bottom: '1px', }, - }), -}); - -/** - * Styles for the thumb slot - */ -export const useThumbWrapperStyles = makeStyles({ - thumbWrapper: theme => ({ - position: 'absolute', - outlineStyle: 'none', - zIndex: 2, - }), - - horizontal: theme => ({ - left: 'calc(var(--slider-thumb-size) / 2)', - right: 'calc(var(--slider-thumb-size) / 2)', - top: '50%', - }), - - vertical: theme => ({ - top: 'calc(var(--slider-thumb-size) / 2)', - bottom: 'calc(var(--slider-thumb-size) / 2)', - left: '50%', - }), + }, }); /** @@ -303,18 +146,14 @@ export const useThumbWrapperStyles = makeStyles({ export const useThumbStyles = makeStyles({ thumb: theme => ({ position: 'absolute', - width: 'var(--slider-thumb-size)', - height: 'var(--slider-thumb-size)', - top: '0px', - left: '0px', - bottom: '0px', - right: '0px', + width: `var(${thumbSizeVar})`, + height: `var(${thumbSizeVar})`, + pointerEvents: 'none', outlineStyle: 'none', ...shorthands.borderRadius(theme.borderRadiusCircular), - boxSizing: 'border-box', - boxShadow: `0 0 0 calc(var(--slider-thumb-size) * .2) ${theme.colorNeutralBackground1} inset`, - transform: 'translate(-50%, -50%)', - + boxShadow: `0 0 0 calc(var(${thumbSizeVar}) * .2) ${theme.colorNeutralBackground1} inset`, + backgroundColor: `var(${thumbColorVar})`, + transform: 'translateX(-50%)', ':before': { position: 'absolute', top: '0px', @@ -324,43 +163,21 @@ export const useThumbStyles = makeStyles({ ...shorthands.borderRadius(theme.borderRadiusCircular), boxSizing: 'border-box', content: "''", - ...shorthands.border('calc(var(--slider-thumb-size) * .05)', 'solid', theme.colorNeutralStroke1), + ...shorthands.border(`calc(var(${thumbSizeVar}) * .05)`, 'solid', theme.colorNeutralStroke1), }, }), - - enabled: theme => ({ - backgroundColor: theme.colorCompoundBrandBackground, - }), - disabled: theme => ({ - backgroundColor: theme.colorNeutralForegroundDisabled, ':before': { - ...shorthands.border('calc(var(--slider-thumb-size) * .05)', 'solid', theme.colorNeutralForegroundDisabled), + ...shorthands.border(`calc(var(${thumbSizeVar}) * .05)`, 'solid', theme.colorNeutralForegroundDisabled), }, }), - - horizontal: theme => ({ - top: '50%', - }), -}); - -/** - * Styles for the activeRail slot - */ -export const useActiveRailStyles = makeStyles({ - activeRail: theme => ({ - position: 'absolute', - }), - - horizontal: theme => ({ - left: 'calc(var(--slider-thumb-size) / 2)', - right: 'calc(var(--slider-thumb-size) / 2)', - }), - - vertical: theme => ({ - top: 'calc(var(--slider-thumb-size) / 2)', - bottom: 'calc(var(--slider-thumb-size) / 2)', - }), + horizontal: { + left: `var(${thumbPositionVar})`, + }, + vertical: { + transform: 'translateY(50%)', + bottom: `var(${thumbPositionVar})`, + }, }); /** @@ -369,13 +186,21 @@ export const useActiveRailStyles = makeStyles({ const useInputStyles = makeStyles({ input: { opacity: 0, - position: 'absolute', - ...shorthands.padding('0'), - ...shorthands.margin('0'), - width: '100%', - height: '100%', - touchAction: 'none', - pointerEvents: 'none', + gridRowStart: 'slider', + gridRowEnd: 'slider', + gridColumnStart: 'slider', + gridColumnEnd: 'slider', + ...shorthands.padding(0), + ...shorthands.margin(0), + }, + horizontal: { + height: `var(${thumbSizeVar})`, + width: `calc(100% + var(${thumbSizeVar}))`, + }, + vertical: { + height: `calc(100% + var(${thumbSizeVar}))`, + width: `var(${thumbSizeVar})`, + '-webkit-appearance': 'slider-vertical', }, }); @@ -384,88 +209,38 @@ const useInputStyles = makeStyles({ */ export const useSliderStyles = (state: SliderState): SliderState => { const rootStyles = useRootStyles(); - const sliderWrapperStyles = useSliderWrapper(); const railStyles = useRailStyles(); - const trackWrapperStyles = useTrackWrapperStyles(); - const trackStyles = useTrackStyles(); - const marksWrapperStyles = useMarksWrapperStyles(); - const thumbWrapperStyles = useThumbWrapperStyles(); const thumbStyles = useThumbStyles(); - const activeRailStyles = useActiveRailStyles(); const inputStyles = useInputStyles(); state.root.className = mergeClasses( sliderClassName, rootStyles.root, - rootStyles.focusIndicator, + state.vertical ? rootStyles.focusIndicatorVertical : rootStyles.focusIndicatorHorizontal, rootStyles[state.size!], state.vertical ? rootStyles.vertical : rootStyles.horizontal, state.disabled ? rootStyles.disabled : rootStyles.enabled, - rootStyles.focusIndicator, state.root.className, ); - state.sliderWrapper.className = mergeClasses( - sliderWrapperStyles.sliderWrapper, - state.vertical ? sliderWrapperStyles.vertical : sliderWrapperStyles.horizontal, - state.sliderWrapper.className, - ); - state.rail.className = mergeClasses( railStyles.rail, state.vertical ? railStyles.vertical : railStyles.horizontal, - state.disabled ? railStyles.disabled : railStyles.enabled, state.rail.className, ); - state.sliderWrapper.className = mergeClasses( - sliderWrapperStyles.sliderWrapper, - state.vertical ? sliderWrapperStyles.vertical : sliderWrapperStyles.horizontal, - state.sliderWrapper.className, - ); - - state.trackWrapper.className = mergeClasses( - trackWrapperStyles.trackWrapper, - state.vertical ? trackWrapperStyles.vertical : trackWrapperStyles.horizontal, - state.trackWrapper.className, - ); - - state.track.className = mergeClasses( - trackClassName, - trackStyles.track, - state.vertical ? trackStyles.vertical : trackStyles.horizontal, - state.disabled ? trackStyles.disabled : trackStyles.enabled, - state.track.className, - ); - - state.marksWrapper.className = mergeClasses( - marksWrapperStyles.marksWrapper, - state.vertical ? marksWrapperStyles.vertical : marksWrapperStyles.horizontal, - state.disabled && marksWrapperStyles.disabled, - state.marksWrapper.className, - ); - - state.thumbWrapper.className = mergeClasses( - thumbWrapperStyles.thumbWrapper, - state.vertical ? thumbWrapperStyles.vertical : thumbWrapperStyles.horizontal, - state.thumbWrapper.className, - ); - state.thumb.className = mergeClasses( - thumbClassName, thumbStyles.thumb, - !state.vertical && thumbStyles.horizontal, - state.disabled ? thumbStyles.disabled : thumbStyles.enabled, + state.vertical ? thumbStyles.vertical : thumbStyles.horizontal, + state.disabled && thumbStyles.disabled, state.thumb.className, ); - state.activeRail.className = mergeClasses( - activeRailStyles.activeRail, - state.vertical ? activeRailStyles.vertical : activeRailStyles.horizontal, - state.activeRail.className, + state.input.className = mergeClasses( + inputStyles.input, + state.vertical ? inputStyles.vertical : inputStyles.horizontal, + state.input.className, ); - state.input.className = mergeClasses(inputStyles.input, state.input.className); - return state; }; diff --git a/packages/react-slider/src/index.ts b/packages/react-slider/src/index.ts index e84dd8b7f473c6..f48a8541581d5a 100644 --- a/packages/react-slider/src/index.ts +++ b/packages/react-slider/src/index.ts @@ -1,2 +1 @@ export * from './Slider'; -export * from './RangedSlider'; diff --git a/packages/react-slider/src/utils/calculateSteps.ts b/packages/react-slider/src/utils/calculateSteps.ts deleted file mode 100644 index ae93b5feda8d6a..00000000000000 --- a/packages/react-slider/src/utils/calculateSteps.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import { clamp } from '@fluentui/react-utilities'; - -/** - * Calculates the `step` position based off of a `Mouse` or `Touch` event relative to the size of the rail. - */ -export const calculateSteps = ( - ev: React.PointerEvent, - railRef: React.RefObject, - min: number, - max: number, - step: number, - vertical: boolean, - dir: 'ltr' | 'rtl', -): number => { - const currentBounds = railRef?.current?.getBoundingClientRect(); - const sliderSize = (vertical ? currentBounds!.height : currentBounds!.width) || 0; - let position; - - if (vertical) { - position = currentBounds!.bottom; - } else if (dir === 'rtl') { - position = currentBounds!.right; - } else { - position = currentBounds!.left; - } - - const totalSteps = (max - min) / step; - const stepLength = sliderSize / totalSteps; - const thumbPosition = vertical ? ev.clientY : ev.clientX; - const distance = dir === 'rtl' || vertical ? position - thumbPosition : thumbPosition - position; - - return clamp(min + step * (distance / stepLength), min, max); -}; diff --git a/packages/react-slider/src/utils/findClosestThumb.ts b/packages/react-slider/src/utils/findClosestThumb.ts deleted file mode 100644 index 94942d069bc3e0..00000000000000 --- a/packages/react-slider/src/utils/findClosestThumb.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Finds the closest thumb based of a given value and returns it's key. - */ -export const findClosestThumb = (thumbArray: [number, number], incomingValue: number) => { - return Math.abs(incomingValue - thumbArray[0]) <= Math.abs(thumbArray[1] - incomingValue) - ? 'lowerValue' - : 'upperValue'; -}; diff --git a/packages/react-slider/src/utils/getKeydownValue.ts b/packages/react-slider/src/utils/getKeydownValue.ts deleted file mode 100644 index 10e4a7490f0c6d..00000000000000 --- a/packages/react-slider/src/utils/getKeydownValue.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; -import { getRTLSafeKey } from '@fluentui/react-utilities'; - -/** - * Determines the incoming value for the Slider based off of a keyboard event. - * It automatically flips the key direction if the dir parameter is rtl. - */ -export const getKeydownValue = ( - ev: React.KeyboardEvent, - currentValue: number, - min: number, - max: number, - dir: 'ltr' | 'rtl', - keyboardStep: number, -): number => { - const normalizedKey = getRTLSafeKey(ev.key, dir); - - const arrowStep = ev.shiftKey ? keyboardStep * 10 : keyboardStep; - - switch (normalizedKey) { - case 'ArrowDown': - case 'ArrowLeft': - return currentValue - arrowStep; - case 'ArrowUp': - case 'ArrowRight': - return currentValue + arrowStep; - case 'PageDown': - return currentValue - keyboardStep * 10; - case 'PageUp': - return currentValue + keyboardStep * 10; - case 'Home': - return min; - case 'End': - return max; - } - - return currentValue; -}; diff --git a/packages/react-slider/src/utils/getMarkPercent.ts b/packages/react-slider/src/utils/getMarkPercent.ts deleted file mode 100644 index efba398d389c1e..00000000000000 --- a/packages/react-slider/src/utils/getMarkPercent.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Gets the current percentage position for the marks with respect to adjacent marks. - * This is used primarily for positioning with [CSS grid](https://developer.mozilla.org/en-US/docs/Web/CSS/grid). - * - * Example - * - `Rail: width = 100px` - * - `Marks: [0, 25, 50, 75, 100]` - * - `getMarkPercent: 0%, 25%, 25%, 25%, 25%` - * - * @param markValues The marks percentage position relative to their individual positions. - */ -export const getMarkPercent = (markValues: number[]) => { - const result: string[] = []; - - // For CSS grid to work the percents array must be remapped by the previous percent - the current percent - if (markValues.length > 0) { - result.push(markValues[0] + '% '); - let prevPercent = markValues[0]; - for (let i = 1; i < markValues.length; i++) { - result.push(markValues[i] - prevPercent + '% '); - prevPercent = markValues[i]; - } - } - - return result; -}; diff --git a/packages/react-slider/src/utils/getMarkValues.ts b/packages/react-slider/src/utils/getMarkValues.ts deleted file mode 100644 index 80b8817b395de3..00000000000000 --- a/packages/react-slider/src/utils/getMarkValues.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { getPercent } from './getPercent'; - -/** - * Gets the current percentage position for the marks with relative to the rail. - * - * Example - * - `Rail: width = 100px` - * - `Marks: [0, 25, 50, 75, 100]` - * - `getMarkValue: 0%, 25%, 50%, 75%, 100%` - */ -export const getMarkValue = ( - marks: boolean | (number | { value: number; label?: string | JSX.Element; mark?: JSX.Element })[] | undefined, - min: number, - max: number, - step: number, -) => { - const valueArray: number[] = []; - - // 1. We receive a boolean: mark for every step. - if (typeof marks === 'boolean' && marks === true) { - for (let i = 0; i < (max - min) / step + 1; i++) { - valueArray.push(getPercent(min + step * i, min, max)); - } - } else if (Array.isArray(marks) && marks.length > 0) { - return marks.map(marksItem => - typeof marksItem === 'number' - ? // 2. We receive an array with numbers: mark for every value in array. - getPercent(min + marksItem, min, max) - : // 3. We receive an array with objects containing numbers and strings: - // mark and label for every value + string in each object. - getPercent(min + marksItem.value, min, max), - ); - } - return valueArray; -}; diff --git a/packages/react-slider/src/utils/getPercent.ts b/packages/react-slider/src/utils/getPercent.ts deleted file mode 100644 index 93cb5a07a413e1..00000000000000 --- a/packages/react-slider/src/utils/getPercent.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Gets the current percent of specified value between a min and max - * - * @param value - the value to find the percent - * @param min - the lowest valid value - * @param max - the highest valid value - */ -export const getPercent = (value: number, min: number, max: number) => { - return max === min ? 0 : ((value - min) / (max - min)) * 100; -}; diff --git a/packages/react-slider/src/utils/index.ts b/packages/react-slider/src/utils/index.ts deleted file mode 100644 index 7d804ac87101ac..00000000000000 --- a/packages/react-slider/src/utils/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './calculateSteps'; -export * from './findClosestThumb'; -export * from './getKeydownValue'; -export * from './getMarkPercent'; -export * from './getMarkValues'; -export * from './getPercent'; -export * from './on'; -export * from './renderMarks'; -export * from './validateRangedThumbValues'; diff --git a/packages/react-slider/src/utils/on.ts b/packages/react-slider/src/utils/on.ts deleted file mode 100644 index ea7c030fff808d..00000000000000 --- a/packages/react-slider/src/utils/on.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Adds and removes a given event listener on an element. - * - * @param element the element to listen to. - * @param eventName the event to watch. - * @param callback the callback to call when the eventName is triggered. - * @param useCapture whether the events should be dispatched to the callback first before other EventTargets. - * - * This should be replaced with a `useEvent` hook. - */ -export const on = ( - element: Element | Window | Document, - eventName: string, - callback: (ev: T) => void, - useCapture?: boolean, -) => { - element.addEventListener(eventName, (callback as unknown) as (ev: Event) => void, useCapture); - return () => element.removeEventListener(eventName, (callback as unknown) as (ev: Event) => void, useCapture); -}; diff --git a/packages/react-slider/src/utils/renderMarks.tsx b/packages/react-slider/src/utils/renderMarks.tsx deleted file mode 100644 index c4cc9174de1fe0..00000000000000 --- a/packages/react-slider/src/utils/renderMarks.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from 'react'; -import { mergeClasses } from '@fluentui/react-make-styles'; -import { - firstMarkClassName, - lastMarkClassName, - markClassName, - markContainerClassName, - markLabelClassName, -} from '../components/Slider/useSliderStyles'; - -/** - * Renders the marks - * - * @param markValues The marks percentage position relative to their individual positions. - * @param marks The provided marks prop from the Slider component. - */ -export const renderMarks = ( - markValues: number[], - marks: boolean | (number | { value: number; label?: string | JSX.Element; mark?: JSX.Element })[], -) => - markValues.map((value, i) => { - const marksItem = typeof marks === 'boolean' ? undefined : marks[i]; - - return ( -
- {marksItem !== undefined && typeof marksItem === 'object' && marksItem.mark ? ( - marksItem.mark - ) : ( -
- )} - {marksItem !== undefined && typeof marksItem === 'object' && marksItem.label && ( -
- {marksItem.label} -
- )} -
- ); - }); diff --git a/packages/react-slider/src/utils/validateRangedThumbValues.ts b/packages/react-slider/src/utils/validateRangedThumbValues.ts deleted file mode 100644 index 8250f0dc0131ee..00000000000000 --- a/packages/react-slider/src/utils/validateRangedThumbValues.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { clamp } from '@fluentui/react-utilities'; - -/** - * Clamps and sorts the values in RangedSlider to a given min and max - */ -export const validateRangedThumbValues = (thumbValues: [number, number], min: number, max: number): [number, number] => - thumbValues.map(value => clamp(value, min, max)).sort((a, b) => a - b) as [number, number];