Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import { useEffect, useMemo, useState } from 'react';
- import Select2, {
- CSSObjectWithLabel,
- GroupBase,
- SingleValue,
- StylesConfig,
- } from 'react-select';
- export interface SelectOption {
- value: string;
- label: string;
- isDefault?: boolean;
- }
- export interface SelectProps {
- id?: string;
- name: string;
- options: SelectOption[];
- defaultValue?: string;
- placeholder?: string;
- className?: string;
- errorMessage?: string;
- hint?: string;
- success?: boolean;
- isDisabled?: boolean;
- resetKey?: number;
- required?: boolean;
- /** Tampilkan tombol clear (Γ) untuk mengosongkan pilihan */
- allowClear?: boolean; // <β baru
- onChange?: (option: SelectOption | null) => void;
- }
- export default function ReactSelect2({
- id,
- name,
- options,
- defaultValue,
- placeholder = 'Pilihβ¦',
- className,
- errorMessage = '',
- hint,
- success = false,
- isDisabled,
- resetKey,
- required = false,
- allowClear = false, // <β default
- onChange,
- }: SelectProps) {
- const error = errorMessage !== '';
- const displayHint = error ? errorMessage : hint;
- // Prioritas: 1) defaultValue 2) opsi pertama isDefault 3) null
- const defaultSelected = useMemo(() => {
- if (defaultValue) {
- const byValue = options.find((opt) => opt.value === defaultValue);
- if (byValue) return byValue;
- }
- const defaults = options.filter((opt) => opt.isDefault);
- if (defaults.length > 0) return defaults[0];
- return null;
- }, [defaultValue, options]);
- useEffect(() => {
- const countDefault = options.reduce(
- (acc, o) => acc + (o.isDefault ? 1 : 0),
- 0,
- );
- if (!defaultValue && countDefault > 1) {
- console.warn(
- `[ReactSelect2:${name}] Terdapat ${countDefault} opsi dengan isDefault=true. ` +
- `Yang dipakai hanya yang pertama: "${
- options.find((o) => o.isDefault)?.label
- }".`,
- );
- }
- }, [options, defaultValue, name]);
- const [selected, setSelected] = useState<SelectOption | null>(
- defaultSelected,
- );
- useEffect(() => {
- setSelected(defaultSelected);
- }, [defaultSelected]);
- useEffect(() => {
- if (resetKey && resetKey > 0) setSelected(null);
- }, [resetKey]);
- const handleChange = (value: SingleValue<SelectOption>) => {
- setSelected(value);
- if (typeof onChange === 'function') onChange(value ?? null);
- };
- // Catatan SSR: idealnya ambil tema dari context/prop.
- const isDark =
- typeof window !== 'undefined' &&
- document.documentElement.classList.contains('dark');
- const customStyles: StylesConfig<
- SelectOption,
- false,
- GroupBase<SelectOption>
- > = {
- control: (base: CSSObjectWithLabel): CSSObjectWithLabel => ({
- ...base,
- minHeight: '44px',
- height: '44px',
- backgroundColor: isDisabled
- ? isDark
- ? '#1f2937'
- : '#f3f4f6'
- : isDark
- ? '#111827'
- : '#ffffff',
- borderColor: error
- ? '#ef4444'
- : success
- ? '#10b981'
- : isDark
- ? '#374151'
- : '#d1d5db',
- color: isDark ? '#f3f4f6' : '#000000',
- boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
- borderRadius: '8px',
- borderWidth: '1px',
- padding: '0 16px',
- fontSize: '14px',
- cursor: isDisabled ? 'not-allowed' : 'pointer',
- opacity: isDisabled ? 0.4 : 1,
- ':hover': {
- borderColor: error
- ? '#ef4444'
- : success
- ? '#10b981'
- : isDark
- ? '#4b5563'
- : '#9ca3af',
- },
- ':focus-within': {
- borderColor: error
- ? '#dc2626'
- : success
- ? '#059669'
- : isDark
- ? '#60a5fa'
- : '#3b82f6',
- boxShadow: error
- ? '0 0 0 3px rgba(239, 68, 68, 0.1)'
- : success
- ? '0 0 0 3px rgba(16, 185, 129, 0.1)'
- : isDark
- ? '0 0 0 3px rgba(96, 165, 250, 0.1)'
- : '0 0 0 3px rgba(59, 130, 246, 0.1)',
- },
- }),
- menu: (base) => ({
- ...base,
- backgroundColor: isDark ? '#1f2937' : '#ffffff',
- boxShadow: isDark
- ? '0 20px 25px -5px rgba(0, 0, 0, 0.5)'
- : '0 20px 25px -5px rgba(0, 0, 0, 0.1)',
- borderRadius: '8px',
- borderWidth: '1px',
- borderColor: isDark ? '#374151' : '#e5e7eb',
- }),
- menuList: (base) => ({
- ...base,
- backgroundColor: isDark ? '#1f2937' : '#ffffff',
- padding: '4px 0',
- }),
- option: (base, props) => ({
- ...base,
- backgroundColor: props.isSelected
- ? error
- ? '#ef4444'
- : success
- ? '#10b981'
- : '#3b82f6'
- : props.isFocused
- ? isDark
- ? '#374151'
- : '#f3f4f6'
- : isDark
- ? '#1f2937'
- : '#ffffff',
- color: props.isSelected
- ? '#ffffff'
- : isDark
- ? '#f3f4f6'
- : '#000000',
- padding: '10px 16px',
- cursor: 'pointer',
- ':active': {
- backgroundColor: error
- ? '#dc2626'
- : success
- ? '#059669'
- : '#2563eb',
- },
- }),
- input: (base) => ({
- ...base,
- color: isDark ? '#f3f4f6' : '#000000',
- margin: 0,
- padding: 0,
- }),
- placeholder: (base) => ({
- ...base,
- color: isDark ? '#9ca3af' : '#9ca3af',
- }),
- singleValue: (base) => ({
- ...base,
- color: isDisabled
- ? isDark
- ? '#6b7280'
- : '#9ca3af'
- : isDark
- ? '#f3f4f6'
- : '#000000',
- }),
- };
- // Style "visually hidden" agar tetap tervalidasi tapi tak terlihat
- const visuallyHidden: React.CSSProperties = {
- position: 'absolute',
- opacity: 0,
- width: 1,
- height: 1,
- padding: 0,
- margin: 0,
- border: 0,
- clip: 'rect(0 0 0 0)',
- clipPath: 'inset(50%)',
- overflow: 'hidden',
- whiteSpace: 'nowrap',
- pointerEvents: 'none',
- };
- const ariaInvalid = error || (required && !selected) ? true : undefined;
- return (
- <div className={className} aria-required={required}>
- <div className="relative">
- <Select2
- inputId={id}
- isClearable={allowClear && !isDisabled}
- isSearchable={true}
- isDisabled={isDisabled}
- options={options}
- value={selected}
- onChange={handleChange}
- placeholder={placeholder}
- classNamePrefix="rs"
- styles={customStyles}
- aria-invalid={ariaInvalid}
- />
- {/* Input yang ikut submit & validasi native (bukan type="hidden") */}
- <input
- type="text"
- name={name}
- value={selected?.value || ''}
- readOnly
- required={required}
- aria-hidden="true"
- aria-invalid={ariaInvalid}
- style={visuallyHidden}
- />
- {displayHint && (
- <p
- className={`mt-1.5 text-xs ${
- error
- ? 'text-error-500 dark:text-error-400'
- : success
- ? 'text-success-500 dark:text-success-400'
- : 'text-gray-500 dark:text-gray-400'
- }`}
- >
- {displayHint}
- </p>
- )}
- </div>
- </div>
- );
- }
Advertisement
Add Comment
Please, Sign In to add comment