"use client"; import { type HTMLAttributes, type ReactElement, type ReactNode, type Ref, useRef, } from "react"; import { type TooltipProps } from "../tooltip/Tooltip.js"; import { type LabelRequiredForA11y, type PropsWithRef, type UseStateSetter, } from "../types.js"; import { useEnsuredId } from "../useEnsuredId.js"; import { identity } from "../utils/identity.js"; import { type SliderAddonProps, SliderContainer } from "./SliderContainer.js"; import { type ConfigurableSliderThumbProps, SliderThumb, type SliderThumbPresentation, type SliderThumbProps, } from "./SliderThumb.js"; import { SliderTrack } from "./SliderTrack.js"; import { type SliderMarksOptions, SliderValueMarks, } from "./SliderValueMarks.js"; import { getJumpValue, getSliderInputName } from "./sliderUtils.js"; import { type RangeSliderState } from "./useRangeSlider.js"; import { type SliderState, type SliderValueOptions } from "./useSlider.js"; import { useSliderDraggable } from "./useSliderDraggable.js"; // NOTE: The augmentation appears in this file since no type definitions are // ever imported from the `sliderStyles` file. declare module "react" { interface CSSProperties { "--rmd-slider-color"?: string; "--rmd-slider-active-color"?: string; "--rmd-slider-inactive-color"?: string; "--rmd-slider-size"?: string | number; "--rmd-slider-active-size"?: string | number; "--rmd-slider-inactive-size"?: string | number; "--rmd-slider-vertical-size"?: string | number; "--rmd-slider-offset-1"?: string; "--rmd-slider-offset-2"?: string; "--rmd-slider-tooltip-scale"?: string | number; "--rmd-slider-tooltip-translate"?: string | number; "--rmd-slider-mark-offset"?: string; "--rmd-slider-mark-active-color"?: string; "--rmd-slider-mark-active-opacity"?: string | number; "--rmd-slider-mark-inactive-color"?: string; "--rmd-slider-mark-inactive-opacity"?: string | number; } } const emptyString = (): string => ""; const noop = (): undefined => { // do nothing }; /** * @since 2.5.0 */ export interface BaseSliderProps extends HTMLAttributes, SliderThumbPresentation, SliderValueOptions, SliderAddonProps, SliderMarksOptions { /** * This can be used to apply a ref to the container element since this * component does not use `forwardRef`. */ containerRef?: Ref; /** * The amount to jump the slider's value when the `PageUp` or `PageDown` * key is pressed. * * The default value is 1/10th of the range. * * @defaultValue `(numberOfSteps / 10) * step` */ jump?: number; /** @defaultValue `false` */ disabled?: boolean; /** @defaultValue `false` */ vertical?: boolean; /** * This can be used to apply custom styles or a `ref` to the track element if * needed. */ trackProps?: PropsWithRef>; /** * This can be used to configure any additional tooltip props like the * CSS transition `classNames`, styles, etc. * * This will only be used when {@link discrete} is `true`. * * Note: The `position` will always be `"above"` for horizontal sliders and * `"left"` for vertical sliders. */ tooltipProps?: Omit, "position">; } /** * @since 2.5.0 * @since 6.0.0 Only requires `value` and `setValue` props instead of all the * slider controls. * @since 6.0.0 The `thumbLabel` and `thumbLabelledBy` props were removed. Use * the `aria-label` or `aria-labelledby` props instead. * @since 6.0.0 The `baseId` prop was removed in favor of `id` or `*Props.id` */ export interface SliderProps extends BaseSliderProps, SliderState { /** * Convenience pass-through prop to {@link ConfigurableSliderThumbProps.name} which * will be applied to the hidden `` */ name?: string; /** * Any additional props that should be provided to the thumb element. This can * be useful for applying additional styling. */ thumbProps?: ConfigurableSliderThumbProps; /** * This can be used to update the discrete slider's tooltip props. * * @example Custom Styles * ```tsx * ({ * className: cssUtils({ * backgroundColor: value < 30 ? "warning" : undefined, * }), * })} * /> * ``` */ getTooltipProps?: (value: number) => Partial | undefined; /** * This can be used to update the discrete slider's value tooltip. * * @example More Value Information * ```tsx * ( * }> * {value} * * )} * /> * ``` * * This will only be used when {@link discrete} is `true`. * * @defaultValue `(value) => value` */ getTooltipChildren?: (value: number) => ReactNode; } /** * @since 2.5.0 * @since 6.0.0 Only requires `rangeValue` and `setRangeValue` props instead of * all the slider controls. * @since 6.0.0 The `thumb1Label`, `thumb1LabelledBy`, `thumb1Props`, * `thumb2Label`, `thumb2LabelledBy` and `thumb2Props` were renamed to * `minThumbLabel`, `minThumbLabelledBy`, `minThumbProps`, `maxThumbLabel`, * `maxThumbLabelledBy`, and `maxThumbProps` respectively. */ export interface RangeSliderProps extends BaseSliderProps, RangeSliderState { /** * Convenience pass-through prop for {@link * ConfigurableSliderThumbProps.name} for both the {@link minThumbProps} and * {@link maxThumbProps}. */ name?: string; /** * Any additional props that should be provided to the min value thumb * element. This can be useful for applying additional styling. */ minThumbProps?: ConfigurableSliderThumbProps; /** * Any additional props that should be provided to the max value thumb * element. This can be useful for applying additional styling. */ maxThumbProps?: ConfigurableSliderThumbProps; /** * The `aria-label` to apply to the min value. * * Note: Either this prop or the {@link minThumbLabelledBy} are required for * accessibility. * * @defaultValue `"Min"` */ minThumbLabel?: string; /** * Set this to an element's id that labels the min value. * * Note: Either this prop or the {@link minThumbLabel} are required for * accessibility. */ minThumbLabelledBy?: string; /** * The `aria-label` to apply to the max value. * * Note: Either this prop or the {@link maxThumbLabelledBy} are required for * accessibility. * * @defaultValue `"Max"` */ maxThumbLabel?: string; /** * Set this to an element's id that labels the max value. * * Note: Either this prop or the {@link maxThumbLabel} are required for * accessibility. */ maxThumbLabelledBy?: string; /** * This can be used to update the discrete slider's tooltip props. * * @example Custom Styles * ```tsx * ({ * className: cssUtils({ * backgroundColor: value < 30 && isFirstThumb ? "warning" : undefined, * }), * })} * /> * ``` */ getTooltipProps?: ( value: number, isFirstThumb: boolean ) => Partial; /** * This can be used to update the discrete slider's value tooltip. * * @example More Value Information * ```tsx * ( * : * {value} * * )} * /> * ``` * * This will only be used when {@link discrete} is `true`. * * @defaultValue `(value) => value` */ getTooltipChildren?: (value: number, isFirstThumb: boolean) => ReactNode; } /** * **Client Component** * * @example Simple Example * ```tsx * import { Form } from "@react-md/core/form/Form"; * import { Slider } from "@react-md/core/form/Slider"; * import { useSlider } from "@react-md/core/form/useSlider"; * import type { ReactElement } from "react"; * * function Example(): ReactElement { * const slider = useSlider({ * // these are the defaults and can be changed * min: 0, * max: 100, * step: 1, * defaultValue: 50, * }); * * // if you need access to the current value or manually change the value * // yourself. * const { value, setValue } = slider; * * return ( *
* * * ); * } * ``` * * @example Range Slider Example * ```tsx * import { Fieldset } from "@react-md/core/form/Fieldset"; * import { Form } from "@react-md/core/form/Form"; * import { Legend } from "@react-md/core/form/Legend"; * import { Slider } from "@react-md/core/form/Slider"; * import { useRangeSlider } from "@react-md/core/form/useRangeSlider"; * import type { ReactElement } from "react"; * import { useId } from "react"; * * function Example(): ReactElement { * const slider = useRangeSlider({ * // these are the defaults and can be changed * min: 0, * max: 100, * step: 1, * defaultValue: [0, 100], * }); * * // if you need access to the current value or manually change the value * // yourself. * const { rangeValue, setRangeValue } = slider; * const [minPrice, maxPrice] = rangeValue; * * return ( *
*
* Price Range * *
*
* ); * } * ``` * * @see {@link https://react-md.dev/components/slider | Slider Demos} * @since 2.5.0 * @since 6.0.0 The `Slider` and `RangeSlider` have been combined into the * single `Slider` component and removed the `label` support. * @since 6.0.0 Each thumb includes an invisible `` instead * of an ``. */ export function Slider(props: LabelRequiredForA11y): ReactElement; export function Slider(props: RangeSliderProps): ReactElement; export function Slider( props: LabelRequiredForA11y | RangeSliderProps ): ReactElement { const { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, id: propId, name, min = 0, max = 100, step = 1, jump: propJump, vertical = false, discrete = false, disabled = false, getValueText = emptyString, children, marks = false, value, setValue, rangeValue, setRangeValue, trackProps, thumbProps, minThumbProps, minThumbLabel, minThumbLabelledBy, maxThumbProps, maxThumbLabel, maxThumbLabelledBy, tooltipProps, containerRef, getMarkProps = noop, getMarkLabelProps = noop, tooltipVisibility = "auto", getTooltipProps = noop, getTooltipChildren = identity, disableSmoothDragging = !!marks, ...remaining } = props as SliderProps & RangeSliderProps; const jump = getJumpValue({ min, max, step, jump: propJump }); const isRangeSlider = typeof value !== "number"; const thumb1Id = useEnsuredId(propId, "slider"); const thumb2Id = `${thumb1Id}-2`; const thumb1Ref = useRef(null); const thumb2Ref = useRef(null); const { thumb1Name, thumb2Name } = getSliderInputName(name, isRangeSlider); let thumb1Max = max; let thumb2Min = min; let thumb1Label: string | undefined; let thumb1LabelledBy: string | undefined; let thumb2Label: string | undefined; let thumb2LabelledBy: string | undefined; let thumb1Value: number; let thumb2Value: number; let setThumb1Value: UseStateSetter; let setThumb2Value: UseStateSetter; if (!isRangeSlider) { thumb1Value = value; setThumb1Value = setValue; thumb2Value = max; setThumb2Value = noop; thumb1Label = ariaLabel; thumb1LabelledBy = ariaLabelledBy; } else { thumb1LabelledBy = minThumbLabelledBy; thumb1Label = minThumbLabel || (minThumbLabelledBy ? undefined : "Min"); thumb2LabelledBy = maxThumbLabelledBy; thumb2Label = maxThumbLabel || (maxThumbLabelledBy ? undefined : "Max"); [thumb1Value, thumb2Value] = rangeValue; thumb1Max = thumb2Value - step; thumb2Min = thumb1Value + step; setThumb1Value = (valueOrSetter) => { setRangeValue((prevRangeValue) => { const value = typeof valueOrSetter === "number" ? valueOrSetter : valueOrSetter(prevRangeValue[0]); return [value, prevRangeValue[1]]; }); }; setThumb2Value = (valueOrSetter) => { setRangeValue((prevRangeValue) => { const value = typeof valueOrSetter === "number" ? valueOrSetter : valueOrSetter(prevRangeValue[1]); return [prevRangeValue[0], value]; }); }; } const { onKeyDown: thumb1OnKeyDown, onMouseUp: thumb1OnMouseUp, onMouseDown: thumb1OnMouseDown, onMouseMove: thumb1OnMouseMove, onTouchStart: thumb1OnTouchStart, onTouchMove: thumb1OnTouchMove, dragPercentage: thumb1DragPercentage, draggableRef: thumb1DraggableRef, dragging: thumb1Dragging, } = useSliderDraggable({ jump, ref: thumb1Ref, min, max: thumb1Max, rangeMax: max, step, value: thumb1Value, setValue: setThumb1Value, disabled, vertical, }); const { onKeyDown: thumb2OnKeyDown, onMouseUp: thumb2OnMouseUp, onMouseDown: thumb2OnMouseDown, onMouseMove: thumb2OnMouseMove, onTouchStart: thumb2OnTouchStart, onTouchMove: thumb2OnTouchMove, dragPercentage: thumb2DragPercentage, draggableRef: thumb2DraggableRef, dragging: thumb2Dragging, } = useSliderDraggable({ jump, ref: thumb2Ref, min: thumb2Min, max, rangeMin: min, step, value: thumb2Value, setValue: setThumb2Value, vertical, disabled, }); const dragging = thumb1Dragging || thumb2Dragging; const sharedThumbProps = { step, animate: !dragging, discrete, disabled, vertical, getValueText, tooltipProps, getTooltipProps, getTooltipChildren, tooltipVisibility, disableSmoothDragging, } as const satisfies Partial; return ( { setThumb1Value(event.currentTarget.valueAsNumber); }} onKeyDown={thumb1OnKeyDown} /> {isRangeSlider && ( { setThumb2Value(event.currentTarget.valueAsNumber); }} onKeyDown={thumb2OnKeyDown} /> )} {marks && ( )} {children} ); }