191
common/resources/client/ui/forms/slider/base-slider.tsx
Executable file
191
common/resources/client/ui/forms/slider/base-slider.tsx
Executable file
@@ -0,0 +1,191 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user