Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/components/Form/Form.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,35 @@
]
};

export const WithNumberRecordInput: StoryObj<
typeof Form<z.ZodType<{ dimensions: { depth: number; height: number; width: number } }>>
> = {
args: {
content: {

Check failure on line 543 in src/components/Form/Form.stories.tsx

View workflow job for this annotation

GitHub Actions / ci

Type '{ dimensions: { items: { depth: { label: string; }; height: { description: string; label: string; }; width: { label: string; }; }; kind: "number-record"; label: string; variant: "input"; }; }' is not assignable to type 'Content<{ dimensions: { depth: number; height: number; width: number; }; }> | undefined'.
dimensions: {
items: {
depth: { label: 'Depth (cm)' },
height: { description: 'Measure from the floor', label: 'Height (cm)' },
width: { label: 'Width (cm)' }
},
kind: 'number-record',
label: 'Dimensions',
variant: 'input'
}
},
validationSchema: z.object({
dimensions: z.object({
depth: z.number().positive(),
height: z.number().positive(),
width: z.number().positive()
})
}),
onSubmit: (data) => {
alert(JSON.stringify(data, null, 2));
}
}
};

export const WithResetButton: StoryObj<typeof Form> = {
args: {
content: {
Expand Down
50 changes: 50 additions & 0 deletions src/components/Form/Form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -399,4 +399,54 @@
expect(onSubmit).toHaveBeenCalledWith({ symptoms: new Set(['none']) });
});
});

describe('number record with the input variant', () => {
const schema = z.object({ dimensions: z.object({ height: z.number('Required'), width: z.number('Required') }) });

beforeEach(() => {
render(
<Form
content={{
dimensions: {
items: { height: { label: 'Height' }, width: { label: 'Width' } },
kind: 'number-record',
label: 'Dimensions',
variant: 'input'

Check failure on line 414 in src/components/Form/Form.test.tsx

View workflow job for this annotation

GitHub Actions / ci

Type '"input"' is not assignable to type '"likert"'.
}
}}
data-testid={testid}
validationSchema={schema}
onError={onError}
onSubmit={onSubmit}
/>
);
});

afterEach(() => {
vi.clearAllMocks();
});

it('should render a number input for each item, and no radio buttons', () => {
expect(screen.getByLabelText<HTMLInputElement>('Height').type).toBe('text');
expect(screen.getByLabelText('Width')).toBeInTheDocument();
expect(screen.queryAllByRole('radio')).toHaveLength(0);
});

it('should submit the number entered for each item', async () => {
await userEvent.type(screen.getByLabelText('Height'), '12.5');
await userEvent.type(screen.getByLabelText('Width'), '30');
fireEvent.submit(screen.getByTestId(testid));
await waitFor(() => expect(onSubmit).toHaveBeenCalledOnce());
expect(onSubmit.mock.lastCall?.[0]).toStrictEqual({ dimensions: { height: 12.5, width: 30 } });
});

it('should show the error of an item below that item', async () => {
await userEvent.type(screen.getByLabelText('Height'), '12');
fireEvent.submit(screen.getByTestId(testid));
await waitFor(() => expect(onError).toHaveBeenCalledOnce());
const errors = screen.getAllByTestId('error-message-text');
expect(errors.map((e) => e.innerHTML)).toMatchObject(['Required']);
expect(errors[0]!.closest('[data-field-group]')).toHaveAttribute('data-field-group', 'width');
});
});
});
113 changes: 112 additions & 1 deletion src/components/Form/NumberField/NumberField.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useState } from 'react';

import { render, screen } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import { NumberField } from './NumberField.tsx';

import type { NumberFieldSliderProps } from './NumberFieldSlider.tsx';

const TestNumberFieldRadio = ({ options }: { options: { [key: number]: string } }) => {
const [error, setError] = useState<string[] | undefined>();
const [value, setValue] = useState<number | undefined>();
Expand All @@ -23,6 +25,33 @@
);
};

const TestNumberFieldSlider = (
props: Partial<
Pick<NumberFieldSliderProps, 'defaultValue' | 'disabled' | 'marks' | 'max' | 'min' | 'readOnly' | 'showRange'>

Check failure on line 30 in src/components/Form/NumberField/NumberField.spec.tsx

View workflow job for this annotation

GitHub Actions / ci

Type '"disabled" | "max" | "min" | "defaultValue" | "readOnly" | "marks" | "showRange"' does not satisfy the constraint '"max" | "min" | "variant" | keyof BaseField | keyof BaseFieldComponentProps<number>'.
>
) => {
const [value, setValue] = useState<number | undefined>();
return (
<>
<NumberField
kind="number"
label="number-field"
max={10}
min={0}
name="number-field"
setError={() => undefined}
setValue={setValue}
value={value}
variant="slider"
{...props}
/>
<button type="button" onClick={() => setValue(undefined)}>
clear
</button>
</>
);
};

