Built-in Editors
Configure the built-in text and checkbox editors and the provided Select and Date plugins, including header controls, value conversion, and editor icons.
import { t } from './i18n';
import * as React from 'react';
import { BGrid, type BGridColumn } from 'beautiful-grid';
import { createDateEditorPlugin, createSelectEditorPlugin } from 'beautiful-grid/editors';
import DataGridContainer from '../components/DataGridContainer';
import { useContainerSize } from '../hooks/useContainerSize';
import { CalendarIcon, ChevronDownIcon } from './editing/editorIcons';
import {
applyEditingDataChange,
cloneEditingOrders,
type EditingOrder,
withEditingCellClasses,
} from './editing/shared';
function formatDate(value: unknown) {
if (typeof value !== 'string') return '';
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
return match ? `${match[1]}.${match[2]}.${match[3]}` : value;
}
const statusEditor = createSelectEditorPlugin<EditingOrder, EditingOrder['status']>({
id: 'built-in-status',
ariaLabel: t('주문 상태 선택', 'Select Order Status'),
options: [
{ value: t('접수', 'Receipt'), label: t('접수', 'Receipt') },
{ value: t('진행', 'In Progress'), label: t('진행', 'In Progress') },
{ value: t('완료', 'Completed'), label: t('완료', 'Completed') },
],
});
const deliveryDateEditor = createDateEditorPlugin<EditingOrder>({
id: 'built-in-delivery-date',
ariaLabel: t('납기일 선택', 'Select Delivery Date'),
min: '2026-08-01',
max: '2026-12-31',
});
export default function BuiltInEditorsExample() {
const [data, setData] = React.useState(cloneEditingOrders);
const containerRef = React.useRef<HTMLDivElement>(null);
const { width, height } = useContainerSize(containerRef);
const columns = React.useMemo<BGridColumn<EditingOrder>[]>(
() => withEditingCellClasses<EditingOrder>([
{ key: 'orderCode', label: t('주문 코드', 'Order Code'), width: 145, editable: false },
{
key: 'customerName',
label: t('내장 text', 'Built-in text'),
width: 180,
editable: true,
editor: {
type: 'text',
inputProps: { maxLength: 50, autoComplete: 'off' },
},
},
{
key: 'status',
label: t('기본 Select', 'Default Select'),
width: 150,
editable: true,
editTrigger: 'click',
editor: statusEditor,
editorIcon: { render: <ChevronDownIcon />, ariaLabel: t('주문 상태 선택', 'Select Order Status'), visibility: 'always' },
},
{
key: 'deliveryDate',
label: t('기본 Date', 'Default Date'),
width: 170,
editable: true,
editTrigger: 'click',
editor: deliveryDateEditor,
itemRender: ({ value }) => formatDate(value),
editorIcon: { render: <CalendarIcon />, ariaLabel: t('납기일 선택', 'Select Delivery Date'), visibility: 'always' },
},
{
key: 'approved',
label: t('승인 권한', 'Approval Permission'),
width: 150,
align: 'center',
headerAlign: 'center',
editable: true,
editor: {
type: 'checkbox',
header: { ariaLabel: t('승인 권한 전체 선택', 'Select All Approval Permissions') },
ariaLabel: ({ values }) => `${values.orderCode} ${t('승인 권한', 'Approval Permission')}`,
label: ({ value }) => (value ? t('허용', 'Allowed') : t('차단', 'Blocked')),
},
},
]),
[],
);
return (
<div className='flex min-h-0 flex-col gap-3'>
<p className='m-0 rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm leading-6 text-slate-700'>
{t('text와 checkbox는 라이브러리 내장 편집기이며 Select와 Date는', 'Text and checkbox are built-in editors, while Select and Date are provided by')} <code>beautiful-grid/editors</code>{t('가 제공하는 의존성 없는 plugin입니다. 승인 권한 헤더의 checkbox로 현재 행을 한 번에 선택하거나 해제할 수 있습니다.', ' as dependency-free plugins. You can select or deselect all rows at once using the checkbox in the Approval Permission header.')}
</p>
<DataGridContainer ref={containerRef} style={{ height: 340 }}>
<BGrid<EditingOrder>
width={width}
height={height}
data={data}
columns={columns}
rowKey='id'
editable
variant='vertical-bordered'
onChangeData={(sourceIndex, _columnIndex, values, _column, meta) => {
setData(current => applyEditingDataChange(current, sourceIndex, values, meta));
}}
/>
</DataGridContainer>
</div>
);
}import * as React from 'react';
import './DataGridContainer.css';
interface DataGridContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
/**
* Keeps a DataGrid in a measured, fixed layout box.
*
* BGrid's rendered root is absolutely positioned within this relative
* container. This makes a ResizeObserver measurement authoritative when a
* surrounding flex or grid layout shrinks as well as when it expands.
*/
const DataGridContainer = React.forwardRef<HTMLDivElement, DataGridContainerProps>(
({ className, ...rest }, ref) => (
<div ref={ref} className={`data-grid-container ${className ?? ''}`.trim()} {...rest} />
),
);
DataGridContainer.displayName = 'DataGridContainer';
export default DataGridContainer;.data-grid-container {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
font-size: 13px;
}
.data-grid-container > .bgrid-root {
position: absolute;
inset: 0;
}import * as React from 'react';
export function useContainerSize(ref: React.MutableRefObject<HTMLElement | null>, additionalDeps: unknown[] = []) {
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
const resizeObserver = React.useRef(
new ResizeObserver(entries => {
if (entries.length !== 1) {
throw new Error('Invalid Container length');
}
const [entry] = entries;
const { width, height } = entry.contentRect;
setWidth(width);
setHeight(height);
}),
);
React.useEffect(() => {
if (!ref.current) return;
const observer = resizeObserver.current;
const element = ref.current;
setWidth(element.clientWidth);
setHeight(element.clientHeight);
observer.observe(element);
return () => {
observer.unobserve(element);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...additionalDeps, ref]);
return {
width,
height,
};
}import { t } from '../i18n';
import type { BGridChangeDataMeta, BGridColumn, BGridDataItem } from 'beautiful-grid';
import './editingExamples.css';
export interface EditingOrder {
id: string;
orderCode: string;
customerCode: string;
customerName: string;
customerGrade: string;
status: string;
deliveryDate: string;
approved: boolean;
quantity: number;
unitPrice: number;
amount: number;
note: string;
mergeGroup: string;
}
export const editingOrders: BGridDataItem<EditingOrder>[] = [
{
values: {
id: 'ORDER-001',
orderCode: 'ORD-2601',
customerCode: 'C001',
customerName: t('서울상사', 'Seoul Sangsa'),
customerGrade: 'VIP',
status: t('접수', 'Receipt'),
deliveryDate: '2026-08-25',
approved: true,
quantity: 2,
unitPrice: 12000,
amount: 24000,
note: t('오전 배송', 'Morning Delivery'),
mergeGroup: 'A',
},
},
{
values: {
id: 'ORDER-002',
orderCode: 'ORD-2602',
customerCode: 'C001',
customerName: t('서울상사', 'Seoul Sangsa'),
customerGrade: 'VIP',
status: t('진행', 'In Progress'),
deliveryDate: '2026-08-26',
approved: false,
quantity: 3,
unitPrice: 18000,
amount: 54000,
note: t('담당자 확인', 'Assignee Confirmation'),
mergeGroup: 'A',
},
},
{
values: {
id: 'ORDER-003',
orderCode: 'ORD-2603',
customerCode: 'C002',
customerName: t('한빛물산', 'Hanbit Mulsan'),
customerGrade: t('우수', 'Excellent'),
status: t('완료', 'Completed'),
deliveryDate: '2026-08-28',
approved: true,
quantity: 1,
unitPrice: 32000,
amount: 32000,
note: '',
mergeGroup: 'B',
},
},
{
values: {
id: 'ORDER-004',
orderCode: 'ORD-2604',
customerCode: 'C003',
customerName: 'Northwind',
customerGrade: t('일반', 'General'),
status: t('접수', 'Receipt'),
deliveryDate: '2026-09-01',
approved: false,
quantity: 5,
unitPrice: 9000,
amount: 45000,
note: t('영문 송장', 'English Invoice'),
mergeGroup: 'C',
},
},
];
export const cloneEditingOrders = (): BGridDataItem<EditingOrder>[] =>
editingOrders.map(item => ({
...item,
values: { ...item.values },
editedColumnIds: item.editedColumnIds ? [...item.editedColumnIds] : undefined,
changedKeys: item.changedKeys ? [...item.changedKeys] : undefined,
}));
export const applyEditingDataChange = <T,>(
current: BGridDataItem<T>[],
sourceIndex: number,
values: T,
meta?: BGridChangeDataMeta<T>,
): BGridDataItem<T>[] =>
current.map((item, index) =>
index === sourceIndex ? meta?.dataItem ?? { ...item, values } : item,
);
export const withEditingCellClasses = <T,>(columns: BGridColumn<T>[]): BGridColumn<T>[] =>
columns.map(column => ({
...column,
className: [
column.className,
column.editable === false ? 'editing-example-cell-readonly' : 'editing-example-cell-editable',
]
.filter(Boolean)
.join(' '),
}));.editing-example-row-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 34px;
padding: 6px 12px;
border: 1px solid var(--site-border-strong, #cbd5e1);
border-radius: var(--site-radius-sm, 8px);
background: var(--site-control-bg, #ffffff);
color: var(--site-text-secondary, #526178);
font: inherit;
font-size: 13px;
font-weight: 600;
line-height: 20px;
white-space: nowrap;
cursor: pointer;
transition: background-color 150ms, border-color 150ms, color 150ms;
}
.editing-example-row-button-add {
border-color: var(--site-accent, #2563eb);
background: var(--site-accent, #2563eb);
color: var(--site-accent-contrast, #ffffff);
}
.editing-example-row-button-add:hover {
border-color: var(--site-accent-strong, #1d4ed8);
background: var(--site-accent-strong, #1d4ed8);
}
.editing-example-row-button-delete:not(:disabled) {
border-color: #fca5a5;
color: #dc2626;
}
.editing-example-row-button-delete:hover:not(:disabled) {
border-color: #ef4444;
background: color-mix(in srgb, #ef4444 8%, var(--site-control-bg, #ffffff));
}
.editing-example-row-button:focus-visible {
outline: 2px solid var(--site-focus-ring, #93c5fd);
outline-offset: 2px;
}
.editing-example-row-button:disabled {
border-color: var(--site-border, #e1e7ef);
background: var(--site-surface, #f7f9fc);
color: var(--site-text-muted, #8491a5);
cursor: not-allowed;
}
.editing-example-cell-editable {
--editing-example-bg: #ffffff;
--editing-example-hover-bg: #dbeafe;
}
.editing-example-cell-readonly {
--editing-example-color: #525252;
--editing-example-bg: #f5f5f5;
--editing-example-hover-bg: #e5e5e5;
}
.bgrid-body-table
td:is(.editing-example-cell-editable, .editing-example-cell-readonly):not(:is(.bgrid-cell-selected, .bgrid-cell-edited, .bgrid-cell-value-changed, .bgrid-cell-editing)) {
color: var(--editing-example-color, inherit);
background-color: var(--editing-example-bg);
}
.bgrid-body-table tr.bgrid-row-hover
> td:is(.editing-example-cell-editable, .editing-example-cell-readonly):not(:is(.bgrid-cell-selected, .bgrid-cell-edited, .bgrid-cell-value-changed, .bgrid-cell-editing)) {
background-color: var(--editing-example-hover-bg);
}import * as React from 'react';
const iconProps = {
width: 14,
height: 14,
viewBox: '0 0 16 16',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 1.5,
strokeLinecap: 'round' as const,
strokeLinejoin: 'round' as const,
focusable: false,
'aria-hidden': true,
};
export function ChevronDownIcon() {
return (
<svg {...iconProps}>
<path d='m4 6 4 4 4-4' />
</svg>
);
}
export function CalendarIcon() {
return (
<svg {...iconProps}>
<rect x='2.5' y='3.5' width='11' height='10' rx='1.5' />
<path d='M5 2.5v2M11 2.5v2M2.5 6.5h11' />
</svg>
);
}
export function ClockIcon() {
return (
<svg {...iconProps}>
<circle cx='8' cy='8' r='5.5' />
<path d='M8 4.75V8l2.25 1.5' />
</svg>
);
}
export function SearchIcon() {
return (
<svg {...iconProps}>
<circle cx='7' cy='7' r='3.75' />
<path d='m10 10 3 3' />
</svg>
);
}
export function CheckIcon() {
return (
<svg {...iconProps}>
<path d='m3 8.25 3 3L13 4.5' />
</svg>
);
}import * as React from 'react';
import {
BGridCellClipboardParseParams,
BGridCellClipboardTextParams,
BGridEditorPluginProps,
BGridPluginEditorConfig,
} from '../types';
import { defineEditorPlugin } from './defineEditorPlugin';
import { getColumnId } from '../utils/getColumnId';
export interface BGridSelectEditorOption<Value extends string | number> {
value: Value;
label: React.ReactNode;
disabled?: boolean;
}
export interface BGridSelectEditorPluginOptions<Value extends string | number> {
id: string;
options: BGridSelectEditorOption<Value>[];
ariaLabel?: string;
placeholder?: string;
openOnMount?: boolean;
/**
* Whether to allow pasting values that do not match any option value or label.
* Defaults to false (unmatched values throw an error and report parseValueFailed).
*/
allowCustomValue?: boolean;
/**
* Clipboard text mode when copying cells using this editor.
* 'label' (default): copies the option's label if it is a string or number.
* 'value': copies the option's raw value.
*/
copyMode?: 'label' | 'value';
/**
* Whether to allow clearing cell values by pasting empty or whitespace-only text.
* Defaults to false (unmatched empty text throws parseValueFailed).
* When true, pastes `emptyValue` into the cell.
*/
allowEmpty?: boolean;
/**
* Value written when an empty text is pasted and `allowEmpty` is true.
* Defaults to undefined.
*/
emptyValue?: Value | null;
/**
* Whether to allow pasting options that have `disabled: true`.
* Defaults to false (pasting a disabled option throws an error).
*/
allowDisabledOptions?: boolean;
/**
* Custom clipboard parser for this editor.
* Throw an error to reject the paste for this cell.
*/
parseClipboardText?: <T>(text: string, params: BGridCellClipboardParseParams<T>) => Value | unknown;
/**
* Custom clipboard text getter for this editor.
*/
getClipboardText?: <T>(params: BGridCellClipboardTextParams<T>) => any;
}
export function createSelectEditorPlugin<T, Value extends string | number = string>(
options: BGridSelectEditorPluginOptions<Value>,
): BGridPluginEditorConfig<T> {
function SelectEditor({ value, column, commit, cancel }: BGridEditorPluginProps<T>) {
const selectedIndex = options.options.findIndex(option => Object.is(option.value, value));
const selectRef = React.useRef<HTMLSelectElement>(null);
const pickerOpenedRef = React.useRef(false);
React.useLayoutEffect(() => {
const select = selectRef.current as (HTMLSelectElement & { showPicker?: () => void }) | null;
if (!select) return;
select.focus({ preventScroll: true });
if (options.openOnMount === false || pickerOpenedRef.current || typeof select.showPicker !== 'function') return;
pickerOpenedRef.current = true;
try {
select.showPicker();
} catch {
// Some browsers require a transient user activation. The focused select remains usable.
}
}, []);
return (
<div className='bgrid-native-select-editor-shell'>
<select
ref={selectRef}
className='bgrid-native-select-editor'
aria-label={options.ariaLabel ?? '셀 선택 편집'}
defaultValue={selectedIndex >= 0 ? String(selectedIndex) : ''}
onChange={event => {
const option = options.options[Number(event.currentTarget.value)];
if (option) void commit([{ columnId: getColumnId(column), value: option.value }]);
}}
onKeyDown={event => {
if (event.key === 'Escape' || event.key === 'Esc') {
event.preventDefault();
cancel();
return;
}
if (event.key === 'Tab' || event.key === 'Enter') {
event.preventDefault();
if (event.currentTarget.value === '') {
cancel();
return;
}
const option = options.options[Number(event.currentTarget.value)];
if (option) {
void commit([{ columnId: getColumnId(column), value: option.value }], {
move: event.key === 'Tab' ? (event.shiftKey ? 'prev' : 'next') : undefined,
});
}
}
}}
>
{selectedIndex < 0 && (
<option value='' disabled>
{options.placeholder ?? '선택'}
</option>
)}
{options.options.map((option, index) => (
<option key={index} value={String(index)} disabled={option.disabled}>
{option.label}
</option>
))}
</select>
<span className='bgrid-native-select-editor-icon' aria-hidden='true'>
<svg width='14' height='14' viewBox='0 0 16 16' fill='none' focusable='false'>
<path d='m4 6 4 4 4-4' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' />
</svg>
</span>
</div>
);
}
SelectEditor.displayName = `BGridSelectEditor(${options.id})`;
// Precompute lookup maps for O(1) clipboard parsing and copying
const valueMap = new Map<string, BGridSelectEditorOption<Value>>();
const labelMap = new Map<string, BGridSelectEditorOption<Value>>();
const valueToLabelMap = new Map<unknown, string>();
const isNumericValue = options.options.some(opt => typeof opt.value === 'number');
for (const opt of options.options) {
const valKey = String(opt.value);
if (!valueMap.has(valKey)) {
valueMap.set(valKey, opt);
}
if (typeof opt.label === 'string' || typeof opt.label === 'number') {
const labelText = String(opt.label);
const trimmedLabel = labelText.trim();
if (!labelMap.has(trimmedLabel)) {
labelMap.set(trimmedLabel, opt);
}
if (!valueToLabelMap.has(opt.value)) {
valueToLabelMap.set(opt.value, labelText);
}
}
}
const defaultParseClipboardText = (text: string): Value | unknown => {
const trimmed = text.trim();
// Check empty input
if (trimmed === '') {
const emptyOption =
options.copyMode === 'value'
? (valueMap.get('') ?? labelMap.get(''))
: (labelMap.get('') ?? valueMap.get(''));
if (emptyOption) {
if (emptyOption.disabled && !options.allowDisabledOptions) {
throw new Error(`Disabled select option for editor "${options.id}"`);
}
return emptyOption.value;
}
if (options.allowEmpty) {
return options.emptyValue !== undefined ? options.emptyValue : ('' as unknown as Value);
}
}
// Match order depends on copyMode:
// If copyMode is 'label' (default), match by label first, then by value.
// If copyMode is 'value', match by value first, then by label.
const matched =
options.copyMode === 'value'
? (valueMap.get(trimmed) ?? labelMap.get(trimmed))
: (labelMap.get(trimmed) ?? valueMap.get(trimmed));
if (matched) {
if (matched.disabled && !options.allowDisabledOptions) {
throw new Error(`Disabled select option "${text}" for editor "${options.id}"`);
}
return matched.value;
}
if (options.allowCustomValue) {
if (isNumericValue) {
const num = Number(trimmed);
if (!Number.isNaN(num)) {
return num as unknown as Value;
}
}
return trimmed as unknown as Value;
}
throw new Error(`Invalid select value "${text}" for editor "${options.id}"`);
};
const defaultGetClipboardText = (params: BGridCellClipboardTextParams<T>): any => {
const { value } = params;
if (options.copyMode === 'value') {
return value;
}
const label = valueToLabelMap.get(value);
if (label !== undefined) {
return label;
}
return value;
};
return defineEditorPlugin<T>({
id: options.id,
component: SelectEditor,
parseClipboardText: options.parseClipboardText ?? ((text, _params) => defaultParseClipboardText(text)),
getClipboardText: options.getClipboardText ?? defaultGetClipboardText,
});
}import * as React from 'react';
import { BGridEditorPluginProps, BGridPluginEditorConfig } from '../types';
import { defineEditorPlugin } from './defineEditorPlugin';
import { getColumnId } from '../utils/getColumnId';
export interface BGridDateEditorPluginOptions {
id: string;
min?: string;
max?: string;
ariaLabel?: string;
}
export function createDateEditorPlugin<T>(options: BGridDateEditorPluginOptions): BGridPluginEditorConfig<T> {
function DateEditor({ value, column, activation, commit, cancel }: BGridEditorPluginProps<T>) {
const dateValue = typeof value === 'string' ? value : '';
const inputRef = React.useRef<HTMLInputElement>(null);
const pickerOpenedRef = React.useRef(false);
React.useLayoutEffect(() => {
const input = inputRef.current as (HTMLInputElement & { showPicker?: () => void }) | null;
if (!input) return;
input.focus({ preventScroll: true });
if (activation !== 'editorIcon' || pickerOpenedRef.current || typeof input.showPicker !== 'function') return;
pickerOpenedRef.current = true;
try {
input.showPicker();
} catch {
// Browsers without transient user activation keep the focused numeric date input usable.
}
}, [activation]);
return (
<div className='bgrid-native-date-editor-shell'>
<input
ref={inputRef}
className='bgrid-native-date-editor'
type='date'
aria-label={options.ariaLabel ?? '셀 날짜 편집'}
min={options.min}
max={options.max}
defaultValue={dateValue}
onChange={event =>
void commit([{ columnId: getColumnId(column), value: event.currentTarget.value }])
}
onKeyDown={event => {
if (event.key === 'Escape' || event.key === 'Esc') {
event.preventDefault();
cancel();
return;
}
if (event.key === 'Tab' || event.key === 'Enter') {
event.preventDefault();
void commit([{ columnId: getColumnId(column), value: event.currentTarget.value }], {
move: event.key === 'Tab' ? (event.shiftKey ? 'prev' : 'next') : undefined,
});
}
}}
/>
<span className='bgrid-native-date-editor-icon' aria-hidden='true'>
<svg
width='14'
height='14'
viewBox='0 0 16 16'
fill='none'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
focusable='false'
>
<rect x='2.5' y='3.5' width='11' height='10' rx='1.5' />
<path d='M5 2.5v2M11 2.5v2M2.5 6.5h11' />
</svg>
</span>
</div>
);
}
DateEditor.displayName = `BGridDateEditor(${options.id})`;
return defineEditorPlugin<T>({
id: options.id,
component: DateEditor,
});
}Use the built-in text and checkbox editors for free-form input and boolean or permission toggles. Use the standard plugins from beautiful-grid/editors for selecting predefined values and dates. These plugins add no dependency on an external UI framework.
Checkbox and bulk header control
{
key: 'approved',
label: 'Approval',
width: 150,
align: 'center',
editable: true,
editor: {
type: 'checkbox',
header: { ariaLabel: 'Toggle all approvals' },
ariaLabel: ({ values }) => `Approval for ${values.orderCode}`,
label: ({ value }) => (value ? 'Allowed' : 'Blocked'),
disabled: ({ values }) => values.locked,
},
}
With header enabled, the header checkbox selects or clears every eligible row and reports an indeterminate state when only some rows are checked. It operates on the rows currently available to the Grid after filtering or paging. Disabled cells and removed rows are excluded from both the state calculation and the bulk update.
The default stored values are true and false. Map another domain model with options such as trueValue: 'Y' and falseValue: 'N'. Space, Enter, or F2 toggles an active checkbox cell. Cell and header changes pass through onChangeValue, then call onChangeData for each changed row with meta.source set to 'checkbox'.
Text
{
key: 'quantity',
editable: true,
editor: {
type: 'text',
inputProps: { inputMode: 'numeric', autoComplete: 'off' },
formatValue: value => String(value ?? ''),
parseValue: text => {
const value = Number(text);
if (!Number.isFinite(value)) throw new Error('Enter a number.');
return value;
},
},
}
If parseValue throws, the Grid does not save the value. It keeps the editor open and sets aria-invalid="true". With commitOnBlur: false, moving focus outside the editor cancels the edit instead of saving it.
Select and Date
const statusEditor = createSelectEditorPlugin<Order, Order['status']>({
id: 'order-status',
options: [
{ value: 'ready', label: 'Ready' },
{ value: 'done', label: 'Completed' },
],
});
const dateEditor = createDateEditorPlugin<Order>({
id: 'delivery-date',
min: '2026-01-01',
max: '2026-12-31',
});
Create each factory result once, either outside the component or inside useMemo. Creating a new plugin object on every column render can remount the input component.
The standard Select opens its native option picker as soon as the editor mounts after a cell or icon click. Set openOnMount: false in the factory options to disable this automatic opening behavior.
The standard Date editor activates only the numeric date input when entered through the cell body. It opens the native calendar picker only when entered through editorIcon. An editor plugin can distinguish these entry paths through the activation value ('cell' | 'editorIcon').
{
key: 'status',
editable: true,
editTrigger: 'click',
editor: statusEditor,
editorIcon: { render: <ChevronDownIcon />, ariaLabel: 'Select status' },
}
The Grid does not infer an icon from the editor type. Provide an icon that matches your product design system through editorIcon.render.
Use your application’s UI library
Choose an official UI integration when you need richer pickers or controls that match the rest of your application. BeautifulGrid provides editor plugins for Ant Design, Shadcn UI, MUI, and Mantine, with popup portals and save and cancel lifecycles already connected.