Skip to content
Open
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
176 changes: 176 additions & 0 deletions frui/src/field/RadioGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// types
import type { ChangeEvent, CSSProperties, MouseEvent, ReactNode } from 'react';
import type { InputConfig } from './Input';
import type { HTMLInputProps } from '../types';
// hooks
import { useState, useEffect } from 'react';
// utils
import { Children, isValidElement, cloneElement } from 'react';

export type RadioGroupProps = {
children: ReactNode;
name: string;
defaultValue?: string | number;
onChange?: (value: string | number) => void;
onUpdate?: (value: string | number | undefined, checked: boolean) => void;
orientation?: 'row' | 'column';
style?: CSSProperties;
className?: string;
color?: string;
error?: boolean;
disabled?: boolean;
};

export type RadioGroupItemProps = InputConfig & HTMLInputProps & {
label: string;
value: string | number;
color?: string;
error?: boolean;
disabled?: boolean;
onMouseOver?: (event: MouseEvent<HTMLInputElement>) => void;
onMouseOut?: (event: MouseEvent<HTMLInputElement>) => void;
onUpdate?: (value: string | number | undefined, checked: boolean) => void;
};

export function RadioGroupItem({
label,
value,
name,
checked,
defaultChecked,
onChange,
color,
error,
disabled,
onUpdate,
onMouseOver,
onMouseOut,
...rest
}: RadioGroupItemProps) {
const handleChange = () => {
if (disabled) return;
onChange?.({ target: { value } } as ChangeEvent<HTMLInputElement>);
onUpdate?.(value, true);
};

const inputStyle: CSSProperties = {
marginRight: '6px',
width: '18px',
height: '18px',
minWidth: '18px',
minHeight: '18px',
marginTop: '2px',
accentColor: error ? 'red' : color,
cursor: 'pointer',
};

const labelStyle: CSSProperties = {
color: error ? 'red' : 'inherit',
marginRight: '8px',
};

return (
<label
className="radio-option"
style={{
display: 'flex',
alignItems: 'center',
opacity: disabled ? 0.5 : 1,
}}
>
<input
{...rest}
type="radio"
name={name}
value={value}
checked={checked}
onChange={handleChange}
className="radio-input"
disabled={disabled}
style={inputStyle}
onMouseOver={(e) => onMouseOver?.(e)}
onMouseOut={(e) => onMouseOut?.(e)}
/>
<span className="radio-label" style={labelStyle}>{label}</span>
</label>
);
}

