Files
lti-web-client/src/components/input/RadioInput.tsx
T

213 lines
4.5 KiB
TypeScript

'use client';
import {
ChangeEventHandler,
ReactNode,
createContext,
useContext,
} from 'react';
import { cn } from '@/lib/helper';
export interface RadioOption {
label: string;
value: string;
}
// DaisyUI Radio Colors
export type RadioColor =
| 'neutral'
| 'primary'
| 'secondary'
| 'accent'
| 'success'
| 'warning'
| 'info'
| 'error';
// DaisyUI Radio Sizes
export type RadioSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
// Context untuk RadioGroup
interface RadioGroupContextValue {
name: string;
value?: string;
color?: RadioColor;
size?: RadioSize;
disabled?: boolean;
onChange?: ChangeEventHandler<HTMLInputElement>;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
}
const RadioGroupContext = createContext<RadioGroupContextValue | undefined>(
undefined
);
const useRadioGroup = () => {
const context = useContext(RadioGroupContext);
if (!context) {
throw new Error('RadioGroupItem must be used within RadioGroup');
}
return context;
};
// RadioGroup Component
export interface RadioGroupProps {
label?: string;
bottomLabel?: string;
name: string;
value?: string;
options?: RadioOption[];
color?: RadioColor;
size?: RadioSize;
className?: {
wrapper?: string;
label?: string;
radioWrapper?: string;
};
isError?: boolean;
errorMessage?: string;
required?: boolean;
disabled?: boolean;
onChange?: ChangeEventHandler<HTMLInputElement>;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
children?: ReactNode;
}
export const RadioGroup = ({
label,
bottomLabel,
name,
value,
options,
color = 'primary',
size = 'md',
className,
isError,
errorMessage,
required = false,
disabled = false,
onChange,
onBlur,
children,
}: RadioGroupProps) => {
const contextValue: RadioGroupContextValue = {
name,
value,
color,
size,
disabled,
onChange,
onBlur,
};
return (
<RadioGroupContext.Provider value={contextValue}>
<div className={cn('w-full flex flex-col gap-2', className?.wrapper)}>
{/* Label atas */}
{label && (
<label
className={cn(
'w-full text-sm font-normal leading-5',
{ 'text-error': isError },
className?.label
)}
>
{label}
{required && (
<span className='text-error ml-1' title='required'>
*
</span>
)}
</label>
)}
{/* Daftar opsi radio */}
<div
className={cn(
'flex flex-row flex-wrap gap-4 items-center',
className?.radioWrapper
)}
>
{/* Jika options diberikan, render otomatis */}
{options &&
options.map((option) => (
<RadioGroupItem
key={option.value}
value={option.value}
label={option.label}
/>
))}
{/* Atau gunakan children untuk custom rendering */}
{children}
</div>
{/* Label bawah */}
{!isError && bottomLabel && (
<p className='text-sm opacity-60'>{bottomLabel}</p>
)}
{/* Pesan error */}
{isError && errorMessage && (
<p className='text-sm text-error'>{errorMessage}</p>
)}
</div>
</RadioGroupContext.Provider>
);
};
// RadioGroupItem Component
export interface RadioGroupItemProps {
value: string;
label?: string;
className?: string;
disabled?: boolean;
color?: RadioColor;
size?: RadioSize;
}
export const RadioGroupItem = ({
value,
label,
className,
disabled: itemDisabled,
color: itemColor,
size: itemSize,
}: RadioGroupItemProps) => {
const {
name,
value: groupValue,
color: groupColor,
size: groupSize,
disabled: groupDisabled,
onChange,
onBlur,
} = useRadioGroup();
const isDisabled = itemDisabled ?? groupDisabled;
const radioColor = itemColor ?? groupColor;
const radioSize = itemSize ?? groupSize;
return (
<label
className={cn(
'flex flex-row items-center gap-2 cursor-pointer',
isDisabled && 'opacity-60 cursor-not-allowed',
className
)}
>
<input
type='radio'
name={name}
value={value}
checked={groupValue === value}
onChange={onChange}
onBlur={onBlur}
disabled={isDisabled}
className={cn('radio', `radio-${radioColor}`, `radio-${radioSize}`)}
/>
{label && <span className='text-sm'>{label}</span>}
</label>
);
};