192 lines
4.7 KiB
TypeScript
Executable File
192 lines
4.7 KiB
TypeScript
Executable File
import React, {ReactNode} from 'react';
|
||
import clsx from 'clsx';
|
||
import {getInputFieldClassNames} from '../input-field/get-input-field-class-names';
|
||
import {UseSliderProps, UseSliderReturn} from './use-slider';
|
||
|
||
export interface BaseSliderProps extends UseSliderProps {
|
||
slider: UseSliderReturn;
|
||
children: ReactNode;
|
||
}
|
||
|
||
export function BaseSlider(props: BaseSliderProps) {
|
||
const {
|
||
size = 'md',
|
||
inline,
|
||
label,
|
||
showValueLabel = !!label,
|
||
className,
|
||
width = 'w-full',
|
||
slider,
|
||
children,
|
||
trackColor = 'primary',
|
||
fillColor = 'primary',
|
||
} = props;
|
||
|
||
const {
|
||
domProps,
|
||
trackRef,
|
||
getThumbPercent,
|
||
getThumbValueLabel,
|
||
labelId,
|
||
groupId,
|
||
thumbIds,
|
||
isDisabled,
|
||
numberFormatter,
|
||
minValue,
|
||
maxValue,
|
||
step,
|
||
values,
|
||
getValueLabel,
|
||
} = slider;
|
||
|
||
let outputValue = '';
|
||
let maxLabelLength = Math.max(
|
||
[...numberFormatter.format(minValue)].length,
|
||
[...numberFormatter.format(maxValue)].length,
|
||
[...numberFormatter.format(step)].length,
|
||
);
|
||
|
||
if (getValueLabel) {
|
||
outputValue = getValueLabel(values[0]);
|
||
} else if (values.length === 1) {
|
||
outputValue = getThumbValueLabel(0);
|
||
} else if (values.length === 2) {
|
||
// This should really use the NumberFormat#formatRange proposal...
|
||
// https://github.com/tc39/ecma402/issues/393
|
||
// https://github.com/tc39/proposal-intl-numberformat-v3#formatrange-ecma-402-393
|
||
outputValue = `${getThumbValueLabel(0)} – ${getThumbValueLabel(1)}`;
|
||
maxLabelLength =
|
||
3 +
|
||
2 *
|
||
Math.max(
|
||
maxLabelLength,
|
||
[...numberFormatter.format(minValue)].length,
|
||
[...numberFormatter.format(maxValue)].length,
|
||
);
|
||
}
|
||
const style = getInputFieldClassNames({
|
||
size,
|
||
disabled: isDisabled,
|
||
labelDisplay: 'flex',
|
||
});
|
||
|
||
const wrapperClassname = clsx('touch-none', className, width, {
|
||
'flex items-center': inline,
|
||
});
|
||
|
||
return (
|
||
<div className={wrapperClassname} role="group" id={groupId}>
|
||
{(label || showValueLabel) && (
|
||
<div className={clsx(style.label, 'select-none')}>
|
||
{label && (
|
||
<label
|
||
onClick={() => {
|
||
// Safari does not focus <input type="range"> elements when clicking on an associated <label>,
|
||
// so do it manually. In addition, make sure we show the focus ring.
|
||
document.getElementById(thumbIds[0])?.focus();
|
||
}}
|
||
id={labelId}
|
||
htmlFor={groupId}
|
||
>
|
||
{label}
|
||
</label>
|
||
)}
|
||
{showValueLabel && (
|
||
<output
|
||
htmlFor={thumbIds[0]}
|
||
className="ml-auto text-right"
|
||
aria-live="off"
|
||
style={
|
||
!maxLabelLength
|
||
? undefined
|
||
: {
|
||
width: `${maxLabelLength}ch`,
|
||
minWidth: `${maxLabelLength}ch`,
|
||
}
|
||
}
|
||
>
|
||
{outputValue}
|
||
</output>
|
||
)}
|
||
</div>
|
||
)}
|
||
<div
|
||
ref={trackRef}
|
||
className={clsx('relative', getWrapperHeight(props))}
|
||
{...domProps}
|
||
role="presentation"
|
||
>
|
||
<div
|
||
className={clsx(
|
||
'absolute inset-0 m-auto rounded',
|
||
getTrackColor(trackColor, isDisabled),
|
||
getTrackHeight(size),
|
||
)}
|
||
/>
|
||
<div
|
||
className={clsx(
|
||
'absolute inset-0 my-auto rounded',
|
||
getFillColor(fillColor, isDisabled),
|
||
getTrackHeight(size),
|
||
)}
|
||
style={{width: `${Math.max(getThumbPercent(0) * 100, 0)}%`}}
|
||
/>
|
||
{children}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function getWrapperHeight({size, wrapperHeight}: UseSliderProps): string {
|
||
if (wrapperHeight) return wrapperHeight;
|
||
switch (size) {
|
||
case 'xs':
|
||
return 'h-14';
|
||
case 'sm':
|
||
return 'h-20';
|
||
default:
|
||
return 'h-30';
|
||
}
|
||
}
|
||
|
||
function getTrackHeight(size: UseSliderProps['size']): string {
|
||
switch (size) {
|
||
case 'xs':
|
||
return 'h-2';
|
||
case 'sm':
|
||
return 'h-3';
|
||
default:
|
||
return 'h-4';
|
||
}
|
||
}
|
||
|
||
function getTrackColor(color: string, isDisabled: boolean): string {
|
||
if (isDisabled) {
|
||
color = 'disabled';
|
||
}
|
||
switch (color) {
|
||
case 'disabled':
|
||
return 'bg-slider-disabled/60';
|
||
case 'primary':
|
||
return 'bg-primary-light';
|
||
case 'neutral':
|
||
return 'bg-divider';
|
||
default:
|
||
return color;
|
||
}
|
||
}
|
||
|
||
function getFillColor(color: string, isDisabled: boolean): string {
|
||
if (isDisabled) {
|
||
color = 'disabled';
|
||
}
|
||
switch (color) {
|
||
case 'disabled':
|
||
return 'bg-slider-disabled';
|
||
case 'primary':
|
||
return 'bg-primary';
|
||
default:
|
||
return color;
|
||
}
|
||
}
|