feat: Add detailed documentation and improve styling for form components

This commit is contained in:
Firman Ramdhani
2026-01-28 12:09:18 +07:00
parent b52e458a9d
commit 883f42f598
11 changed files with 184 additions and 52 deletions
@@ -3,6 +3,27 @@ import { CheckboxProps } from '../types';
import { forwardRef } from 'react';
import { InputRef } from '@rc-component/input/lib/interface';
/**
* Checkbox component built on top of RcCheckbox.
*
* Renders a checkbox with an optional label (uses `label` prop or `children`).
* Default prop values: `status = 'default'`, `variant = 'outlined'`.
*
* The wrapper element will receive these classes:
* - 'rc-checkbox-wrapper'
* - `checkbox-status-{status}`
* - `checkbox-variant-{variant}`
* - 'rc-checkbox-wrapper-disabled' when `disabled` is true
* and any additional classes passed via `className`.
*
* All remaining props are forwarded to the underlying RcCheckbox, which is rendered with `prefixCls="rc-checkbox"`.
*
* Note: a `ref` parameter is accepted by the forwardRef call but is not currently attached to the rendered input element.
*
* @param props - Checkbox props
* @param ref - Forwarded ref (InputRef). Accepted but not applied to the DOM node in the current implementation.
* @returns JSX.Element
*/
export const Checkbox = forwardRef<InputRef, CheckboxProps>((props, ref) => {
const { label, status = 'default', variant = 'outlined', className, children, ...rest } = props;
@@ -21,11 +42,8 @@ export const Checkbox = forwardRef<InputRef, CheckboxProps>((props, ref) => {
return (
<label className={wrapperClass}>
<span className="rc-checkbox-container">
<RcCheckbox {...rest} prefixCls="rc-checkbox" />
{/* HAPUS BARIS DI BAWAH INI */}
{/* <span className="rc-checkbox-inner" /> */}
<RcCheckbox ref={ref} {...rest} prefixCls="rc-checkbox" />
</span>
{displayLabel && <span className="rc-checkbox-label-text">{displayLabel}</span>}
</label>
);
@@ -1,6 +1,14 @@
import RcForm from '@rc-component/form';
import { FormProps } from '../types';
/**
* A form control component that wraps the underlying form library.
*
* @template Values - The type of values managed by the form. Defaults to `any`.
* @param props - The form configuration properties.
* @param props.children - The child elements to render within the form.
* @returns A rendered form component with the provided children.
*/
export default function FormControl<Values = any>({ children, ...props }: FormProps<Values>) {
return <RcForm {...props}>{children}</RcForm>;
}
@@ -2,6 +2,36 @@ import React, { ReactElement } from 'react';
import { Field } from '@rc-component/form';
import { FormItemProps } from '../types';
/**
* FormItem component that wraps form fields with validation, error/warning messaging, and styling.
*
* This component manages the rendering of a form field with associated label, validation rules,
* and status-based styling. It integrates with a Field control system to handle form state,
* errors, and warnings.
*
* @component
* @example
* ```tsx
* <FormItem
* name="username"
* label="Username"
* rules={[{ required: true, message: 'Username is required' }]}
* >
* <Input />
* </FormItem>
* ```
*
* @param {FormItemProps} props - The component props
* @param {string} props.name - The field name for form control binding
* @param {string} [props.label] - The label text to display above the input
* @param {ReactNode} props.children - The form control element (e.g., Input, Textarea)
* @param {Array<ValidationRule>} [props.rules] - Validation rules for the field
* @param {string} [props.valuePropName='value'] - The prop name used to bind the field value
* @param {any} [props.initialValue] - The initial value for the field
* @param {'error' | 'warning' | 'normal'} [props.status] - Override the status appearance
*
* @returns {ReactElement} A form item container with label, control, and validation messages
*/
export default function FormItem(props: FormItemProps) {
const { name, label, children, rules, valuePropName = 'value', initialValue, status: OverrideStatus } = props;
@@ -9,6 +9,18 @@ import FormItem from './form-item.component';
/**
* Internal Form component using forwardRef to handle RcForm instance
*/
/**
* React component that provides form context and forwards a ref to the underlying FormControl.
*
* @template Values - Shape of the form values (extends object). Defaults to any.
* @param props - FormProps for the form, augmented with an optional forwarded ref.
* @param props.ref - Optional forwarded ref passed to the underlying FormControl.
* @returns A React element that sets up form context and delegates rendering/behavior to FormControl.
*
* @remarks
* - Built using React.forwardRef and typed as a generic component so callers can infer form value types.
* - Use to wrap form fields and access form state/actions via context.
*/
const FormProvider = React.forwardRef(FormControl) as <Values extends object = any>(
props: FormProps<Values> & { ref?: React.ForwardedRef<any> },
) => React.ReactElement;
@@ -1,9 +1,35 @@
/**
* A controlled input component for numeric values with increment/decrement handlers.
*
* Wraps the rc-component InputNumber with custom styling support and status indicators.
* Features custom chevron-style up/down icons and support for multiple visual variants.
*
* @component
* @example
* ```tsx
* const ref = useRef<HTMLInputElement>(null);
* <InputNumber
* ref={ref}
* value={10}
* onChange={(val) => console.log(val)}
* status="success"
* variant="outlined"
* />
* ```
*
* @param {InputNumberProps} props - The input number component props
* @param {InputStatusType} [props.status] - The validation status ('default', 'success', 'error', etc.)
* @param {string} [props.className] - Additional CSS classes to apply
* @param {'outlined' | 'filled' | 'borderless'} [props.variant='outlined'] - The visual variant style
* @param {React.Ref<HTMLInputElement>} ref - Ref to the underlying input element
*
* @returns {React.ReactElement} The rendered input number component
*/
import { forwardRef } from 'react';
import RcInputNumber from '@rc-component/input-number';
import { InputNumberProps, InputStatusType, VALID_INPUT_STATUSES } from '../types';
// Handler Icons (Chevron Modern)
// Kita bungkus dengan span agar styling flexbox lebih mudah diatur nanti
const UpIcon = () => (
<span className="input-number-handler-up-inner flex items-center justify-center w-full h-full">
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="currentColor" className="w-2.5 h-2.5">
@@ -2,19 +2,36 @@ import { forwardRef } from 'react';
import RcInput, { InputRef } from '@rc-component/input';
import { InputProps, InputStatusType, VALID_INPUT_STATUSES } from '../types';
/**
* Input component that wraps the RC Input library with custom status and variant support.
*
* @component
* @param {InputProps} props - The input component props
* @param {InputStatusType} [props.status] - The validation status of the input (validated against VALID_INPUT_STATUSES)
* @param {string} [props.className] - Additional CSS classes to apply to the input
* @param {'outlined' | 'filled'} [props.variant='outlined'] - The visual variant of the input
* @param {boolean} [props.disabled] - Whether the input is disabled
* @param {InputRef} ref - Reference to the underlying RC Input element
* @returns {React.ReactElement} The rendered Input component with computed classes for status, variant, and disabled state
*
* @remarks
* - Invalid status values default to 'default'
* - The 'rc-input-disabled' class is automatically applied by RC Input library when disabled={true}
* - The 'input-disabled' helper class is manually added for custom wrapper styling when needed
*/
export const Input = forwardRef<InputRef, InputProps>((props, ref) => {
const { status, className, variant = 'outlined', disabled, ...restProps } = props;
// Validasi status agar aman
// Determine current status, defaulting to 'default' if invalid
const currentStatus: InputStatusType = status && VALID_INPUT_STATUSES.includes(status) ? status : 'default';
// Construct classname
// Note: class 'rc-input-disabled' biasanya otomatis ditambahkan oleh library jika disabled={true}
// Note: class 'rc-input-disabled' is added automatically by RC Input when disabled
const computedClassName = [
className,
`input-status-${currentStatus}`,
`input-variant-${variant}`,
// Kita tambahkan class helper ini manual jika ingin styling spesifik wrapper saat disabled
// Add custom disabled class if disabled
disabled ? 'input-disabled' : '',
]
.filter(Boolean)
@@ -10,7 +10,7 @@
font-weight: var(--form-label-font-weight);
color: var(--form-label-color);
line-height: 1;
gap: 8px; /* Jarak antara kotak dan teks */
gap: 8px; /* Distance between checkbox and label */
transition: all 0.2s;
}
@@ -30,7 +30,7 @@
position: relative;
line-height: 1;
vertical-align: middle;
top: -1px; /* Optikal alignment */
top: -1px; /* Optically aligns the checkbox */
}
.rc-checkbox-input {
@@ -47,18 +47,18 @@
top: 0;
left: 0;
display: inline-block;
width: 16px; /* Sedikit diperbesar dari 14px agar proporsional dengan font 13px */
width: 16px; /* Size of the checkbox */
height: 16px;
border-width: 1px;
border-style: solid;
border-radius: var(--radius-sm); /* Konsisten dengan shape global */
border-radius: var(--radius-sm); /* Consistent with global shape */
border-color: var(--input-border);
background-color: var(--input-bg);
/* Menggunakan durasi transisi standar kita 0.2s */
/* Use standard 0.2s transition */
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Checkmark (The L-shape) */
/* Check mark (The L-shape) */
.rc-checkbox-inner:after {
content: ' ';
position: absolute;
@@ -68,7 +68,7 @@
border-left: 0;
transform: rotate(45deg) scale(0);
opacity: 0;
/* Position adjust untuk 16px box */
/* Position adjust for 16px box */
left: 4.5px;
top: 1.5px;
width: 5px;
@@ -191,13 +191,13 @@
/* --- Default Filled --- */
.checkbox-variant-filled .rc-checkbox-inner {
background-color: var(--color-gray-50);
border-color: var(--input-border); /* Sedikit border agar tetap terlihat di bg putih */
border-color: var(--input-border); /* Slightly border to remain visible on white background */
}
.checkbox-variant-filled:hover .rc-checkbox-inner {
background-color: var(--color-gray-100);
border-color: var(--input-border-hover);
}
/* Saat checked, filled beralih ke warna solid brand */
/* Checked State for Filled */
.checkbox-variant-filled.checkbox-status-default .rc-checkbox-checked .rc-checkbox-inner {
background-color: var(--color-brand-500);
border-color: var(--color-brand-500);
@@ -259,9 +259,9 @@
border-color: var(--color-info-500);
}
/* Logic Focus untuk Filled (tetap konsisten dengan ring status) */
/* Focus Ring for Filled Variant */
.checkbox-variant-filled .rc-checkbox-input:focus-visible + .rc-checkbox-inner {
/* Box shadow akan mengikuti warna status masing-masing dari block STATUS MATRIX di atas */
/* Box shadow will follow the status color from the STATUS MATRIX block above */
outline: none;
}
@@ -25,7 +25,7 @@
.rc-form-item-required-mark {
margin-left: 0.25rem;
color: var(--color-error-500);
font-family: SimSun, sans-serif; /* Standard font for asterisks agar vertical align bagus */
font-family: SimSun, sans-serif; /* Standard font for asterisks agar vertical align */
}
/* =========================
@@ -35,7 +35,7 @@
margin-top: var(--form-msg-margin-top);
font-size: var(--form-msg-font-size);
line-height: 1.4;
min-height: 1.4em; /* Mencegah layout jumping jika error muncul/hilang */
min-height: 1.4em; /* Reserve space even when empty to prevent layout shift */
transition: color 0.2s;
}
@@ -45,7 +45,7 @@
/* --- ERROR STATE --- */
.rc-form-item.status-error .rc-form-item-label {
color: var(--color-error-600); /* Label jadi merah saat error (opsional UI pattern) */
color: var(--color-error-600); /* Darker shade for label on error */
}
.rc-form-item.status-error .rc-form-item-explain {
color: var(--color-error-500);
@@ -19,7 +19,7 @@
background-color: var(--input-bg);
border: 1px solid transparent;
/* Transisi standar */
/* Standard Transition */
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
margin: 0;
@@ -35,10 +35,10 @@
border: 0;
border-radius: var(--input-radius);
outline: 0;
color: inherit; /* Mewarisi dari parent */
color: inherit; /* Inherit from .rc-input-number */
transition: all 0.2s linear;
/* Padding Kanan Wajib (Space untuk Actions) */
/* Prevent text under action buttons */
padding-right: var(--input-handler-width);
}
@@ -74,7 +74,7 @@
transition: margin-right 0.2s ease-in-out;
}
/* Animation Trigger: Geser Suffix saat hover/focus agar tidak tertutup tombol action */
/* Animation Trigger: On Hover/Focus, Suffix shifts left to accommodate action buttons */
.rc-input-number:hover .rc-input-number-suffix,
.rc-input-number-affix-wrapper:hover .rc-input-number-suffix,
.rc-input-number-focused .rc-input-number-suffix,
@@ -97,10 +97,10 @@
width: var(--input-handler-width);
height: 100%;
/* Menggunakan var input-bg agar konsisten (putih) */
/* Using var input-bg for consistency (white) */
background: var(--input-bg);
/* Default Border (akan di-override status) */
/* Default Border (will be overridden by status) */
border-left: 1px solid var(--input-border);
border-radius: 0 var(--input-radius) var(--input-radius) 0;
@@ -138,10 +138,10 @@
.rc-input-number-action:hover {
color: var(--color-brand-500);
height: 60%; /* Efek hover button membesar dikit */
height: 60%; /* Effect hover slightly reduces height */
}
/* Border Bawah untuk Action UP */
/* Border between Up/Down Buttons */
.rc-input-number-action-up {
border-bottom: 1px solid var(--input-border);
}
@@ -3,15 +3,15 @@
BASE RESET & SHARED
========================================= */
.rc-input {
/* Wajib border-box agar padding tidak merusak lebar */
/* Use box-sizing border-box for predictable sizing */
box-sizing: border-box;
display: inline-block;
width: 100%;
/* Menggunakan Variable Dimensi Input */
/* Using variable dimensions */
height: var(--input-height);
padding: 0 var(--input-padding-x); /* Konsisten dengan 0.92rem */
padding: 0 var(--input-padding-x); /* Consistent with 0.92rem */
font-size: var(--input-font-size);
line-height: 1.5;
@@ -21,7 +21,7 @@
border: 1px solid transparent;
border-radius: var(--input-radius);
/* Transisi standar */
/* Standard Transition */
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
}
@@ -43,7 +43,7 @@
/* =========================================
CLEAR ICON
Tombol (x) untuk reset value
Button (x) to clear input content
========================================= */
.rc-input-clear-icon {
/* Reset Button Styles */
@@ -57,20 +57,20 @@
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-sm); /* ~12px sesuai global */
font-size: var(--text-sm); /* ~12px icon size */
line-height: 1;
cursor: pointer;
/* Colors & Transition */
color: var(--color-gray-400); /* Warna default (abu muda) */
color: var(--color-gray-400); /* Default gray color */
transition: color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
/* Visibility Control (Opacity lebih smooth dari display:none jika mau animasi) */
/* Tapi untuk simplicity sesuai request, kita pakai logic display/visibility standar */
/* Visibility Control ( Smooth appearance/disappearance could use opacity + pointer-events )
/* visibility: hidden; */
visibility: visible;
}
/* Hover State: Lebih gelap agar user tahu bisa diklik */
/* Hover State: Darker Gray */
.rc-input-clear-icon:hover {
color: var(--color-gray-500);
}
@@ -83,19 +83,16 @@
/* Hidden State */
.rc-input-clear-icon-hidden {
display: none;
/* Alternatif jika ingin layout tetap terjaga tapi invisible:
/* Alternative approach to keep layout but invisible:
visibility: hidden;
pointer-events: none;
*/
}
/* Placement Helper:
Biasanya clear icon muncul di dalam suffix atau menggantikan suffix.
Jika dia berdiri sendiri sebagai sibling terakhir:
*/
/* Placement Helper: Margin to separate from input text */
.rc-input-affix-wrapper .rc-input-clear-icon {
margin-left: 0.5rem; /* ~8px spacing dari text */
z-index: 2; /* Pastikan di atas layer lain */
margin-left: 0.5rem; /* ~8px spacing from text */
z-index: 2; /* Above input text */
}
/* =========================================
@@ -109,7 +106,7 @@
align-items: center;
width: 100%;
/* Menggunakan Variable Dimensi Input */
/* Using variable dimensions */
height: var(--input-height);
padding: 0 var(--input-padding-x);
@@ -119,7 +116,7 @@
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Reset Input di dalam Wrapper */
/* Reset Input inside Affix Wrapper */
.rc-input-affix-wrapper .rc-input {
width: 100%;
height: 100%;
@@ -133,7 +130,7 @@
/* Prefix & Suffix Positioning */
.rc-input-prefix {
margin-right: 0.5rem; /* ~8px, hardcoded ok atau bisa buat var baru */
margin-right: 0.5rem; /* ~8px, hardcoded */
display: flex;
align-items: center;
flex: none;
@@ -260,7 +257,7 @@
background-color: var(--color-error-50);
border-color: var(--color-error-500);
}
/* Saat focus, background jadi default input-bg (putih) */
/* When focused, background becomes default input-bg (white) */
.input-variant-filled.input-status-error:focus,
.input-variant-filled.input-status-error.rc-input-affix-wrapper-focused {
background-color: var(--input-bg);
@@ -1,7 +1,31 @@
import React, { forwardRef } from 'react';
import { forwardRef } from 'react';
import RcTextArea, { TextAreaRef } from '@rc-component/textarea';
import { InputStatusType, TextAreaProps, VALID_INPUT_STATUSES } from '../types';
/**
* Forwarding React component that renders an RcTextArea with normalized status and variant styling.
*
* The component computes a CSS class string from the optional `className`, a `textarea-status-<status>` class
* (falling back to 'default' when `status` is missing or not one of VALID_INPUT_STATUSES), and a
* `textarea-variant-<variant>` class. All other props are passed through to the underlying RcTextArea.
*
* The component forwards a ref to the underlying RcTextArea element.
*
* @remarks
* - `variant` defaults to `'outlined'`.
* - `status` is validated against `VALID_INPUT_STATUSES`; if invalid or absent, `'default'` is used.
* - Computed className shape: [className?, `textarea-status-${status}`, `textarea-variant-${variant}`].filter(Boolean).join(' ')
*
* @param props.status - Optional input status (validated against VALID_INPUT_STATUSES).
* @param props.className - Optional additional class name(s) to apply.
* @param props.variant - Visual variant of the textarea; defaults to `'outlined'`.
* @param props.rest - All other props are forwarded to RcTextArea.
*
* @returns A React element: the wrapped RcTextArea with forwarded ref and computed classes.
*
* @example
* <TextArea status="error" variant="filled" placeholder="Enter text" />
*/
export const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
const { status, className, variant = 'outlined', ...restProps } = props;