셀 가상 목록 옵션
type: "virtual-list"는 한 셀에 여러 항목을 렌더링하며 헤더에는 사용할 수 없습니다. 목록의 선택·확장·검색 상태는 일시적이며 가상 스크롤로 셀이 재생성되면 초기화될 수 있습니다.
표시와 선택
섹션 제목: “표시와 선택”| props | 기본값 | 동작 |
|---|---|---|
| itemHeight | 28px | 목록 항목 높이, 최소 1 |
| limit | 5 | 초기 미리보기 개수, 최소 1 |
| height | itemHeight × 5 | 펼친 목록의 제한된 스크롤 높이; 숫자 또는 CSS 길이 |
| more | false | true이면 넘친 항목의 …를 확장 버튼으로 표시 |
| searchable | false | true이며 해당 행만 단독 선택했을 때 검색 표시 |
More는 먼저 해당 행을 단독 선택한 뒤 목록을 확장합니다. more가 false여도 행을 단독 선택하면 넘친 목록이 가상 스크롤로 전환됩니다. 일반 항목 클릭은 행을 선택하고 Ctrl/Cmd는 토글, Shift는 표시 행 범위 선택입니다. Enter/Space도 같은 선택 규칙을 따릅니다. 검색어는 항목 클릭 이벤트의 value와 다른 의미입니다.
직접 확인
섹션 제목: “직접 확인”목록의 More를 누른 뒤 검색하십시오. 표시 목록은 바뀌어도 원본 12개 항목은 유지됩니다.
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>;}CominsVirtualListComponentConfig
섹션 제목: “CominsVirtualListComponentConfig”type
섹션 제목: “type”type: "virtual-list";필수 virtual-list입니다.
items
섹션 제목: “items”items: CominsVirtualListItems<TPayload>;필수. 항목 배열 또는 셀 payload 함수입니다. value는 목록 내에서 고유하게 유지합니다.
props
섹션 제목: “props”props?: CominsTableComponentProps<TPayload, React.HTMLAttributes<HTMLDivElement> & { height?: number | string; itemHeight?: number; limit?: number; more?: boolean; searchable?: boolean; }>;div HTML 속성과 위 표의 height/itemHeight/limit/more/searchable 설정입니다. 객체 또는 resolver를 받습니다.
searchFilter
섹션 제목: “searchFilter”searchFilter?: (payload: CominsVirtualListSearchFilterPayload) => boolean;기본 검색은 label의 텍스트를 대소문자 무시 포함 검색합니다. 사용자 함수는 item/itemIndex/검색어 value를 받고 boolean을 반환합니다.
onClickItem
섹션 제목: “onClickItem”onClickItem?: (payload: TPayload & { event: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>; item: CominsVirtualListItem; itemIndex: number; value: CominsComponentPrimitiveValue; }) => void;item/itemIndex/선택한 item.value와 셀 payload를 전달합니다. event는 MouseEvent 또는 KeyboardEvent입니다.
onContextMenuItem
섹션 제목: “onContextMenuItem”onContextMenuItem?: (payload: TPayload & { event: React.MouseEvent<HTMLButtonElement>; item: CominsVirtualListItem; itemIndex: number; value: CominsComponentPrimitiveValue; }) => void;항목 우클릭 이벤트입니다. 사용자 메뉴를 표시하려면 event.preventDefault()로 브라우저 메뉴를 막습니다.
CominsVirtualListItem
섹션 제목: “CominsVirtualListItem”value
섹션 제목: “value”value: CominsComponentPrimitiveValue;필수. 항목 식별에 사용하는 string/number/boolean 값입니다.
label
섹션 제목: “label”label: React.ReactNode;필수. 표시 내용이자 기본 검색 대상입니다. 복잡한 ReactNode는 검색용 텍스트 추출이 제한될 수 있습니다.
data
섹션 제목: “data”data?: TItem;선택 사항. 항목에 연결할 사용자 데이터입니다. 자동 테이블 편집에 사용하지 않습니다.
disabled
섹션 제목: “disabled”disabled?: boolean;기본 false. 항목 활성화를 막습니다.
searchText
섹션 제목: “searchText”searchText?: string;배포 타입에는 있지만 0.2.0 기본 검색은 이 필드를 읽지 않습니다. 사용하려면 searchFilter에서 item.searchText를 직접 참조합니다.
CominsVirtualListSearchFilterPayload
섹션 제목: “CominsVirtualListSearchFilterPayload”item
섹션 제목: “item”item: CominsVirtualListItem;검사할 원본 항목입니다.
itemIndex
섹션 제목: “itemIndex”itemIndex: number;필터 전 원본 items 배열 인덱스입니다.
value
섹션 제목: “value”value: string;현재 검색 input 문자열입니다. item.value와 다릅니다.
CominsVirtualListItems
섹션 제목: “CominsVirtualListItems”export type CominsVirtualListItems<TPayload> = Array<CominsVirtualListItem> | ((payload: TPayload) => Array<CominsVirtualListItem>);배열 또는 셀 payload를 받는 항목 생성 함수입니다.