const TWO_OPTIONS = { 0: 'The first in the morning', 1: 'Any other' };
const THREE_OPTIONS = { 1: 'Never', 2: 'Sometimes', 3: 'Always' };
const FIVE_OPTIONS = { 1: 'Very Low', 2: 'Low', 3: 'Medium', 4: 'High', 5: 'Very High' };
Expand Down Expand Up @@ -53,4 +82,86 @@
expect(radioGroup.style.gridTemplateColumns).toBe('repeat(5, auto)');
});
});

describe('slider variant', () => {
const getThumbPosition = () => screen.getByRole('slider').getAttribute('aria-valuenow');
const getDisplayedValue = () => screen.getByTestId('slider-value').textContent;

it('should render no annotations by default', () => {
render(<TestNumberFieldSlider />);
expect(screen.queryByTestId('slider-annotations')).not.toBeInTheDocument();
});

it('should display a dimmed thumb at the midpoint when there is no value', () => {
render(<TestNumberFieldSlider max={20} min={10} />);
expect(getThumbPosition()).toBe('15');
expect(getDisplayedValue()).toBe('NA');
expect(screen.getByTestId('slider-thumb').closest('[data-unset]')).toBeInTheDocument();
});

it('should select the midpoint when the user presses the slider without a value', () => {
render(<TestNumberFieldSlider />);
// happy-dom does not implement pointer capture, which the slider uses when it is pressed
screen.getByTestId('slider-thumb').setPointerCapture = () => undefined;
fireEvent.pointerDown(screen.getByTestId('slider-thumb'));
expect(getDisplayedValue()).toBe('5');
expect(screen.getByTestId('slider-thumb').closest('[data-unset]')).not.toBeInTheDocument();
});

it.each(['disabled', 'readOnly'] as const)(
'should not select a value when the slider is pressed and is %s',
(prop) => {
render(<TestNumberFieldSlider {...{ [prop]: true }} />);
screen.getByTestId('slider-thumb').setPointerCapture = () => undefined;
fireEvent.pointerDown(screen.getByTestId('slider-thumb'));
expect(getDisplayedValue()).toBe('NA');
}
);

it('should select a value with the keyboard when there is no value', () => {
render(<TestNumberFieldSlider />);
fireEvent.keyDown(screen.getByRole('slider'), { key: 'ArrowRight' });
expect(getDisplayedValue()).toBe('6');
});

it('should set the default value as a real value that is visible', () => {
render(<TestNumberFieldSlider defaultValue={3} />);
expect(getDisplayedValue()).toBe('3');
expect(getThumbPosition()).toBe('3');
expect(screen.getByTestId('slider-thumb').closest('[data-unset]')).not.toBeInTheDocument();
});

it('should not set the default value when the slider is read-only', () => {
render(<TestNumberFieldSlider readOnly defaultValue={3} />);
expect(getDisplayedValue()).toBe('NA');
});

it('should set the default value again when the value is cleared', () => {
render(<TestNumberFieldSlider defaultValue={3} />);
fireEvent.keyDown(screen.getByRole('slider'), { key: 'ArrowRight' });
expect(getDisplayedValue()).toBe('4');
fireEvent.click(screen.getByText('clear'));
expect(getDisplayedValue()).toBe('3');
});

it('should display the minimum and maximum when showRange is set', () => {
render(<TestNumberFieldSlider showRange max={20} min={10} />);
expect(screen.getByTestId('slider-range')).toHaveTextContent('1020');
expect(screen.queryByTestId('slider-marks')).not.toBeInTheDocument();
});

it('should display the marks in order at the position of their value', () => {
render(<TestNumberFieldSlider marks={{ 0: 'Not at all', 5: 'Moderately', 10: 'Extremely' }} />);
const marks = [...screen.getByTestId('slider-marks').querySelectorAll<HTMLElement>('[data-mark-value]')];
expect(marks.map((mark) => mark.textContent)).toStrictEqual(['Not at all', 'Moderately', 'Extremely']);
expect(marks.map((mark) => mark.style.marginLeft)).toStrictEqual(['0%', '50%', '100%']);
expect(screen.queryByTestId('slider-range')).not.toBeInTheDocument();
});

it('should not display a mark that is outside of the range', () => {
render(<TestNumberFieldSlider marks={{ 5: 'Moderately', 11: 'Too high' }} />);
expect(screen.getByTestId('slider-marks')).toHaveTextContent('Moderately');
expect(screen.queryByText('Too high')).not.toBeInTheDocument();
});
});
});
96 changes: 96 additions & 0 deletions src/components/Form/NumberField/NumberField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
args={{
description: 'Lorem ipsum dolor sit amet consectetur adipisicing elit.',
label: 'Slider Number Field',
max: 10,
min: 0,
name: 'number-slider',
setValue,
value,
Expand All @@ -48,6 +50,100 @@
]
};

