Skip to content
Merged
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
23 changes: 1 addition & 22 deletions src/components/Form/BaseRadioField.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { cva } from 'class-variance-authority';
import type { Simplify } from 'type-fest';

import { Label } from '../Label/Label.tsx';
Expand All @@ -7,25 +6,12 @@ import { FieldGroup } from './FieldGroup/FieldGroup.tsx';

import type { BaseFieldComponentProps } from './types.ts';

const baseRadioFieldVariants = cva('flex', {
defaultVariants: {
orientation: 'vertical'
},
variants: {
orientation: {
horizontal: 'flex-col @3xl:flex-row @3xl:items-center @3xl:justify-between',
vertical: 'flex-col'
}
}
});

export type BaseRadioFieldProps<T extends string> = Simplify<
BaseFieldComponentProps<T> & {
description?: string;
disabled?: boolean;
label: string;
options: { [K in T]: string };
orientation?: 'horizontal' | 'vertical';
}
>;

Expand All @@ -36,24 +22,17 @@ export const BaseRadioField = <T extends string>({
label,
name,
options,
orientation = 'vertical',
readOnly,
setValue,
value
}: BaseRadioFieldProps<T>) => {
const optionsCount = Object.keys(options).length;
return (
<FieldGroup name={name}>
<FieldGroup.Row>
<Label>{label}</Label>
<FieldGroup.Description description={description} />
</FieldGroup.Row>
<RadioGroup
className={baseRadioFieldVariants({ orientation: optionsCount > 5 ? 'vertical' : orientation })}
name={name}
value={value ?? ''}
onValueChange={(value) => setValue(value as T)}
>
<RadioGroup name={name} value={value ?? ''} onValueChange={(value) => setValue(value as T)}>
{Object.keys(options).map((option) => (
<div className="flex items-center gap-2" key={option}>
<RadioGroup.Item disabled={disabled || readOnly} id={`${name}-${option}`} value={option} />
Expand Down
39 changes: 29 additions & 10 deletions src/components/Form/NumberField/NumberFieldRadio.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect, useRef, useState } from 'react';

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';

Expand All @@ -24,37 +25,55 @@ export const NumberFieldRadio = ({
setValue,
value
}: NumberFieldRadioProps) => {
const radioGroupRef = useRef<HTMLDivElement>(null);
const [isColumnLayout, setIsColumnLayout] = useState<boolean>(false);

const optionsCount = Object.keys(options).length;

useEffect(() => {
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);
});
if (radioGroupRef.current) {
observer.observe(radioGroupRef.current);
}
return () => observer.disconnect();
}, []);

return (
<FieldGroup name={name}>
<FieldGroup.Row>
<Label>{label}</Label>
<FieldGroup.Description description={description} />
</FieldGroup.Row>
<RadioGroup
className={cn(
'flex',
optionsCount > 5 ? 'flex-col' : 'flex-col @3xl:flex-row @3xl:items-center @3xl:justify-between'
)}
className="grid justify-between"
name={name}
ref={radioGroupRef}
style={{
gridTemplateColumns: isColumnLayout ? 'repeat(1, 1fr)' : `repeat(${optionsCount}, auto)`
}}
value={value?.toString() ?? ''}
onValueChange={(value) => setValue(parseInt(value))}
onValueChange={(value) => setValue(parseInt(value, 10))}
>
{Object.keys(options)
.map((val) => parseInt(val))
.toSorted((a, b) => a - b)
.map((val) => {
const text = (disableAutoPrefix ? '' : `${val} - `) + options[val];
return (
<div className="flex items-center gap-2" key={val}>
<div className="flex w-fit items-center gap-2" key={val}>
<RadioGroup.Item disabled={disabled || readOnly} id={`${name}-${val}`} value={val.toString()} />
<Label
aria-disabled={disabled || readOnly}
className="text-muted-foreground font-normal"
className="text-muted-foreground flex items-center font-normal"
htmlFor={`${name}-${val}`}
>
{text}
{!disableAutoPrefix && <span className="whitespace-nowrap">{val}&nbsp;-&nbsp;</span>}
<span>{options[val]}</span>
</Label>
</div>
);
Expand Down
15 changes: 5 additions & 10 deletions src/components/RadioGroup/RadioGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import * as React from 'react';

import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import { Root } from '@radix-ui/react-radio-group';

import { cn } from '#utils';

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

type RadioGroupProps = React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>;
type RadioGroupProps = React.ComponentProps<typeof Root>;

const RadioGroupRoot = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(function RadioGroup({ className, ...props }, ref) {
return (
<RadioGroupPrimitive.Root className={cn('grid gap-2', className)} data-testid="radio-group" {...props} ref={ref} />
);
});
const RadioGroupRoot: React.FC<RadioGroupProps> = ({ className, ...props }) => {
return <Root className={cn('grid gap-2', className)} data-testid="radio-group" {...props} />;
};

export const RadioGroup = Object.assign(RadioGroupRoot, {
Item: RadioGroupItem
Expand Down
20 changes: 7 additions & 13 deletions src/components/RadioGroup/RadioGroupItem.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
import { forwardRef } from 'react';

import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import { Indicator, Item } from '@radix-ui/react-radio-group';
import { CircleIcon } from 'lucide-react';

import { cn } from '#utils';

export const RadioGroupItem = forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(function RadioGroupItem({ className, ...props }, ref) {
export const RadioGroupItem: React.FC<React.ComponentProps<typeof Item>> = ({ className, ...props }) => {
return (
<RadioGroupPrimitive.Item
<Item
className={cn(
'border-primary text-primary focus-visible:ring-ring flex aspect-square h-4 w-4 items-center justify-center rounded-full border shadow-sm focus:outline-hidden focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
>
<RadioGroupPrimitive.Indicator asChild>
<Indicator asChild>
<CircleIcon
className="fill-current text-current"
style={{ height: '0.625rem', strokeWidth: '2px', width: '0.625rem' }}
/>
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
</Indicator>
</Item>
);
});
};