diff --git a/src/components/Form/NumberField/NumberField.spec.tsx b/src/components/Form/NumberField/NumberField.spec.tsx new file mode 100644 index 0000000..0edc8b4 --- /dev/null +++ b/src/components/Form/NumberField/NumberField.spec.tsx @@ -0,0 +1,56 @@ +import { useState } from 'react'; + +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { NumberField } from './NumberField.tsx'; + +const TestNumberFieldRadio = ({ options }: { options: { [key: number]: string } }) => { + const [error, setError] = useState(); + const [value, setValue] = useState(); + return ( + + ); +}; + +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' }; + +describe('NumberField', () => { + describe('radio variant', () => { + it('should render one radio per option', () => { + render(); + expect(screen.getByRole('radiogroup')).toBeInTheDocument(); + expect(screen.getAllByRole('radio')).toHaveLength(5); + }); + it('should stack two options vertically', () => { + render(); + const radioGroup = screen.getByTestId('radio-group'); + expect(radioGroup).not.toHaveClass('justify-between'); + expect(radioGroup.style.gridTemplateColumns).toBe(''); + }); + it('should stack three options vertically', () => { + render(); + const radioGroup = screen.getByTestId('radio-group'); + expect(radioGroup).not.toHaveClass('justify-between'); + expect(radioGroup.style.gridTemplateColumns).toBe(''); + }); + it('should spread four or more options horizontally', () => { + render(); + const radioGroup = screen.getByTestId('radio-group'); + expect(radioGroup).toHaveClass('justify-between'); + expect(radioGroup.style.gridTemplateColumns).toBe('repeat(5, auto)'); + }); + }); +}); diff --git a/src/components/Form/NumberField/NumberField.stories.tsx b/src/components/Form/NumberField/NumberField.stories.tsx index 4374a77..8513d0a 100644 --- a/src/components/Form/NumberField/NumberField.stories.tsx +++ b/src/components/Form/NumberField/NumberField.stories.tsx @@ -75,6 +75,55 @@ export const Radio: Story = { ] }; +export const RadioTwoOptions: Story = { + decorators: [ + (Story) => { + const [value, setValue] = useState(); + return ( + + ); + } + ] +}; + +export const RadioThreeOptions: Story = { + decorators: [ + (Story) => { + const [value, setValue] = useState(); + return ( + + ); + } + ] +}; + export const Select: Story = { decorators: [ (Story) => { diff --git a/src/components/Form/NumberField/NumberFieldRadio.tsx b/src/components/Form/NumberField/NumberFieldRadio.tsx index ddba5dc..fa48eec 100644 --- a/src/components/Form/NumberField/NumberFieldRadio.tsx +++ b/src/components/Form/NumberField/NumberFieldRadio.tsx @@ -4,11 +4,20 @@ import type { NumberFormField } from '@douglasneuroinformatics/libui-form-types' import type { Simplify } from 'type-fest'; import { Label, RadioGroup } from '#components'; +import { cn } from '#utils'; import { FieldGroup } from '../FieldGroup/FieldGroup.tsx'; import type { BaseFieldComponentProps } from '../types.ts'; +/** + * The minimum number of options required to render the group horizontally. Below this, options are + * stacked vertically, matching the other radio field variants, since spreading two or three options + * across the full width of a wide container pushes them far apart and makes the later options easy + * to miss. + */ +const MIN_OPTIONS_FOR_HORIZONTAL_LAYOUT = 4; + export type NumberFieldRadioProps = Simplify< BaseFieldComponentProps & Extract >; @@ -26,23 +35,26 @@ export const NumberFieldRadio = ({ value }: NumberFieldRadioProps) => { const radioGroupRef = useRef(null); - const [isColumnLayout, setIsColumnLayout] = useState(false); + const [isOverflowing, setIsOverflowing] = useState(false); const optionsCount = Object.keys(options).length; + const canUseHorizontalLayout = optionsCount >= MIN_OPTIONS_FOR_HORIZONTAL_LAYOUT; + const isColumnLayout = !canUseHorizontalLayout || isOverflowing; useEffect(() => { + if (!canUseHorizontalLayout || !radioGroupRef.current) { + return; + } const observer = new ResizeObserver(([entry]) => { const { width: rootWidth } = entry!.target.getBoundingClientRect(); const children = Array.from(entry!.target.children); const totalChildWidth = children.reduce((sum, child) => sum + child.scrollWidth, 0); - const isOverflowing = totalChildWidth > rootWidth - children.length * 24; // to additional provide spacing between items - setIsColumnLayout(isOverflowing); + // subtract an allowance so that items always have some spacing between them + setIsOverflowing(totalChildWidth > rootWidth - children.length * 24); }); - if (radioGroupRef.current) { - observer.observe(radioGroupRef.current); - } + observer.observe(radioGroupRef.current); return () => observer.disconnect(); - }, []); + }, [canUseHorizontalLayout]); return ( @@ -51,12 +63,10 @@ export const NumberFieldRadio = ({ setValue(parseInt(value, 10))} > diff --git a/src/components/RadioGroup/RadioGroupItem.tsx b/src/components/RadioGroup/RadioGroupItem.tsx index e2285a0..86691a5 100644 --- a/src/components/RadioGroup/RadioGroupItem.tsx +++ b/src/components/RadioGroup/RadioGroupItem.tsx @@ -7,7 +7,7 @@ export const RadioGroupItem: React.FC> = ({ cl return (