slider updates

This commit is contained in:
Nate Kelley 2025-03-31 16:24:36 -06:00
parent d8aabfba1b
commit ce4f4f705e
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
13 changed files with 266 additions and 88 deletions

View File

@ -371,7 +371,6 @@ export const useUpdateMetric = (params?: {
});
if (prevMetric && newMetric) {
console.log(newMetric.chart_config.yAxisScaleType);
queryClient.setQueryData(options.queryKey, newMetric);
}

View File

@ -0,0 +1,100 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Slider } from './Slider';
const meta: Meta<typeof Slider> = {
title: 'UI/Slider/Slider',
component: Slider,
tags: ['autodocs'],
args: {
defaultValue: [50],
min: 0,
max: 100,
step: 1,
showTooltip: true
},
argTypes: {
min: {
control: 'number',
description: 'Minimum value of the slider'
},
max: {
control: 'number',
description: 'Maximum value of the slider'
},
step: {
control: 'number',
description: 'Step size for slider increments'
},
defaultValue: {
control: 'object',
description: 'Default value of the slider (array of numbers)'
},
value: {
control: 'object',
description: 'Current value of the slider (array of numbers)'
},
showTooltip: {
control: 'boolean',
description: 'Whether to show tooltip while sliding or hovering'
},
disabled: {
control: 'boolean',
description: 'Whether the slider is disabled'
},
onValueChange: {
action: 'valueChanged',
description: 'Callback when value changes'
},
onValueCommit: {
action: 'valueCommitted',
description: 'Callback when value is committed'
}
}
};
export default meta;
type Story = StoryObj<typeof Slider>;
export const Default: Story = {
args: {
defaultValue: [50]
}
};
export const WithRange: Story = {
args: {
defaultValue: [25, 75]
}
};
export const WithCustomMinMax: Story = {
args: {
min: 1000,
max: 5000,
defaultValue: [2500]
}
};
export const WithCustomStep: Story = {
args: {
min: 0,
max: 100,
step: 10,
defaultValue: [40]
}
};
export const WithoutTooltip: Story = {
args: {
defaultValue: [50],
showTooltip: false
}
};
export const Disabled: Story = {
args: {
defaultValue: [50],
disabled: true
}
};

View File

@ -2,7 +2,12 @@
import * as React from 'react';
import * as SliderPrimitive from '@radix-ui/react-slider';
import { Tooltip } from '@/components/ui/tooltip';
import {
Tooltip,
TooltipProvider,
TooltipTrigger,
TooltipContent
} from '@/components/ui/tooltip/TooltipBase';
import { cn } from '@/lib/utils';
@ -28,42 +33,24 @@ const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, S
},
ref
) => {
const [isDragging, setIsDragging] = React.useState(false);
const [isHovered, setIsHovered] = React.useState(false);
const [useTooltip, setUseTooltip] = React.useState(false);
const [internalValues, setInternalValues] = React.useState(value || defaultValue || [min]);
const currentValue = value || defaultValue || [min];
const handleValueChange = React.useCallback(
(newValue: number[]) => {
setIsDragging(true);
onValueChange?.(newValue);
setInternalValues(newValue);
setUseTooltip(true);
},
[onValueChange]
);
const handleValueCommit = React.useCallback(() => {
setIsDragging(false);
setUseTooltip(false);
setInternalValues(currentValue);
}, []);
const renderThumb = (index: number) => {
const thumbValue = currentValue[index];
const shouldShowTooltip = showTooltip && (isDragging || isHovered);
return (
<Tooltip
key={index}
title={shouldShowTooltip ? String(thumbValue) : undefined}
open={shouldShowTooltip}
side="top"
sideOffset={5}>
<SliderPrimitive.Thumb
className="border-primary/50 bg-background focus-visible:ring-ring block h-4 w-4 rounded-full border shadow transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
/>
</Tooltip>
);
};
return (
<SliderPrimitive.Root
ref={ref}
@ -79,7 +66,21 @@ const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, S
<SliderPrimitive.Track className="bg-primary/20 relative h-1.5 w-full grow overflow-hidden rounded-full">
<SliderPrimitive.Range className="bg-primary absolute h-full" />
</SliderPrimitive.Track>
{Array.from({ length: currentValue.length }).map((_, index) => renderThumb(index))}
<TooltipProvider>
<Tooltip open={useTooltip}>
<TooltipTrigger asChild>
<SliderPrimitive.Thumb
onMouseEnter={() => setUseTooltip(true)}
onMouseLeave={() => setUseTooltip(false)}
className="border-primary bg-background block h-4 w-4 rounded-full border-2 shadow transition-all hover:scale-110 focus:outline-0 disabled:pointer-events-none disabled:opacity-50"
/>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={5}>
{internalValues[0]}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</SliderPrimitive.Root>
);
}