export const SliderWithRange: Story = {
decorators: [
(Story) => {
const [value, setValue] = useState<number | undefined>();
return (
<Story
args={{
description: 'Lorem ipsum dolor sit amet consectetur adipisicing elit.',
label: 'Slider Number Field',
max: 10,
min: 0,
name: 'number-slider',
setValue,
showRange: true,

Check failure on line 66 in src/components/Form/NumberField/NumberField.stories.tsx

View workflow job for this annotation

GitHub Actions / ci

Object literal may only specify known properties, and 'showRange' does not exist in type 'Partial<{ error?: string[] | undefined; name: string; readOnly?: boolean | undefined; setError: (error: string[]) => void; setValue: (value: number | undefined) => void; value: number | undefined; description?: string | undefined; disabled?: boolean | undefined; kind: "number"; label: string; max: number; min: number; variant: "slider"; }>'.
value,
variant: 'slider'
}}
/>
);
}
]
};

export const SliderWithMarks: Story = {
decorators: [
(Story) => {
const [value, setValue] = useState<number | undefined>();
return (
<Story
args={{
description: 'Lorem ipsum dolor sit amet consectetur adipisicing elit.',
label: 'Slider Number Field',
marks: { 0: 'Not at all', 5: 'Moderately', 10: 'Extremely' },

Check failure on line 85 in src/components/Form/NumberField/NumberField.stories.tsx

View workflow job for this annotation

GitHub Actions / ci

Object literal may only specify known properties, and 'marks' does not exist in type 'Partial<{ error?: string[] | undefined; name: string; readOnly?: boolean | undefined; setError: (error: string[]) => void; setValue: (value: number | undefined) => void; value: number | undefined; description?: string | undefined; disabled?: boolean | undefined; kind: "number"; label: string; max: number; min: number; variant: "slider"; }>'.
max: 10,
min: 0,
name: 'number-slider',
setValue,
value,
variant: 'slider'
}}
/>
);
}
]
};

export const SliderWithRangeAndMarks: Story = {
decorators: [
(Story) => {
const [value, setValue] = useState<number | undefined>();
return (
<Story
args={{
description: 'Lorem ipsum dolor sit amet consectetur adipisicing elit.',
label: 'Slider Number Field',
marks: { 0: 'No pain', 10: 'The worst pain that you can imagine' },

Check failure on line 108 in src/components/Form/NumberField/NumberField.stories.tsx

View workflow job for this annotation

GitHub Actions / ci

Object literal may only specify known properties, and 'marks' does not exist in type 'Partial<{ error?: string[] | undefined; name: string; readOnly?: boolean | undefined; setError: (error: string[]) => void; setValue: (value: number | undefined) => void; value: number | undefined; description?: string | undefined; disabled?: boolean | undefined; kind: "number"; label: string; max: number; min: number; variant: "slider"; }>'.
max: 10,
min: 0,
name: 'number-slider',
setValue,
showRange: true,
value,
variant: 'slider'
}}
/>
);
}
]
};

export const SliderWithDefaultValue: Story = {
decorators: [
(Story) => {
const [value, setValue] = useState<number | undefined>();
return (
<Story
args={{
defaultValue: 3,

Check failure on line 130 in src/components/Form/NumberField/NumberField.stories.tsx

View workflow job for this annotation

GitHub Actions / ci

Object literal may only specify known properties, and 'defaultValue' does not exist in type 'Partial<{ error?: string[] | undefined; name: string; readOnly?: boolean | undefined; setError: (error: string[]) => void; setValue: (value: number | undefined) => void; value: number | undefined; description?: string | undefined; disabled?: boolean | undefined; kind: "number"; label: string; max: number; min: number; variant: "slider"; }>'.
description: 'Lorem ipsum dolor sit amet consectetur adipisicing elit.',
label: 'Slider Number Field',
max: 10,
min: 0,
name: 'number-slider',
setValue,
showRange: true,
value,
variant: 'slider'
}}
/>
);
}
]
};

export const Radio: Story = {
decorators: [
(Story) => {
Expand Down
Loading
Loading