export default function RadioGroup({
children,
name,
defaultValue,
onChange,
onUpdate,
orientation = 'row',
style,
className,
color,
error,
disabled,
}: RadioGroupProps) {
const [selectedValue, setSelectedValue] = useState<string | number | undefined>(defaultValue);

// Set default selected value
useEffect(() => {
if (defaultValue !== undefined) {
setSelectedValue(defaultValue);
}
}, [defaultValue]);

// If no defaultValue, check for item with defaultChecked
useEffect(() => {
if (defaultValue !== undefined) return;

Children.forEach(children, (child) => {
if (
isValidElement<RadioGroupItemProps>(child) &&
child.props.defaultChecked
) {
setSelectedValue(child.props.value);
}
});
}, [children, defaultValue]);

const handleChange = (
newValue: string | number,
itemOnUpdate?: (value: string | number | undefined, checked: boolean) => void
) => {
if (disabled) return;
setSelectedValue(newValue);
onChange?.(newValue);
itemOnUpdate?.(newValue, true);
onUpdate?.(newValue, true);
};

return (
<div
className={['radio-group', className].filter(Boolean).join(' ')}
style={{
display: 'flex',
flexDirection: orientation,
gap: '8px',
...style,
opacity: disabled ? 0.5 : 1,
}}
>
<input type="hidden" name={name} value={selectedValue} />
{Children.map(children, (child) =>
isValidElement<RadioGroupItemProps>(child)
? cloneElement(child, {
name,
checked: selectedValue === child.props.value,
onChange: (e: ChangeEvent<HTMLInputElement>) =>
handleChange(
e.target.value,
child.props.onUpdate
),
color: child.props.color || color,
error: child.props.error ?? error,
disabled: disabled || child.props.disabled,
})
: child
)}
</div>
);
}
3 changes: 3 additions & 0 deletions web/modules/theme/layouts/components/MainMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ const MainMenu: React.FC<{
<Link href="/field/radio" className={`${pathname.indexOf('/field/radio') === 0 ? 'text-white' : ''} block pl-7 pr-3 py-2 cursor-pointer`}>
<span className="inline-block pl-2">{_('Radio')}</span>
</Link>
<Link href="/field/radiogroup" className={`${pathname.indexOf('/field/radiogroup') === 0 ? 'text-white' : ''} block pl-7 pr-3 py-2 cursor-pointer`}>
<span className="inline-block pl-2">{_('Radio Group')}</span>
</Link>
<Link href="/field/select" className={`${pathname.indexOf('/field/select') === 0 ? 'text-white' : ''} block pl-7 pr-3 py-2 cursor-pointer`}>
<span className="inline-block pl-2">{_('Select')}</span>
</Link>
Expand Down
28 changes: 18 additions & 10 deletions web/pages/field/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import Metadata from 'frui/field/Metadata';
import Number from 'frui/field/Number';
import Password from 'frui/field/Password';
import Radio from 'frui/field/Radio';
import RadioGroup, { RadioGroupItem } from 'frui/field/RadioGroup'
import Select from 'frui/field/Select';
import Slug from 'frui/field/Slug';
import Switch from 'frui/field/Switch';
Expand Down Expand Up @@ -334,6 +335,23 @@ export default function Home() {
</h2>
</div>
</div>
<div
className="block basis-full sm:basis-1/2 md:basis-1/3 text-center cursor-pointer"
onClick={() => router.push('/field/radiogroup')}
>
<div className="m-2 border border-b2 rounded overflow-hidden">
<div className="flex items-center justify-center h-[130px] w-full bg-b1 px-3">
<RadioGroup name="myRadioGroup" orientation="row">
<RadioGroupItem label="Yes" value="yes" />
<RadioGroupItem label="No" value="no" />
<RadioGroupItem label="Maybe" value="maybe" />
</RadioGroup>
</div>
<h2 className="my-2 font-semibold text-center uppercase">
{_('Radio Group')}
</h2>
</div>
</div>
<div
className="block basis-full sm:basis-1/2 md:basis-1/3 text-center cursor-pointer"
onClick={() => router.push('/field/select')}
Expand Down Expand Up @@ -477,16 +495,6 @@ export default function Home() {
</h2>
</div>
</div>
<div className="block basis-full sm:basis-1/2 md:basis-1/3 text-center cursor-pointer">
<div className="m-2 border border-b2 rounded overflow-hidden">
<div className="flex items-center justify-center h-[130px] w-full bg-b1 px-3">
Unlocks at 9,000 downloads
</div>
<h2 className="my-2 font-semibold text-center uppercase">
{_('Radio Group')}
</h2>
</div>
</div>
<div className="block basis-full sm:basis-1/2 md:basis-1/3 text-center cursor-pointer">
<div className="m-2 border border-b2 rounded overflow-hidden">
<div className="flex items-center justify-center h-[130px] w-full bg-b1 px-3">
Expand Down
4 changes: 2 additions & 2 deletions web/pages/field/radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,8 @@ export default function Home() {
{_('Password')}
</Link>
<div className="flex-grow"></div>
<Link className="text-t2" href="/field/select">
{_('Select')}
<Link className="text-t2" href="/field/radiogroup">
{_('Radio Group')}
<i className="fas fa-arrow-right ml-2"></i>
</Link>
</div>
Expand Down
Loading
Loading