View File

@ -0,0 +1,63 @@
import type { Meta, StoryObj } from '@storybook/react';
import { SliderWithInputNumber } from './SliderWithInputNumber';
import { fn } from '@storybook/test';
const meta: Meta<typeof SliderWithInputNumber> = {
title: 'UI/Slider/SliderWithInputNumber',
component: SliderWithInputNumber,
tags: ['autodocs'],
args: {
min: 0,
max: 100,
value: 50,
onChange: fn()
},
argTypes: {
min: {
control: 'number',
description: 'Minimum value of the slider'
},
max: {
control: 'number',
description: 'Maximum value of the slider'
},
value: {
control: 'number',
description: 'Current value of the slider'
},
onChange: {
action: 'changed',
description: 'Callback when value changes'
}
}
};
export default meta;
type Story = StoryObj<typeof SliderWithInputNumber>;
export const Default: Story = {
args: {
value: 50
}
};
export const WithCustomMinMax: Story = {
args: {
min: 1000,
max: 5000,
value: 2500
}
};
export const WithLowValue: Story = {
args: {
value: 10
}
};
export const WithHighValue: Story = {
args: {
value: 90
}
};

View File

@ -0,0 +1,54 @@
import { InputNumber } from '../inputs';
import { Slider } from './Slider';
import React, { useMemo } from 'react';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
interface SliderWithInputNumberProps {
min: number;
max: number;
value: number;
onChange: (value: number) => void;
}
export const SliderWithInputNumber: React.FC<SliderWithInputNumberProps> = ({
min,
max,
value,
onChange
}) => {
const [internalValue, setInternalValue] = React.useState(value);
const onChangeSlider = useMemoizedFn((value: number[]) => {
setInternalValue(value[0]);
onChange(value[0]);
});
const onChangeInputNumber = useMemoizedFn((value: number) => {
setInternalValue(value);
onChange(value);
});
const styleOfInputNumber = useMemo(() => {
return { width: `${internalValue.toString().length * 17}px` };
}, [internalValue]);
return (
<div className="flex items-center space-x-3">
<InputNumber
className="min-w-[50px]"
style={styleOfInputNumber}
min={min}
max={max}
value={internalValue}
onChange={onChangeInputNumber}
/>
<Slider
className="w-full"
min={min}
max={max}
value={[internalValue]}
onValueChange={onChangeSlider}
/>
</div>
);
};

View File

@ -1 +1,2 @@
export { Slider } from './Slider';
export { SliderWithInputNumber } from './SliderWithInputNumber';

View File

