import {
Fragment,
type HTMLAttributes,
type ReactElement,
type ReactNode,
} from "react";
import { getPercentage } from "../utils/getPercentage.js";
import {
type RangeStepsOptions,
getRangeSteps,
} from "../utils/getRangeSteps.js";
import { SliderMark } from "./SliderMark.js";
import {
type CustomizableSliderMarkLabelProps,
SliderMarkLabel,
} from "./SliderMarkLabel.js";
/**
* @since 6.0.0
*/
export interface SliderValueMark {
/**
* An optional label to display alongside the current mark. This will be
* positioned below the mark for horizontal sliders and to the right for
* vertical sliders.
*/
label?: ReactNode;
/**
* The value of the mark that should be a valid step within the slider.
*/
value: number;
}
/**
* @since 6.0.0
*/
export interface SliderValueMarkState {
/** The {@link SliderValueMark.value} */
value: number;
/**
* No idea if this is actually useful (maybe custom styles?), but it is the
* current percentage the mark is offset and is what is set as the
* `left`/`top` values for the mark.
*/
offset: string;
/**
* This will be `true` when the mark is being covered by the slider track's
* active state.
*/
active: boolean;
}
/**
* @since 6.0.0
*/
export interface SliderMarksOptions {
/**
* Set this to `true` to display a mark for each step within the slider. This
* can be used alongside the {@link getMarkProps} and
* {@link getMarkLabelProps} to customize the styles or display a label for
* the mark.
*
* @example Custom Marks
* ```tsx
* const slider = useSlider({ step: 10 });
*
*
* ```
*/
marks?: boolean | readonly SliderValueMark[];
/**
* This can be used to override any styles for the specific mark.
*/
getMarkProps?: (
options: SliderValueMarkState
) => HTMLAttributes | undefined;
/**
* This can be used to override any styles for a specific mark's label or
* display a label dynamically.
*
* @example Dynamic Labels
* ```tsx
* {
* if (value % 10 !== 0) {
* return;
* }
*
* return {
* children: `${value} degrees`,
* className: cnb(active && styles.somethingCustom),
* };
* }}
* />
* ```
*/
getMarkLabelProps?: (
options: SliderValueMarkState
) => Partial | undefined;
}
/**
* @internal
* @since 6.0.0
*/
export interface SliderValueMarksProps
extends RangeStepsOptions, Required {
vertical: boolean;
thumb1Value: number;
thumb2Value: number;
isRangeSlider: boolean;
}
/**
* @internal
* @since 6.0.0
*/
export function SliderValueMarks(props: SliderValueMarksProps): ReactElement {
const {
min,
max,
step,
marks: propMarks,
vertical,
thumb1Value,
thumb2Value,
isRangeSlider,
getMarkProps,
getMarkLabelProps,
} = props;
let marks: readonly SliderValueMark[];
if (typeof propMarks === "boolean") {
const steps = getRangeSteps({ min, max, step }) + 1;
marks = Array.from({ length: steps }, (_, i) => ({
value: min + i * step,
}));
} else {
marks = propMarks;
}
return (
<>
{marks.map(({ value, label: markLabel }) => {
// I can't think of a good name, but this is when the slider's track's
// active color is covering the mark which requires different styles
let active: boolean;
let percentage = getPercentage({ min, max, value }) * 100;
let markValue = value;
if (vertical) {
// need to reverse the percentage since it uses `top` for positioning
// where the max value is at the top instead of bottom
percentage = 100 - percentage;
// need to reverse the mark's value as well for the same reason as
// above
markValue = max - value;
}
if (isRangeSlider) {
active = markValue > thumb1Value && markValue < thumb2Value;
} else {
active = markValue < thumb1Value;
}
const offset = `${percentage}%`;
const markProps = getMarkProps({ value, active, offset });
const labelProps = getMarkLabelProps({ value, active, offset });
const label = markLabel ?? labelProps?.children ?? null;
return (
{label !== null && (
{label}
)}
);
})}
>
);
}