"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 (
*
* );
* }
* ```
*
* @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}
);
}