feat(ui): redesign range input with dynamic progress styling and improved accessibility
Revamp the range input component to support dynamic progress indication using CSS custom properties and linear gradients. Add logic to compute and set the progress percentage based on current value, min, and max. Refine focus and disabled states for better accessibility and usability. Update styling for both WebKit and Mozilla engines to ensure consistent appearance. This change enhances visual feedback and modernizes the range slider UI.
This commit is contained in:
parent
4086f80066
commit
7c8577ba32
1 changed files with 51 additions and 7 deletions
|
|
@ -1,12 +1,50 @@
|
||||||
import type { InputHTMLAttributes } from 'react';
|
import type { CSSProperties, InputHTMLAttributes } from 'react';
|
||||||
import { cn } from './cn';
|
import { cn } from './cn';
|
||||||
|
|
||||||
|
type RangeStyle = CSSProperties & {
|
||||||
|
'--range-progress'?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function toNumber(value: string | number | readonly string[] | undefined, fallback: number): number {
|
||||||
|
if (typeof value === 'number') return Number.isFinite(value) ? value : fallback;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const parsed = Number.parseFloat(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRangeProgress(props: InputHTMLAttributes<HTMLInputElement>): number {
|
||||||
|
const min = toNumber(props.min, 0);
|
||||||
|
const max = toNumber(props.max, 100);
|
||||||
|
const value = toNumber(props.value, min);
|
||||||
|
const span = max - min;
|
||||||
|
if (span <= 0) return 0;
|
||||||
|
const progress = ((value - min) / span) * 100;
|
||||||
|
return Math.min(100, Math.max(0, progress));
|
||||||
|
}
|
||||||
|
|
||||||
const rangeInputClass = cn(
|
const rangeInputClass = cn(
|
||||||
'w-full cursor-pointer appearance-none rounded-lg bg-surface-sunken accent-accent',
|
'h-6 w-full cursor-pointer appearance-none bg-transparent [--range-track-h:0.5rem] [--range-thumb-h:1.25rem]',
|
||||||
'[&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:rounded-lg [&::-webkit-slider-runnable-track]:bg-surface-sunken',
|
'focus-visible:outline-none focus-visible:[&::-webkit-slider-thumb]:ring-4 focus-visible:[&::-webkit-slider-thumb]:ring-accent/25',
|
||||||
'[&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent',
|
'focus-visible:[&::-moz-range-thumb]:ring-4 focus-visible:[&::-moz-range-thumb]:ring-accent/25',
|
||||||
'[&::-moz-range-track]:h-1.5 [&::-moz-range-track]:rounded-lg [&::-moz-range-track]:bg-surface-sunken',
|
'disabled:cursor-not-allowed disabled:opacity-60',
|
||||||
'[&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-accent',
|
'[&::-webkit-slider-runnable-track]:h-[var(--range-track-h)] [&::-webkit-slider-runnable-track]:rounded-full',
|
||||||
|
'[&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,var(--accent)_0%,var(--accent)_var(--range-progress),color-mix(in_srgb,var(--offbase)_82%,var(--background))_var(--range-progress),color-mix(in_srgb,var(--offbase)_82%,var(--background))_100%)]',
|
||||||
|
'[&::-webkit-slider-runnable-track]:shadow-[inset_0_1px_0_color-mix(in_srgb,var(--foreground)_10%,transparent)]',
|
||||||
|
'[&::-webkit-slider-thumb]:mt-[calc((var(--range-track-h)-var(--range-thumb-h))/2)] [&::-webkit-slider-thumb]:h-[var(--range-thumb-h)] [&::-webkit-slider-thumb]:w-[var(--range-thumb-h)] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full',
|
||||||
|
'[&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-[color-mix(in_srgb,var(--background)_78%,white)]',
|
||||||
|
'[&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:shadow-[0_2px_10px_color-mix(in_srgb,var(--accent)_45%,transparent)]',
|
||||||
|
'[&::-webkit-slider-thumb]:transition-transform [&::-webkit-slider-thumb]:duration-150 [&::-webkit-slider-thumb]:ease-out',
|
||||||
|
'active:[&::-webkit-slider-thumb]:scale-[1.07]',
|
||||||
|
'[&::-moz-range-track]:h-[var(--range-track-h)] [&::-moz-range-track]:rounded-full [&::-moz-range-track]:border-0',
|
||||||
|
'[&::-moz-range-track]:bg-[color-mix(in_srgb,var(--offbase)_82%,var(--background))]',
|
||||||
|
'[&::-moz-range-progress]:h-[var(--range-track-h)] [&::-moz-range-progress]:rounded-full [&::-moz-range-progress]:border-0 [&::-moz-range-progress]:bg-accent',
|
||||||
|
'[&::-moz-range-thumb]:h-[var(--range-thumb-h)] [&::-moz-range-thumb]:w-[var(--range-thumb-h)] [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2',
|
||||||
|
'[&::-moz-range-thumb]:border-[color-mix(in_srgb,var(--background)_78%,white)] [&::-moz-range-thumb]:bg-accent',
|
||||||
|
'[&::-moz-range-thumb]:shadow-[0_2px_10px_color-mix(in_srgb,var(--accent)_45%,transparent)]',
|
||||||
|
'[&::-moz-range-thumb]:transition-transform [&::-moz-range-thumb]:duration-150 [&::-moz-range-thumb]:ease-out',
|
||||||
|
'active:[&::-moz-range-thumb]:scale-[1.07]',
|
||||||
);
|
);
|
||||||
|
|
||||||
function rangeInputClassName(className?: string) {
|
function rangeInputClassName(className?: string) {
|
||||||
|
|
@ -15,7 +53,13 @@ function rangeInputClassName(className?: string) {
|
||||||
|
|
||||||
export function RangeInput({
|
export function RangeInput({
|
||||||
className,
|
className,
|
||||||
|
style,
|
||||||
...props
|
...props
|
||||||
}: InputHTMLAttributes<HTMLInputElement>) {
|
}: InputHTMLAttributes<HTMLInputElement>) {
|
||||||
return <input type="range" className={rangeInputClassName(className)} {...props} />;
|
const rangeStyle: RangeStyle = {
|
||||||
|
...style,
|
||||||
|
'--range-progress': `${resolveRangeProgress(props)}%`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <input type="range" className={rangeInputClassName(className)} style={rangeStyle} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue