Ant Design Editor Plugins
Connect Ant Design or application-specific inputs as editor plugins and manage popup portals, multi-value commits, and the editor lifecycle.
import { t } from './i18n';
import * as React from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';
import {
createAntdCascaderEditorPlugin,
createAntdColorPickerEditorPlugin,
createAntdDatePickerEditorPlugin,
createAntdSelectEditorPlugin,
createAntdTimePickerEditorPlugin,
createAntdTreeSelectEditorPlugin,
formatCascaderClipboardText,
parseCascaderClipboardText,
} from '@beautifuljs/grid-antd';
import '@beautifuljs/grid-antd/style.css';
import DataGridContainer from '../components/DataGridContainer';
import { useContainerSize } from '../hooks/useContainerSize';
import { CalendarIcon, ChevronDownIcon, ClockIcon } from './editing/editorIcons';
import {
applyEditingDataChange,
cloneEditingOrders,
type EditingOrder,
withEditingCellClasses,
} from './editing/shared';
type ExternalEditorOrder = EditingOrder & {
labelColor: string;
categoryPath: string[];
deliveryTime: string;
organization: string;
};
const antdStatusEditor = createAntdSelectEditorPlugin<ExternalEditorOrder, EditingOrder['status']>({
id: 'external-antd-status',
ariaLabel: t('Ant Design 주문 상태 선택', 'Ant Design 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 antdDeliveryDateEditor = createAntdDatePickerEditorPlugin<ExternalEditorOrder>({
id: 'external-antd-delivery-date',
ariaLabel: t('Ant Design 납기일 선택', 'Ant Design Select Delivery Date'),
});
const antdLabelColorEditor = createAntdColorPickerEditorPlugin<ExternalEditorOrder>({
id: 'external-antd-label-color',
ariaLabel: t('Ant Design 라벨 색상 선택', 'Ant Design Select Label Color'),
});
const antdCategoryEditor = createAntdCascaderEditorPlugin<ExternalEditorOrder>({
id: 'external-antd-category',
ariaLabel: t('Ant Design 분류 경로 선택', 'Ant Design Select Category Path'),
options: [
{
value: t('국내', 'Domestic'),
label: t('국내', 'Domestic'),
children: [
{ value: t('서울', 'Seoul'), label: t('서울', 'Seoul') },
{ value: t('부산', 'Busan'), label: t('부산', 'Busan') },
],
},
{
value: t('해외', 'Overseas'),
label: t('해외', 'Overseas'),
children: [
{ value: t('아시아', 'Asia'), label: t('아시아', 'Asia') },
{ value: t('유럽', 'Europe'), label: t('유럽', 'Europe') },
],
},
],
});
const antdDeliveryTimeEditor = createAntdTimePickerEditorPlugin<ExternalEditorOrder>({
id: 'external-antd-delivery-time',
ariaLabel: t('Ant Design 배송 시간 선택', 'Ant Design Select Delivery Time'),
});
const antdOrganizationEditor = createAntdTreeSelectEditorPlugin<ExternalEditorOrder>({
id: 'external-antd-organization',
ariaLabel: t('Ant Design 담당 조직 선택', 'Ant Design Select Responsible Organization'),
treeData: [
{
value: t('영업본부', 'Sales Headquarters'),
title: t('영업본부', 'Sales Headquarters'),
children: [
{ value: t('서울 영업팀', 'Seoul Sales Team'), title: t('서울 영업팀', 'Seoul Sales Team') },
{ value: t('부산 영업팀', 'Busan Sales Team'), title: t('부산 영업팀', 'Busan Sales Team') },
],
},
{
value: t('운영본부', 'Operations Headquarters'),
title: t('운영본부', 'Operations Headquarters'),
children: [
{ value: t('물류팀', 'Logistics Team'), title: t('물류팀', 'Logistics Team') },
{ value: t('고객지원팀', 'Customer Support Team'), title: t('고객지원팀', 'Customer Support Team') },
],
},
],
});
const initialColors = ['#1677FF', '#13C2C2', '#52C41A', '#FA8C16'];
const initialCategoryPaths = [
[t('국내', 'Domestic'), t('서울', 'Seoul')],
[t('국내', 'Domestic'), t('부산', 'Busan')],
[t('해외', 'Overseas'), t('아시아', 'Asia')],
[t('해외', 'Overseas'), t('유럽', 'Europe')],
];
const initialDeliveryTimes = ['09:30', '11:00', '14:30', '16:00'];
const initialOrganizations = [
t('서울 영업팀', 'Seoul Sales Team'),
t('부산 영업팀', 'Busan Sales Team'),
t('물류팀', 'Logistics Team'),
t('고객지원팀', 'Customer Support Team'),
];
const cloneExternalEditorOrders = (): BGridDataItem<ExternalEditorOrder>[] =>
cloneEditingOrders().map((item, index) => ({
...item,
values: {
...item.values,
labelColor: initialColors[index],
categoryPath: initialCategoryPaths[index],
deliveryTime: initialDeliveryTimes[index],
organization: initialOrganizations[index],
},
}));
export default function ExternalEditorPluginExample() {
const [data, setData] = React.useState(cloneExternalEditorOrders);
const containerRef = React.useRef<HTMLDivElement>(null);
const { width, height } = useContainerSize(containerRef);
const columns = React.useMemo<BGridColumn<ExternalEditorOrder>[]>(
() =>
withEditingCellClasses<ExternalEditorOrder>([
{ key: 'orderCode', label: t('주문 코드', 'Order Code'), width: 140, editable: false },
{ key: 'customerName', label: t('고객명', 'Customer Name'), width: 160, editable: false },
{
key: 'status',
label: 'Ant Design Select',
width: 180,
editable: true,
editor: antdStatusEditor,
editorIcon: { render: <ChevronDownIcon />, ariaLabel: t('Ant Design 상태 선택', 'Ant Design Select Status') },
},
{
key: 'deliveryDate',
label: 'Ant Design DatePicker',
width: 200,
editable: true,
editor: antdDeliveryDateEditor,
editorIcon: {
render: <CalendarIcon />,
ariaLabel: t('Ant Design 납기일 선택', 'Ant Design Select Delivery Date'),
},
},
{
key: 'labelColor',
label: 'Ant Design ColorPicker',
width: 210,
editable: true,
editor: antdLabelColorEditor,
itemRender: ({ value }) => <>{String(value ?? '')}</>,
editorIcon: {
render: ({ value }) => (
<span
className='bgrid-color-swatch'
style={{ backgroundColor: typeof value === 'string' ? value : 'transparent' }}
aria-hidden='true'
/>
),
ariaLabel: t('Ant Design 라벨 색상 선택', 'Ant Design Select Label Color'),
},
},
{
key: 'categoryPath',
label: 'Ant Design Cascader',
width: 200,
editable: true,
editor: antdCategoryEditor,
itemRender: ({ value }) => <>{Array.isArray(value) ? value.join(' / ') : ''}</>,
getClipboardText: ({ value }) => formatCascaderClipboardText(value),
parseClipboardText: parseCascaderClipboardText,
editorIcon: {
render: <ChevronDownIcon />,
ariaLabel: t('Ant Design 분류 경로 선택', 'Ant Design Select Category Path'),
},
},
{
key: 'deliveryTime',
label: 'Ant Design TimePicker',
width: 190,
editable: true,
editor: antdDeliveryTimeEditor,
editorIcon: {
render: <ClockIcon />,
ariaLabel: t('Ant Design 배송 시간 선택', 'Ant Design Select Delivery Time'),
},
},
{
key: 'organization',
label: 'Ant Design TreeSelect',
width: 210,
editable: true,
editor: antdOrganizationEditor,
editorIcon: {
render: <ChevronDownIcon />,
ariaLabel: t('Ant Design 담당 조직 선택', 'Ant Design Select Responsible Organization'),
},
},
]),
[],
);
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 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300'>
{t(
'Ant Design Select, DatePicker, ColorPicker, Cascader, TimePicker, TreeSelect를',
'Ant Design Select, DatePicker, ColorPicker, Cascader, TimePicker, TreeSelect connected via',
)}{' '}
<code>@beautifuljs/grid-antd</code>
{t(
' 패키지로 연결했습니다. 셀을 더블클릭하거나 각 아이콘과 ColorPicker 색상 박스를 클릭해 편집을 시작합니다. popup은 plugin의',
" package. Double-click a cell or click each icon and ColorPicker color box to start editing. The popup is rendered in the plugin's",
)}{' '}
<code>getPortalContainer()</code>
{t('에 렌더링하고 값 선택 시', ' and upon value selection,')} <code>commit(changes[])</code>
{t('을 호출합니다.', ' is called.')}
</p>
<DataGridContainer ref={containerRef} style={{ height: 340 }}>
<BGrid<ExternalEditorOrder>
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>
);
}Connect Ant Design Select, DatePicker, ColorPicker, Cascader, TimePicker, and TreeSelect with the official @beautifuljs/grid-antd integration package. Extend application-specific inputs and asynchronous autocompletes directly with defineEditorPlugin(). If you only need text, basic Select, or Date editing, start with Built-in Editors.
Use the official Ant Design package
npm install @beautifuljs/grid-antd antd
Import the integration stylesheet once in your application entry point, then use the factories you need.
import { createAntdSelectEditorPlugin, createAntdDatePickerEditorPlugin } from '@beautifuljs/grid-antd';
import '@beautifuljs/grid-antd/style.css';
const priorityEditor = createAntdSelectEditorPlugin<Task, Task['priority']>({
id: 'task-priority',
ariaLabel: 'Edit priority',
options: priorityOptions,
});
const dueDateEditor = createAntdDatePickerEditorPlugin<Task>({
id: 'task-due-date',
ariaLabel: 'Edit due date',
});
The package provides all six editor factories and Cascader clipboard conversion helpers. See the live example below for a complete integration.
Define a custom plugin
function PriorityEditor({ value, column, commit, cancel, getPortalContainer }: BGridEditorPluginProps<Task>) {
return (
<Select
autoFocus
open
defaultValue={value as Task['priority']}
getPopupContainer={getPortalContainer}
options={priorityOptions}
onChange={nextValue => void commit([{ key: column.key, value: nextValue }])}
onKeyDown={event => {
if (event.key === 'Escape') cancel();
}}
/>
);
}
const priorityEditor = defineEditorPlugin<Task>({
id: 'task-priority',
component: PriorityEditor,
});
Pass even a single value to commit as a change array of length 1. A cell value can itself be an array, so the API deliberately avoids an ambiguous commit(value) form.
Connect DatePicker and ColorPicker
Convert dates to your application’s storage format before committing them. For example, if you store a dayjs value as a YYYY-MM-DD string, connect the picker as follows.
<DatePicker
autoFocus
open
defaultValue={value ? dayjs(String(value)) : null}
getPopupContainer={getPortalContainer}
onChange={date =>
void commit([
{
key: column.key,
value: date ? date.format('YYYY-MM-DD') : '',
},
])
}
onOpenChange={open => {
if (!open) cancel();
}}
/>
With ColorPicker, use onChange only to preview the value while dragging, then save the final color from onChangeComplete when the interaction ends.
<ColorPicker
open
defaultValue={String(value)}
disabledAlpha
getPopupContainer={getPortalContainer}
onChange={(_color, css) => setPreviewColor(css)}
onChangeComplete={color =>
void commit([
{
key: column.key,
value: color.toHexString().toUpperCase(),
},
])
}
/>
Connect Cascader, TimePicker, and TreeSelect
Cascader commits the entire selected path as string[], not just the last item. TimePicker uses needConfirm so editing does not end while the user is choosing an hour and minute; convert the value to your application’s storage format in onOk. TreeSelect stores the selected node’s value directly.
<Cascader
open
defaultValue={value as string[]}
options={categoryOptions}
getPopupContainer={getPortalContainer}
onChange={path =>
void commit([{
key: column.key,
value: Array.from(path, String),
}])
}
/>
<TimePicker
open
needConfirm
defaultValue={dayjs(String(value), 'HH:mm')}
format='HH:mm'
getPopupContainer={getPortalContainer}
onOk={time =>
void commit([{
key: column.key,
value: time ? time.format('HH:mm') : '',
}])
}
/>
<TreeSelect
open
defaultValue={String(value)}
treeData={organizationTree}
getPopupContainer={getPortalContainer}
onChange={nodeValue =>
void commit([{
key: column.key,
value: nodeValue,
}])
}
/>
All six adapters in @beautifuljs/grid-antd inherit the cell’s font, color, and height. When building a custom adapter for a UI library that specifies its own font size, apply font: inherit to the editor root and selected-value element. Also pass --bgrid-font-family and --bgrid-font-size to the popup so the cell remains visually consistent before and after activation.
Convert values for copy and paste
The clipboard carries tab- and newline-delimited text/plain, not the editor’s React value. Neither the cell’s itemRender nor the editor’s defaultValue participates in clipboard conversion. A non-string cell such as a Cascader that stores string[] must therefore define both directions of the column’s clipboard contract.
const categoryColumn: BGridColumn<Order> = {
key: 'categoryPath',
label: 'Category',
width: 200,
editable: true,
editor: categoryEditor,
itemRender: ({ value }) => <>{Array.isArray(value) ? value.join(' / ') : ''}</>,
getClipboardText: ({ value }) => JSON.stringify(value),
parseClipboardText: text => {
const parsed: unknown = JSON.parse(text);
if (!Array.isArray(parsed) || !parsed.every(segment => typeof segment === 'string')) {
throw new TypeError('Category path must be a JSON string array.');
}
return parsed;
},
};
The example formats the cell as Domestic / Seoul but copies a lossless ["Domestic","Seoul"]. Paste restores a string[], so Ant Design Cascader receives the same path through defaultValue. If you prefer a human-readable path such as Domestic / Seoul on the clipboard, define escaping and validation for path values that may themselves contain /.
Conversion follows this order:
- Copy uses the column’s
getClipboardText. Without it, strings remain unchanged, numbers and booleans become strings,Datebecomes an ISO string, and arrays or objects are JSON-serialized. - Paste first uses the column’s
parseClipboardText. It applies to every editable column, regardless of whether the editor is text, checkbox, or plugin based. - If the column has no parser and its built-in text editor defines
parseValue, the Grid uses that existing parser. - Without either parser, the Grid stores the clipboard string as-is. A structured value can then become a string and disappear from the editor’s selected-value display.
Validate each domain explicitly: finite numbers with Number.isFinite(Number(text)), booleans through an allowed token map (true/false or Y/N), dates in the application’s storage format, and enums against the option list. JSON plus shape validation is recommended for arrays and objects. If the parser throws, that cell remains unchanged and cellSelectionOptions.onPasteError receives parseValueFailed. The second parseClipboardText argument exposes the current value, row values, item, index, columnIndex, column, and original text.
Save multiple columns at once
When an autocomplete resolves both a code and a name, send both changes in one request.
await commit([
{ key: 'customerCode', value: selected.code },
{ key: 'customerName', value: selected.name },
]);
If a target key or columnId is missing or ambiguous, the entire commit is rejected without a partial save.
Plugin props
value,item,values,column,index,columnIndex: context for the current logical cellcommit(changes, options?): save the change list and end the sessioncancel(): keep the original value and end the sessionmove(direction): move to the specified cell without savingsessionId: identifies the session associated with an asynchronous callbackgetPortalContainer(): returns the Grid-specific floating portal root for popup UI
Popup and session-ending rules
Rendering a popup directly in the UI library’s default document.body portal can make the Grid treat popup interaction as an outside click. Rendering it inside the Grid DOM can instead clip a large picker at the container’s overflow: hidden boundary. getPortalContainer() returns a Grid-tracked floating portal directly under document.body, so connect it whenever the external component supports a custom portal. This portal copies the Grid theme variables and participates in frozen/scroll position calculations and outside-click detection.
Use only one of commit, cancel, or move as the final action for a session. The library honors only the first completion request, so a cancel() triggered by blur immediately after selection cannot overwrite a successful save. If asynchronous validation fails and the commit() Promise rejects, the editor remains open so the user can correct the value and try again.
await commit(changes, { move: 'next' });
Do not move DOM focus manually after saving or canceling. The Grid restores focus to the active cell.