5180 fichiers 38.5 Mo 0 téléchargements 147 vues mis à jour il y a 4 heures ui
Slider.tsx 6.7 Ko Télécharger le fichier
import { Slider as BaseSlider } from '@base-ui/react/slider';
import { CSSProperties, FC, ReactNode, useState } from 'react';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import { Button } from './Button';
import { Flex } from './Flex';

export interface SliderThumbState {
    index: number;
    value: number | number[];
    valueNow: number;
}

export interface SliderProps {
    min?: number;
    max?: number;
    step?: number;
    value?: number | number[];
    defaultValue?: number | number[];
    onChange?: (value: any, thumbIndex: number) => void;
    disabled?: boolean;
    disabledButton?: boolean;
    className?: string;
    style?: CSSProperties;
    trackClassName?: string;
    thumbClassName?: string;
    thumbAriaLabel?: string;
    /** `vertical` renders the bare Base UI parts (max at the top, no step buttons) so the caller styles them. */
    orientation?: 'horizontal' | 'vertical';
    /** With `horizontal`: render the bare Base UI parts too (no step buttons, no indicator), like `vertical`. */
    bare?: boolean;
    title?: string;
    controlClassName?: string;
    trackStyle?: CSSProperties;
    trackTestId?: string;
    renderThumbContent?: (state: SliderThumbState) => ReactNode;
}

const toArray = (value: number | number[] | undefined, fallback = 0): number[] => {
    if (Array.isArray(value)) return value;
    if (typeof value === 'number') return [value];

    return [fallback];
};

const cn = (...parts: (string | undefined | false)[]) => parts.filter(Boolean).join(' ');

export const Slider: FC<SliderProps> = (props) => {
    const {
        disabledButton,
        disabled,
        max = 100,
        min = 0,
        step = 1,
        value,
        defaultValue,
        onChange,
        className,
        style,
        trackClassName,
        thumbClassName,
        thumbAriaLabel,
        orientation = 'horizontal',
        bare = false,
        title,
        controlClassName,
        trackStyle,
        trackTestId,
        renderThumbContent
    } = props;

    const minimum = typeof min === 'number' ? min : 0;
    const maximum = typeof max === 'number' ? max : 0;
    // Without `value` the slider keeps its own position, seeded from `defaultValue`.
    const [innerValue, setInnerValue] = useState<number | number[] | undefined>(defaultValue);
    const source = value !== undefined ? value : innerValue;
    const valueArr = toArray(source, minimum);
    const currentValue = valueArr[0] ?? minimum;
    const buttonStep = typeof step === 'number' && step > 0 ? step : 1;
    const isRange = valueArr.length > 1;

    const roundToStep = (nextValue: number) => {
        const decimalStep = buttonStep.toString();
        const precision = decimalStep.includes('.') ? decimalStep.length - decimalStep.indexOf('.') - 1 : 0;

        return parseFloat(nextValue.toFixed(precision));
    };

    const emit = (next: number[], thumbIndex = 0) => {
        if (value === undefined) setInnerValue(isRange ? next : next[0]);

        if (!onChange) return;

        if (isRange) onChange(next, thumbIndex);
        else onChange(next[0], thumbIndex);
    };

    const stepDown = () => {
        const next = Math.max(minimum, roundToStep(currentValue - buttonStep));

        emit([next, ...valueArr.slice(1)]);
    };

    const stepUp = () => {
        const next = Math.min(maximum, roundToStep(currentValue + buttonStep));

        emit([next, ...valueArr.slice(1)]);
    };

    const renderThumbElement = (i: number) => {
        const state: SliderThumbState = {
            index: i,
            value: isRange ? valueArr : currentValue,
            valueNow: valueArr[i] ?? 0
        };

        return (
            <BaseSlider.Thumb key={i} index={i} aria-label={thumbAriaLabel} className={cn('thumb', `thumb-${i}`, thumbClassName)}>
                {renderThumbContent?.(state)}
            </BaseSlider.Thumb>
        );
    };

    if (orientation === 'vertical' || bare) {
        const prefix = orientation === 'vertical' ? 'octane-slider-vertical' : 'octane-slider-bare';

        return (
            <BaseSlider.Root
                disabled={disabled}
                className={cn(prefix, className)}
                style={style}
                title={title}
                max={max}
                min={min}
                step={step}
                orientation={orientation}
                thumbAlignment="center"
                value={isRange ? valueArr : currentValue}
                onValueChange={(next, details) => emit(toArray(next), details.activeThumbIndex)}
            >
                <BaseSlider.Control className={cn(`${prefix}-control`, controlClassName)}>
                    <BaseSlider.Track className={cn(`${prefix}-track`, trackClassName)} style={trackStyle} data-testid={trackTestId} />
                    {valueArr.map((_, i) => (
                        <BaseSlider.Thumb key={i} index={i} aria-label={thumbAriaLabel} className={cn(`${prefix}-thumb`, thumbClassName)}>
                            {renderThumbContent?.({ index: i, value: isRange ? valueArr : currentValue, valueNow: valueArr[i] ?? 0 })}
                        </BaseSlider.Thumb>
                    ))}
                </BaseSlider.Control>
            </BaseSlider.Root>
        );
    }

    return (
        <Flex fullWidth gap={1} classNames={['octane-slider-wrapper']}>
            {!disabledButton && (
                <Button classNames={['octane-slider-button', 'octane-slider-button-left']} disabled={disabled || minimum >= currentValue} onClick={stepDown}>
                    <FaAngleLeft />
                </Button>
            )}
            <BaseSlider.Root
                disabled={disabled}
                className={cn('octane-slider', 'relative', 'min-w-0', 'grow', className)}
                style={style}
                max={max}
                min={min}
                step={step}
                thumbAlignment="center"
                value={isRange ? valueArr : currentValue}
                onValueChange={(next, details) => emit(toArray(next), details.activeThumbIndex)}
            >
                <BaseSlider.Control className="flex h-full w-full grow items-center">
                    <BaseSlider.Track className={cn('track', 'track-1', 'grow', trackClassName)}>
                        <BaseSlider.Indicator className={cn('track', 'track-0', trackClassName)} />
                    </BaseSlider.Track>
                    {valueArr.map((_, i) => renderThumbElement(i))}
                </BaseSlider.Control>
            </BaseSlider.Root>
            {!disabledButton && (
                <Button classNames={['octane-slider-button', 'octane-slider-button-right']} disabled={disabled || maximum <= currentValue} onClick={stepUp}>
                    <FaAngleRight />
                </Button>
            )}
        </Flex>
    );
};
15Sujets
1Dépôts
20Membres
7En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser