입력·선택 컴포넌트
input, checkbox, radio, select, toggle의 설정과 변경 이벤트입니다. 모두 셀·헤더에 사용할 수 있습니다. 셀 input/select는 정확히 한 행을 선택했을 때 나타납니다. 콜백을 생략하면 애플리케이션 data를 갱신하지 않습니다.
입력 확정과 데이터 갱신
섹션 제목: “입력 확정과 데이터 갱신”input은 타이핑 중 내부 draft만 변경합니다. Enter 또는 blur에서 이전 확정 값과 다르면 onChange → onValueChange 순으로 호출합니다. Enter 뒤 blur가 이어져도 같은 값을 두 번 확정하지 않습니다. 두 콜백을 모두 등록하면 각각 호출되므로 실제 데이터 갱신은 한쪽에서 수행하십시오. props.onChange는 이 컴포넌트의 확정 이벤트가 아닙니다. Escape 취소를 기본 동작으로 가정하지 마십시오.
select/radio의 onValueChange.value는 원래 option.value가 number/boolean이어도 string입니다. 업무 타입으로 명시적으로 변환합니다. checkbox/toggle은 checked boolean을 받습니다. checkbox/toggle의 즉시 표시 상태는 일시적이며 최종 값은 부모 data에 반영해야 합니다.
직접 확인
섹션 제목: “직접 확인”행을 선택한 뒤 이름을 수정하십시오. 타이핑 중에는 아래 데이터가 유지되고 Enter로 한 번 확정됩니다. 역할 선택·체크·토글·버튼은 전용 콜백만 기록하며 일반 셀/행 클릭 횟수를 늘리지 않습니다.
import { useMemo, useRef, useState } from 'react';import { CominsTable, type CominsTableColumn, type CominsTableRef, type CominsVirtualListItem } from 'comins-table';import 'comins-table/styles.css';
type Row = { id: string; name: string; role: string; active: boolean; ready: boolean; progress: number; items: CominsVirtualListItem[] };const initialRows: Row[] = ['alpha', 'beta'].map((id, index) => ({ id, name: index ? 'Beta' : 'Alpha', role: 'reader', active: false, ready: false, progress: index ? 75 : 40, items: Array.from({ length: 12 }, (_, i) => ({ value: `${id}-${i + 1}`, label: `Task ${i + 1}`, searchText: `task-${i + 1}` })),}));
export default function TableComponentsDemo({ lang = 'ko', mode = 'inputs' }: { lang?: string; mode?: 'inputs' | 'list' }) { const ko = lang === 'ko'; const table = useRef<CominsTableRef<Row>>(null); const [rows, setRows] = useState(initialRows); const [events, setEvents] = useState<string[]>([]); const [clicks, setClicks] = useState({ cell: 0, row: 0 }); const record = (message: string) => setEvents(previous => [...previous.slice(-11), message]); const update = (id: string, patch: Partial<Row>) => setRows(previous => previous.map(row => row.id === id ? { ...row, ...patch } : row)); const columns = useMemo<CominsTableColumn<Row>[]>(() => { const identity: CominsTableColumn<Row> = { field: 'id', label: 'ID', width: 130, header: { components: [{ type: 'menu', id: 'actions', direction: 'right', props: { children: '⋯', 'aria-label': ko ? '헤더 메뉴' : 'Header menu' }, items: [{ type: 'label', label: ko ? '동작' : 'Actions' }, { value: 'inspect', label: ko ? '상태 확인' : 'Inspect state' }, { type: 'divider' }, { value: 'disabled', label: ko ? '비활성 항목' : 'Disabled item', disabled: true }], onOpenChange: ({ open }) => record(`menu:${open}`), onSelect: ({ value }) => record(`menu-select:${value}`), }] }, }; if (mode === 'list') return [identity, { field: 'items', label: ko ? '작업 목록' : 'Tasks', width: 420, cell: { components: [{ type: 'virtual-list', id: 'tasks', items: ({ row }) => row.data.items, props: { limit: 3, itemHeight: 28, height: 140, more: true, searchable: true, 'aria-label': ko ? '작업 목록' : 'Tasks' }, searchFilter: ({ item, value }) => (item.searchText ?? '').includes(value.trim().toLowerCase()), onClickItem: ({ row, itemIndex, value }) => record(`item:${row.id}:${itemIndex}:${value}`), }] }, }]; return [identity, { field: 'name', label: ko ? '이름' : 'Name', width: 180, cell: { components: [{ type: 'input', id: 'name', props: ({ value, row }) => ({ value: String(value), 'aria-label': `${row.id} name` }), onChange: ({ row, value }) => { update(String(row.id), { name: value }); record(`commit:${row.id}:${value}`); }, onValueChange: ({ row, value }) => record(`value:${row.id}:${value}`), }] } }, { field: 'role', label: ko ? '역할' : 'Role', width: 140, cell: { components: [{ type: 'select', id: 'role', options: [{ value: 'reader', label: ko ? '읽기' : 'Reader' }, { value: 'editor', label: ko ? '편집' : 'Editor' }], props: ({ value, row }) => ({ value: String(value), 'aria-label': `${row.id} role` }), onValueChange: ({ row, value }) => { update(String(row.id), { role: value }); record(`role:${value}`); }, }] } }, { field: 'active', label: ko ? '활성' : 'Active', width: 90, cell: { components: [{ type: 'checkbox', id: 'active', props: ({ value, row }) => ({ checked: Boolean(value), 'aria-label': `${row.id} active` }), onCheckedChange: ({ row, checked }) => { update(String(row.id), { active: checked }); record(`active:${checked}`); }, }] } }, { field: 'role', id: 'role-radio', label: ko ? '역할 라디오' : 'Role radio', width: 190, cell: { components: [{ type: 'radio', id: 'role-radio', options: [{ value: 'reader', label: ko ? '읽기' : 'Reader' }, { value: 'editor', label: ko ? '편집' : 'Editor' }], props: ({ value, row }) => ({ value: String(value), 'aria-label': `${row.id} role radio` }), onValueChange: ({ row, value }) => { update(String(row.id), { role: value }); record(`radio:${value}`); }, }] } }, { field: 'ready', label: ko ? '준비' : 'Ready', width: 90, cell: { components: [{ type: 'toggle', id: 'ready', props: ({ value, row }) => ({ checked: Boolean(value), children: value ? 'ON' : 'OFF', 'aria-label': `${row.id} ready` }), onCheckedChange: ({ row, checked }) => { update(String(row.id), { ready: checked }); record(`ready:${checked}`); }, }] } }, { field: 'progress', label: ko ? '진행률' : 'Progress', width: 120, cell: { components: [{ type: 'progress', id: 'progress', props: ({ value }) => ({ value: Number(value), max: 100 }) }] } }, { field: 'id', id: 'action', label: ko ? '동작' : 'Action', width: 110, cell: { components: [{ type: 'button', id: 'inspect', props: ({ row }) => ({ children: ko ? '확인' : 'Inspect', 'aria-label': `${row.id} inspect` }), onClick: ({ row }) => record(`button:${row.id}`), }] } }, ]; }, [ko, mode]); return <div className="demo" data-demo={`table-components-${mode}`}> <div className="demo-toolbar"> <button onClick={() => table.current?.setSelectedRows([0])}>{ko ? '첫 행 선택' : 'Select first row'}</button> <button onClick={() => { setRows(initialRows); setEvents([]); setClicks({ cell: 0, row: 0 }); table.current?.setSelectedRows([]); }}>{ko ? '초기화' : 'Reset'}</button> </div> <div className="table-viewport" style={mode === 'list' ? { height: 380 } : undefined}> <CominsTable ref={table} columns={columns} data={rows} getRowId={row => row.id} onChangeData={setRows} getRowHeight={mode === 'list' ? () => 'auto' : undefined} onClickCell={() => setClicks(value => ({ ...value, cell: value.cell + 1 }))} onClickRow={() => setClicks(value => ({ ...value, row: value.row + 1 }))} /> </div> <p className="demo-status" data-component-clicks>{ko ? '일반 클릭' : 'Ordinary clicks'}: cell {clicks.cell} / row {clicks.row}</p> <details open><summary>{ko ? '이벤트 순서' : 'Event order'}</summary><pre data-component-events>{JSON.stringify(events, null, 2)}</pre></details> <details><summary>{ko ? '부모 데이터' : 'Parent data'}</summary><pre data-component-data>{JSON.stringify(rows.map(({ items, ...row }) => ({ ...row, itemCount: items.length })), null, 2)}</pre></details> </div>;}CominsInputComponentConfig
섹션 제목: “CominsInputComponentConfig”type
섹션 제목: “type”type: "input";필수 리터럴 input입니다.
props
섹션 제목: “props”props?: CominsTableComponentProps<TPayload, React.InputHTMLAttributes<HTMLInputElement>>;input HTML 속성 또는 resolver. value/defaultValue가 없으면 빈 문자열입니다. 숫자형 input도 이벤트 값은 문자열입니다.
onValueChange
섹션 제목: “onValueChange”onValueChange?: (payload: TPayload & { value: string; }) => void;확정된 문자열을 전달합니다. 기본 콜백 없음. 변경 확정 시점은 위 설명을 따릅니다.
onChange
섹션 제목: “onChange”onChange?: (payload: TPayload & { event: CominsInputCommitEvent; value: string; }) => void;확정 이벤트와 문자열 value를 전달합니다. onValueChange보다 먼저 호출합니다. event 타입은 아래 CominsInputCommitEvent입니다.
CominsCheckboxComponentConfig
섹션 제목: “CominsCheckboxComponentConfig”type
섹션 제목: “type”type: "checkbox";필수 리터럴 checkbox입니다.
props
섹션 제목: “props”props?: CominsTableComponentProps<TPayload, React.InputHTMLAttributes<HTMLInputElement>>;checked/defaultChecked 초기값은 false입니다. disabled·aria-label 등 input 속성을 지정합니다.
onCheckedChange
섹션 제목: “onCheckedChange”onCheckedChange?: (payload: TPayload & { checked: boolean; }) => void;변경된 checked를 전달합니다. 미지정 시 부모 data는 그대로입니다.
CominsRadioComponentConfig
섹션 제목: “CominsRadioComponentConfig”type
섹션 제목: “type”type: "radio";필수 리터럴 radio입니다.
props
섹션 제목: “props”props?: CominsTableComponentProps<TPayload, React.HTMLAttributes<HTMLDivElement> & { value?: CominsComponentPrimitiveValue; }>;div HTML 속성과 선택 value입니다. 셀에서는 value 생략 시 원본 셀 값을 사용합니다. 항목 비활성은 options[].disabled입니다.
onValueChange
섹션 제목: “onValueChange”onValueChange?: (payload: TPayload & { value: string; }) => void;선택한 option.value를 문자열로 전달합니다.
options
섹션 제목: “options”options: CominsTableOptions<TPayload>;필수. 선택지 배열 또는 payload 기반 함수입니다. label/value를 제공하고 문자열로 변환했을 때도 value가 고유하도록 구성합니다.
CominsSelectComponentConfig
섹션 제목: “CominsSelectComponentConfig”type
섹션 제목: “type”type: "select";필수 리터럴 select입니다.
props
섹션 제목: “props”props?: CominsTableComponentProps<TPayload, React.SelectHTMLAttributes<HTMLSelectElement>>;select HTML 속성 또는 resolver입니다. props.value로 부모 값을 연결합니다.
onValueChange
섹션 제목: “onValueChange”onValueChange?: (payload: TPayload & { value: string; }) => void;선택 변경 시 문자열을 전달합니다. 기본 콜백 없음.
options
섹션 제목: “options”options: CominsTableOptions<TPayload>;필수. 선택지 배열 또는 payload 기반 함수입니다. label/value를 제공하고 문자열로 변환했을 때도 value가 고유하도록 구성합니다.
CominsToggleComponentConfig
섹션 제목: “CominsToggleComponentConfig”type
섹션 제목: “type”type: "toggle";필수 리터럴 toggle입니다.
props
섹션 제목: “props”props?: CominsTableComponentProps<TPayload, React.ButtonHTMLAttributes<HTMLButtonElement> & { checked?: boolean; }>;button HTML 속성과 checked입니다. 초기 checked는 false. children으로 버튼 내용을 제공합니다.
onCheckedChange
섹션 제목: “onCheckedChange”onCheckedChange?: (payload: TPayload & { checked: boolean; }) => void;반전된 checked boolean을 전달합니다. 기본 콜백 없음.
CominsTableComponentOption
섹션 제목: “CominsTableComponentOption”label
섹션 제목: “label”label: React.ReactNode;필수. 선택지에 표시하는 내용입니다.
value
섹션 제목: “value”value: CominsComponentPrimitiveValue;필수. string/number/boolean 값입니다. select/radio 이벤트에서는 문자열로 전달됩니다.
disabled
섹션 제목: “disabled”disabled?: boolean;기본 false. 해당 선택지를 비활성화합니다.
CominsInputCommitEvent
섹션 제목: “CominsInputCommitEvent”export type CominsInputCommitEvent = React.ChangeEvent<HTMLInputElement> | React.FocusEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>;배포 타입은 Change/Focus/Keyboard 이벤트의 합집합입니다. 현재 input 구현은 Enter/blur 확정에서 Keyboard/Focus 이벤트를 전달합니다.