@ -41,7 +41,6 @@ export const useUpdateMetricChart = (props?: { metricId?: string }) => {
...chartConfig
};
console.log(newChartConfig.yAxisScaleType);
onUpdateMetricDebounced({
id: metricId,
chart_config: newChartConfig

View File

@ -6,7 +6,7 @@ import {
MIN_DONUT_WIDTH
} from '@/api/asset_interfaces';
import { InputNumber } from '@/components/ui/inputs';
import { Slider } from '@/components/ui/slider';
import { Slider, SliderWithInputNumber } from '@/components/ui/slider';
import { useMemoizedFn } from '@/hooks';
import { AppSegmented, SegmentedItem } from '@/components/ui/segmented';
@ -71,22 +71,12 @@ export const EditPieAppearance = React.memo(
{showDonutWidthSelector && !hasMultipleYAxis && (
<LabelAndInput label="Donut width">
<div className="flex items-center space-x-3">
<InputNumber
className="max-w-[50px]"
min={DONUT_WIDTH_MIN}
max={DONUT_WIDTH_MAX}
value={value}
onChange={setPieDonutWidth}
/>
<Slider
className="w-full"
min={DONUT_WIDTH_MIN}
max={DONUT_WIDTH_MAX}
defaultValue={[value]}
onValueChange={onChangeSlider}
/>
</div>
<SliderWithInputNumber
min={DONUT_WIDTH_MIN}
max={DONUT_WIDTH_MAX}
value={value}
onChange={setPieDonutWidth}
/>
</LabelAndInput>
)}
</>

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { LabelAndInput } from '../Common';
import { InputNumber } from '@/components/ui/inputs';
import { Slider } from '@/components/ui/slider';
import { Slider, SliderWithInputNumber } from '@/components/ui/slider';
import { IBusterMetricChartConfig } from '@/api/asset_interfaces';
import { useMemoizedFn } from '@/hooks';
@ -26,23 +26,12 @@ export const EditPieMinimumSlicePercentage = React.memo(
return (
<LabelAndInput label="Minimum slice %">
<div className="flex flex-row items-center gap-2">
<InputNumber
min={0}
max={100}
placeholder="2.5"
className="max-w-[50px]"
value={pieMinimumSlicePercentage}
onChange={(value) => onChange(value || 0)}
/>
<Slider
className="w-full"
min={0}
max={100}
value={[pieMinimumSlicePercentage]}
onValueChange={onChangeSlider}
/>
</div>
<SliderWithInputNumber
min={0}
max={100}
value={pieMinimumSlicePercentage}
onChange={onChange}
/>
</LabelAndInput>
);
},

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { LabelAndInput } from '../../../Common/LabelAndInput';
import { InputNumber } from '@/components/ui/inputs';
import { Slider } from '@/components/ui/slider';
import { Slider, SliderWithInputNumber } from '@/components/ui/slider';
import { ColumnSettings } from '@/api/asset_interfaces/metric/charts';
import { useMemoizedFn } from '@/hooks';
@ -22,28 +22,14 @@ export const EditBarRoundness: React.FC<{
setValue(v || 0);
});
const onUpdateBarRoundnessSlider = useMemoizedFn((v: [number]) => {
if (v) onUpdateBarRoundness(v?.[0]);
});
return (
<LabelAndInput label="Bar roundness">
<div className="flex items-center space-x-3">
<InputNumber
className="max-w-[40px]"
min={BAR_ROUNDNESS_MIN}
max={BAR_ROUNDNESS_MAX}
value={value}
onChange={onUpdateBarRoundness}
/>
<Slider
className="w-full"
min={BAR_ROUNDNESS_MIN}
max={BAR_ROUNDNESS_MAX}
defaultValue={[value]}
onValueChange={onUpdateBarRoundnessSlider}
/>
</div>
<SliderWithInputNumber
min={BAR_ROUNDNESS_MIN}
max={BAR_ROUNDNESS_MAX}
value={value}
onChange={onUpdateBarRoundness}
/>
</LabelAndInput>
);
},

View File

@ -25,8 +25,6 @@ export const EditAxisLabelRotation: React.FC<{
onChangeLabelRotation(value.value as IBusterMetricChartConfig['xAxisLabelRotation']);
});
console.log(selectedOption, xAxisLabelRotation);
return (
<LabelAndInput label="Axis orientation">
<AppSegmented

View File

@ -14,7 +14,6 @@ export const EditAxisScale: React.FC<{
| IBusterMetricChartConfig['y2AxisScaleType'];
onChangeAxisScale: (value: IBusterMetricChartConfig['yAxisScaleType']) => void;
}> = React.memo(({ scaleType, onChangeAxisScale }) => {
console.log(scaleType);
return (
<LabelAndInput label="Scale">
<Select items={options} value={scaleType} onChange={onChangeAxisScale} />

View File

@ -24,7 +24,6 @@ export const EditShowAxisTitle: React.FC<{
const [show, setShow] = useState(showAxisTitle);
const onToggleAxisTitle = useMemoizedFn((show: boolean) => {
console.log(show);
setShow(show);
onChangeShowAxisTitle(show);