Skip to content

Commit

Permalink
OPHJOD-292: Add Slider component
Browse files Browse the repository at this point in the history
  • Loading branch information
ketsappi committed Apr 29, 2024
1 parent 5efd6b8 commit a0e851f
Show file tree
Hide file tree
Showing 5 changed files with 1,134 additions and 0 deletions.
50 changes: 50 additions & 0 deletions lib/components/Slider/Slider.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';

import { Slider } from './Slider';
import { fn } from '@storybook/test';

const meta = {
title: 'Slider',
component: Slider,
tags: ['autodocs'],
} satisfies Meta<typeof Slider>;

export default meta;

type Story = StoryObj<typeof meta>;

const render = (args: Story['args']) => {
const { value, onValueChange, ...rest } = args;
const [numberValue, setNumberValue] = useState(value);
return (
<Slider
value={numberValue}
onValueChange={(newValue) => {
setNumberValue(newValue);
onValueChange(newValue);
}}
{...rest}
/>
);
};

const parameters = {
design: {
type: 'figma',
url: 'https://www.figma.com/file/6M2LrpSCcB0thlFDaQAI2J/cx_jod_client?type=design&node-id=542-7071',
},
};

export const Default: Story = {
render,
parameters: {
...parameters,
},
args: {
label: 'Osaamiset',
icon: 'school',
onValueChange: fn(),
value: 0,
},
};
40 changes: 40 additions & 0 deletions lib/components/Slider/Slider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { screen, render, waitFor } from '@testing-library/react';
import { Slider } from './Slider';
import userEvent from '@testing-library/user-event';

const { ResizeObserver } = window;

beforeEach(() => {
//@ts-expect-error Without this, the following error is introduced: "TypeError: win.ResizeObserver is not a constructor"
delete window.ResizeObserver;
window.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
});

afterEach(() => {
window.ResizeObserver = ResizeObserver;
vi.restoreAllMocks();
});

describe('Slider', () => {
it('should call onValueChange when value changes', async () => {
const onValueChangeMock = vi.fn();
const user = userEvent.setup();

render(<Slider label="Target" icon="target" onValueChange={onValueChangeMock} />);

Check failure on line 28 in lib/components/Slider/Slider.test.tsx

View workflow job for this annotation

GitHub Actions / Build

Property 'value' is missing in type '{ label: string; icon: string; onValueChange: Mock<any, any>; }' but required in type 'SliderProps'.

const [sliderThumb] = screen.getAllByRole('slider', { hidden: true });
sliderThumb.focus();

await user.keyboard('[ArrowRight]');
expect(sliderThumb).toHaveAttribute('aria-valuenow', '1');
await user.keyboard('[ArrowLeft]');
expect(sliderThumb).toHaveAttribute('aria-valuenow', '0');

await waitFor(() => expect(onValueChangeMock).toHaveBeenCalledTimes(2));
});
});
133 changes: 133 additions & 0 deletions lib/components/Slider/Slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { useRef, useState } from 'react';
import {
Slider as ArkSlider,
SliderValueChangeDetails as ArkValueChangeDetails,
SliderFocusChangeDetails as ArkFocusChangeDetails,
} from '@ark-ui/react';
import {
arrow,
autoUpdate,
offset,
flip,
FloatingArrow,
shift,
useDismiss,
useFloating,
useFocus,
useHover,
useInteractions,
useRole,
} from '@floating-ui/react';

const ARROW_HEIGHT = 12;

export interface SliderProps {
/** Label for tooltip */
label: string;
/** Icon shown on the button */
icon: string;
/** On slider value change */
onValueChange: (value: number) => void;
/** Current value of the slider */
value: number;
}

const getThumbClassName = (focused: boolean, value: number) => {
return focused ? `${value === 0 ? 'bg-[#DB2C35]' : 'bg-[#25A249]'} ` : 'bg-secondary-gray';
};

const getThumbBackgroundColor = (focused: boolean, value: number) => {
const lightColorSaturation = 41;
const lightColorLightness = 85;
const darkColorSaturation = 63;
const darkColorLightness = 39;

const saturation = lightColorSaturation + ((darkColorSaturation - lightColorSaturation) * value) / 100;
const lightness = lightColorLightness + ((darkColorLightness - lightColorLightness) * value) / 100;

return focused && value !== 0 ? `hsl(137 ${saturation}% ${lightness}%)` : undefined;
};

export const Slider = ({ label, icon, onValueChange, value }: SliderProps) => {
const [focused, setFocused] = useState(false);
const arrowRef = useRef(null);
const { refs, floatingStyles, context } = useFloating({
open: focused,
middleware: [
offset(ARROW_HEIGHT + 8),
flip(),
shift(),

arrow({
element: arrowRef,
}),
],
whileElementsMounted: autoUpdate,
});

const hover = useHover(context, { move: false });
const focus = useFocus(context);
const dismiss = useDismiss(context);
const role = useRole(context, {
role: 'tooltip',
});

const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss, role]);

const onValueChangeHandler = (details: ArkValueChangeDetails) => {
onValueChange(details.value[0]);
};

const onFocusChangeHandler = (details: ArkFocusChangeDetails) => {
setFocused(details.focusedIndex === 0);
};

const thumbClassName = getThumbClassName(focused, value);
const thumbColorBackgroundColor = getThumbBackgroundColor(focused, value);

return (
<div className="flex h-[63px] w-[348px] rounded-[100px] bg-bg-gray">
<span
className={`m-2 size-[55px] rounded-full ${focused ? 'bg-accent' : 'bg-white'} flex items-center justify-center ${focused ? 'text-white' : 'text-primary-gray'} material-symbols-outlined select-none`}
aria-hidden
>
<span className="flex items-center justify-center text-[37px]">{icon}</span>
</span>
<ArkSlider.Root
className="ml-3 mr-5 flex grow flex-col justify-center"
onValueChange={onValueChangeHandler}
onFocusChange={onFocusChangeHandler}
>
<ArkSlider.Control className="flex">
<ArkSlider.Track className="flex h-[3px] grow bg-white">
<ArkSlider.Range className="" />
</ArkSlider.Track>
<ArkSlider.Thumb
ref={refs.setReference}
{...getReferenceProps()}
index={0}
style={thumbColorBackgroundColor ? { backgroundColor: thumbColorBackgroundColor } : undefined}
className={`absolute -top-[11px] flex size-[24px] justify-center rounded-full border-[3px] border-white ${thumbClassName}`.trim()}
/>
</ArkSlider.Control>
</ArkSlider.Root>
{focused && (
<div
ref={refs.setFloating}
className="max-w-[292px] rounded-[20px] bg-primary-gray px-6 py-5 text-button-md text-white sm:text-body-md"
style={floatingStyles}
{...getFloatingProps()}
>
{label} {value}%
<FloatingArrow
ref={arrowRef}
context={context}
className="fill-primary-gray"
width={ARROW_HEIGHT * 2}
height={ARROW_HEIGHT}
/>
</div>
)}
</div>
);
};
Loading

0 comments on commit a0e851f

Please sign in to comment.