feat(FE-Storyless): update UI form UI repo

This commit is contained in:
rstubryan
2025-11-12 08:57:06 +07:00
parent ecb497430a
commit fde9c449a6
2 changed files with 66 additions and 56 deletions
+38 -23
View File
@@ -1,6 +1,13 @@
'use client';
import { ReactNode, RefObject, useCallback, useRef, useState } from 'react';
import {
ReactNode,
RefObject,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { cn } from '@/lib/helper';
export const useModal = () => {
@@ -8,31 +15,35 @@ export const useModal = () => {
const [open, setOpen] = useState(false);
const openModal = useCallback(() => {
if (!ref.current) return;
ref.current.showModal();
setOpen(true);
ref.current?.showModal();
}, []);
const closeModal = useCallback(() => {
if (!ref.current) return;
ref.current.close();
setOpen(false);
ref.current?.close();
}, []);
const toggle = useCallback(() => {
if (open) {
closeModal();
} else {
openModal();
}
open ? closeModal() : openModal();
}, [open, closeModal, openModal]);
if (ref.current) {
ref.current.addEventListener('close', () => {
closeModal();
});
}
// Gunakan useEffect agar event listener tidak didaftarkan berulang kali
useEffect(() => {
const dialog = ref.current;
if (!dialog) return;
return { ref, open, setOpen, openModal, closeModal, toggle } as const;
const handleClose = () => setOpen(false);
dialog.addEventListener('close', handleClose);
return () => {
dialog.removeEventListener('close', handleClose);
};
}, []);
return { ref, open, openModal, closeModal, toggle } as const;
};
interface ModalProps {
@@ -46,15 +57,19 @@ interface ModalProps {
}
const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => {
return (
<dialog ref={ref} className={cn('modal', className?.modal)}>
<div className={cn('modal-box', className?.modalBox)}>{children}</div>
const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {
if (closeOnBackdrop && e.target === ref.current) {
ref.current?.close();
}
};
{closeOnBackdrop && (
<form method='dialog' className='modal-backdrop'>
<button>close</button>
</form>
)}
return (
<dialog
ref={ref}
className={cn('modal', className?.modal)}
onClick={handleBackdropClick}
>
<div className={cn('modal-box', className?.modalBox)}>{children}</div>
</dialog>
);
};
+7 -12
View File
@@ -8,6 +8,7 @@ export interface TextAreaProps {
label?: string;
bottomLabel?: string;
name: string;
rows?: number;
value?: string | number;
placeholder?: string;
className?: {
@@ -23,11 +24,8 @@ export interface TextAreaProps {
required?: boolean;
isLoading?: boolean;
errorMessage?: string;
startAdornment?: ReactNode;
endAdornment?: ReactNode;
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
rows?: number;
}
const TextArea = ({
@@ -35,20 +33,18 @@ const TextArea = ({
bottomLabel,
name,
value,
rows = 3,
placeholder,
className,
isError,
isValid,
errorMessage,
startAdornment,
endAdornment,
disabled = false,
required = false,
onChange,
onBlur,
readOnly = false,
isLoading = false,
rows = 3,
}: TextAreaProps) => {
return (
<div
@@ -79,11 +75,11 @@ const TextArea = ({
)}
</label>
)}
{startAdornment && startAdornment}
<div className='relative size-full'>
<textarea
className={cn(
'input h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all bg-white',
'textarea h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all bg-white',
{
'border-error': isError,
'border-success!': isValid,
@@ -101,13 +97,12 @@ const TextArea = ({
readOnly={readOnly}
/>
{(isLoading || endAdornment) && (
<div className='flex flex-row gap-2'>
{isLoading && (
<div className='absolute right-3 bottom-3 z-10'>
{isLoading && <span className='loading loading-spinner' />}
{endAdornment && endAdornment}
</div>
)}
</div>
{!isError && bottomLabel && (
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>