@ornery/ui-grid-react 0.1.10 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/UiGrid.d.ts +6 -5
- package/dist/UiGrid.d.ts.map +1 -1
- package/dist/gridStateMath.d.ts +2 -2
- package/dist/gridStateMath.d.ts.map +1 -1
- package/dist/index.d.ts +2 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +159 -2142
- package/dist/index.mjs +153 -2229
- package/dist/mountUiGrid.d.ts +2 -0
- package/dist/mountUiGrid.d.ts.map +1 -1
- package/dist/ui-grid.css +48 -15
- package/dist/useGridState.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/UiGrid.test.tsx +64 -235
- package/src/UiGrid.tsx +178 -699
- package/src/gridStateMath.test.ts +3 -3
- package/src/gridStateMath.ts +7 -4
- package/src/index.ts +2 -18
- package/src/mountUiGrid.tsx +26 -0
- package/src/test-setup.ts +13 -0
- package/src/ui-grid.css +48 -15
- package/src/useGridState.ts +8 -7
- package/tsconfig.dts.json +3 -2
- package/tsconfig.json +2 -1
- package/vitest.config.ts +2 -0
package/src/UiGrid.tsx
CHANGED
|
@@ -1,750 +1,229 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
import type {
|
|
3
4
|
GridOptions,
|
|
4
5
|
GridCellTemplateContext,
|
|
5
|
-
|
|
6
|
-
GridHeaderTemplateContext,
|
|
6
|
+
GridRecord,
|
|
7
7
|
UiGridApi,
|
|
8
|
-
GridColumnDef,
|
|
9
|
-
GridRow,
|
|
10
8
|
} from '@ornery/ui-grid-core';
|
|
11
|
-
import type {
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
import type {
|
|
10
|
+
FrameworkCellSlot,
|
|
11
|
+
FrameworkSlotDelta,
|
|
12
|
+
UiGridStandaloneElement,
|
|
13
|
+
} from '@ornery/ui-grid-vanilla';
|
|
14
|
+
import { defineStandaloneUiGridElement } from '@ornery/ui-grid-vanilla';
|
|
15
|
+
|
|
16
|
+
export interface UiGridCellRenderers {
|
|
17
|
+
[columnName: string]: (context: GridCellTemplateContext) => React.ReactNode;
|
|
18
|
+
}
|
|
14
19
|
|
|
15
20
|
export interface UiGridProps {
|
|
16
21
|
options: GridOptions;
|
|
17
22
|
onRegisterApi?: (api: UiGridApi) => void;
|
|
18
|
-
|
|
19
|
-
headerRenderer?: (context: GridHeaderTemplateContext) => React.ReactNode;
|
|
20
|
-
expandableRenderer?: (context: GridExpandableTemplateContext) => React.ReactNode;
|
|
23
|
+
cellRenderers?: UiGridCellRenderers;
|
|
21
24
|
className?: string;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}: UiGridProps) {
|
|
32
|
-
const state = useGridState(options, onRegisterApi);
|
|
33
|
-
|
|
34
|
-
const {
|
|
35
|
-
pipeline,
|
|
36
|
-
visibleColumns,
|
|
37
|
-
labels,
|
|
38
|
-
gridTemplateColumns,
|
|
39
|
-
gridContainerRef,
|
|
40
|
-
displayItems,
|
|
41
|
-
virtualizationEnabled,
|
|
42
|
-
rowSize,
|
|
43
|
-
editingValue,
|
|
44
|
-
autoViewportHeight,
|
|
45
|
-
sortingFeature,
|
|
46
|
-
filteringFeature,
|
|
47
|
-
groupingFeature,
|
|
48
|
-
paginationFeature,
|
|
49
|
-
cellEditFeature,
|
|
50
|
-
expandableFeature,
|
|
51
|
-
treeViewFeature,
|
|
52
|
-
columnMovingFeature,
|
|
53
|
-
paginationCurrentPage,
|
|
54
|
-
paginationTotalPages,
|
|
55
|
-
paginationSelectedPageSize,
|
|
56
|
-
} = state;
|
|
57
|
-
|
|
58
|
-
const headerGridRef = React.useRef<HTMLDivElement | null>(null);
|
|
59
|
-
const filterGridRef = React.useRef<HTMLDivElement | null>(null);
|
|
60
|
-
const [headerStickyHeight, setHeaderStickyHeight] = React.useState(0);
|
|
61
|
-
const [filterStickyHeight, setFilterStickyHeight] = React.useState(0);
|
|
62
|
-
const stickyChromeHeight = headerStickyHeight + filterStickyHeight;
|
|
63
|
-
// Prefer the explicit viewportHeight, otherwise fall back to the container
|
|
64
|
-
// height measured by the autoresize observer so the grid fills its parent
|
|
65
|
-
// by default. The 560 fallback only applies before the first measurement.
|
|
66
|
-
const resolvedViewportHeight =
|
|
67
|
-
options.viewportHeight ?? (autoViewportHeight && autoViewportHeight > 0 ? autoViewportHeight : 560);
|
|
68
|
-
const bodyViewportHeight = Math.max(rowSize, resolvedViewportHeight - stickyChromeHeight);
|
|
69
|
-
|
|
70
|
-
const virtualScroll = useVirtualScroll({
|
|
71
|
-
itemCount: displayItems.length,
|
|
72
|
-
itemSize: rowSize,
|
|
73
|
-
viewportHeight: bodyViewportHeight,
|
|
74
|
-
overscan: 3,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const [openPinMenuColumn, setOpenPinMenuColumn] = React.useState<string | null>(null);
|
|
78
|
-
const [draggedColumnName, setDraggedColumnName] = React.useState<string | null>(null);
|
|
79
|
-
const [dropTargetColumnName, setDropTargetColumnName] = React.useState<string | null>(null);
|
|
80
|
-
const scrollContainerHeight = `${resolvedViewportHeight}px`;
|
|
81
|
-
|
|
82
|
-
function renderHeaderContent(column: GridColumnDef): React.ReactNode {
|
|
83
|
-
const value = state.headerLabel(column);
|
|
84
|
-
const context: GridHeaderTemplateContext = {
|
|
85
|
-
$implicit: value,
|
|
86
|
-
value,
|
|
87
|
-
column,
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
if (headerRenderer) {
|
|
91
|
-
return headerRenderer(context) ?? value;
|
|
92
|
-
}
|
|
27
|
+
interface SlotEntry {
|
|
28
|
+
slotName: string;
|
|
29
|
+
columnName: string;
|
|
30
|
+
rowId: string;
|
|
31
|
+
context: GridCellTemplateContext;
|
|
32
|
+
wrapper: HTMLSpanElement;
|
|
33
|
+
}
|
|
93
34
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
35
|
+
const TAG_NAME = 'ui-grid-element';
|
|
36
|
+
let definePromise: Promise<void> | null = null;
|
|
37
|
+
|
|
38
|
+
export function UiGrid({ options, onRegisterApi, cellRenderers, className }: UiGridProps) {
|
|
39
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
40
|
+
const elementRef = React.useRef<UiGridStandaloneElement | null>(null);
|
|
41
|
+
const [slots, setSlots] = React.useState<Map<string, SlotEntry>>(new Map());
|
|
42
|
+
const cellRenderersRef = React.useRef(cellRenderers);
|
|
43
|
+
cellRenderersRef.current = cellRenderers;
|
|
44
|
+
const onRegisterApiRef = React.useRef(onRegisterApi);
|
|
45
|
+
onRegisterApiRef.current = onRegisterApi;
|
|
46
|
+
const optionsRef = React.useRef(options);
|
|
47
|
+
optionsRef.current = options;
|
|
48
|
+
const currentSlotColumnsRef = React.useRef<string[]>([]);
|
|
49
|
+
|
|
50
|
+
// Mount the vanilla element once
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
const container = containerRef.current;
|
|
53
|
+
if (!container) return;
|
|
97
54
|
|
|
98
|
-
|
|
99
|
-
|
|
55
|
+
let el: UiGridStandaloneElement | null = null;
|
|
56
|
+
let disposed = false;
|
|
100
57
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
? event.composedPath()
|
|
105
|
-
: event.target
|
|
106
|
-
? [event.target]
|
|
107
|
-
: [];
|
|
108
|
-
|
|
109
|
-
return eventPath.some((target) => {
|
|
110
|
-
if (!target || typeof target !== 'object' || !('classList' in target)) {
|
|
111
|
-
return false;
|
|
58
|
+
const mount = async () => {
|
|
59
|
+
if (!definePromise) {
|
|
60
|
+
definePromise = defineStandaloneUiGridElement(TAG_NAME);
|
|
112
61
|
}
|
|
62
|
+
await definePromise;
|
|
63
|
+
if (disposed) return;
|
|
64
|
+
|
|
65
|
+
el = document.createElement(TAG_NAME) as UiGridStandaloneElement;
|
|
66
|
+
el.style.display = 'block';
|
|
67
|
+
el.style.height = '100%';
|
|
68
|
+
el.style.minHeight = '0';
|
|
69
|
+
elementRef.current = el;
|
|
70
|
+
container.appendChild(el);
|
|
71
|
+
|
|
72
|
+
el.addEventListener('cellSlotsChanged', handleCellSlotsChanged);
|
|
73
|
+
applyOptions(el, optionsRef.current);
|
|
74
|
+
};
|
|
113
75
|
|
|
114
|
-
|
|
115
|
-
return classList?.contains(className) ?? false;
|
|
116
|
-
});
|
|
117
|
-
}, []);
|
|
118
|
-
|
|
119
|
-
const isPinMenuOpen = React.useCallback(
|
|
120
|
-
(column: GridColumnDef) => openPinMenuColumn === column.name,
|
|
121
|
-
[openPinMenuColumn],
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
const pinButtonLabel = React.useCallback(
|
|
125
|
-
(column: GridColumnDef) => (state.isPinned(column) ? labels.unpin : labels.pinColumn),
|
|
126
|
-
[labels, state],
|
|
127
|
-
);
|
|
76
|
+
void mount();
|
|
128
77
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return;
|
|
78
|
+
return () => {
|
|
79
|
+
disposed = true;
|
|
80
|
+
if (el) {
|
|
81
|
+
el.removeEventListener('cellSlotsChanged', handleCellSlotsChanged);
|
|
82
|
+
el.remove();
|
|
83
|
+
elementRef.current = null;
|
|
136
84
|
}
|
|
85
|
+
setSlots(new Map());
|
|
86
|
+
};
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
|
+
}, []);
|
|
137
89
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
event?.stopPropagation();
|
|
146
|
-
setOpenPinMenuColumn(null);
|
|
147
|
-
state.gridApi.pinning.pinColumn(column.name, direction);
|
|
148
|
-
},
|
|
149
|
-
[state],
|
|
150
|
-
);
|
|
90
|
+
// Update options when they change
|
|
91
|
+
React.useEffect(() => {
|
|
92
|
+
const el = elementRef.current;
|
|
93
|
+
if (!el) return;
|
|
94
|
+
applyOptions(el, options);
|
|
95
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
|
+
}, [options]);
|
|
151
97
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
event.preventDefault();
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
98
|
+
// Update existing slot contexts when data changes (same rows, new values)
|
|
99
|
+
React.useEffect(() => {
|
|
100
|
+
if (slots.size === 0 || !options.data) return;
|
|
158
101
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
[columnMovingFeature],
|
|
165
|
-
);
|
|
102
|
+
const dataById = new Map<string, GridRecord>();
|
|
103
|
+
for (const row of options.data) {
|
|
104
|
+
const id = String(row['id'] ?? '');
|
|
105
|
+
if (id) dataById.set(id, row);
|
|
106
|
+
}
|
|
166
107
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
108
|
+
let changed = false;
|
|
109
|
+
const nextSlots = new Map(slots);
|
|
110
|
+
for (const [key, entry] of nextSlots) {
|
|
111
|
+
const row = dataById.get(entry.rowId);
|
|
112
|
+
if (!row) continue;
|
|
113
|
+
|
|
114
|
+
const col = options.columnDefs?.find((c) => c.name === entry.columnName);
|
|
115
|
+
const value = col?.field ? getNestedValue(row, col.field) : row[entry.columnName];
|
|
116
|
+
|
|
117
|
+
if (entry.context.value !== value || entry.context.row !== row) {
|
|
118
|
+
nextSlots.set(key, {
|
|
119
|
+
...entry,
|
|
120
|
+
context: { ...entry.context, $implicit: value, value, row },
|
|
121
|
+
});
|
|
122
|
+
changed = true;
|
|
171
123
|
}
|
|
124
|
+
}
|
|
172
125
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
},
|
|
177
|
-
[columnMovingFeature, draggedColumnName],
|
|
178
|
-
);
|
|
126
|
+
if (changed) setSlots(nextSlots);
|
|
127
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
128
|
+
}, [options.data]);
|
|
179
129
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
130
|
+
function applyOptions(el: UiGridStandaloneElement, opts: GridOptions) {
|
|
131
|
+
const renderers = cellRenderersRef.current;
|
|
132
|
+
const cellSlotColumns: string[] = [];
|
|
183
133
|
|
|
184
|
-
|
|
185
|
-
|
|
134
|
+
if (renderers && opts.columnDefs) {
|
|
135
|
+
for (const col of opts.columnDefs) {
|
|
136
|
+
if (renderers[col.name]) {
|
|
137
|
+
cellSlotColumns.push(col.name);
|
|
138
|
+
}
|
|
186
139
|
}
|
|
187
|
-
|
|
188
|
-
const sourceColumnName = draggedColumnName ?? event.dataTransfer.getData('text/plain');
|
|
189
|
-
setDraggedColumnName(null);
|
|
190
|
-
setDropTargetColumnName(null);
|
|
191
|
-
|
|
192
|
-
if (!sourceColumnName || sourceColumnName === column.name) {
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
state.moveVisibleColumn(sourceColumnName, column.name);
|
|
197
|
-
},
|
|
198
|
-
[columnMovingFeature, draggedColumnName, state],
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
const handleHeaderDragEnd = React.useCallback(() => {
|
|
202
|
-
setDraggedColumnName(null);
|
|
203
|
-
setDropTargetColumnName(null);
|
|
204
|
-
}, []);
|
|
205
|
-
|
|
206
|
-
React.useLayoutEffect(() => {
|
|
207
|
-
setHeaderStickyHeight(headerGridRef.current?.offsetHeight ?? 0);
|
|
208
|
-
setFilterStickyHeight(filterGridRef.current?.offsetHeight ?? 0);
|
|
209
|
-
}, [visibleColumns, filteringFeature, options.enableFiltering]);
|
|
210
|
-
|
|
211
|
-
React.useLayoutEffect(() => {
|
|
212
|
-
const headerElement = headerGridRef.current;
|
|
213
|
-
const filterElement = filterGridRef.current;
|
|
214
|
-
if (typeof ResizeObserver === 'undefined' || (!headerElement && !filterElement)) {
|
|
215
|
-
return;
|
|
216
140
|
}
|
|
217
141
|
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
142
|
+
const wrappedOptions: GridOptions = {
|
|
143
|
+
...opts,
|
|
144
|
+
onRegisterApi: (api) => {
|
|
145
|
+
onRegisterApiRef.current?.(api as UiGridApi);
|
|
146
|
+
opts.onRegisterApi?.(api);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
222
149
|
|
|
223
|
-
|
|
224
|
-
observer.observe(headerElement);
|
|
225
|
-
}
|
|
226
|
-
if (filterElement) {
|
|
227
|
-
observer.observe(filterElement);
|
|
228
|
-
}
|
|
150
|
+
el.options = wrappedOptions;
|
|
229
151
|
|
|
230
|
-
|
|
231
|
-
|
|
152
|
+
const prev = currentSlotColumnsRef.current;
|
|
153
|
+
const columnsChanged =
|
|
154
|
+
cellSlotColumns.length !== prev.length ||
|
|
155
|
+
cellSlotColumns.some((name, i) => name !== prev[i]);
|
|
232
156
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
157
|
+
if (columnsChanged) {
|
|
158
|
+
currentSlotColumnsRef.current = cellSlotColumns;
|
|
159
|
+
el.setFrameworkRenderedSlots({ cells: cellSlotColumns });
|
|
236
160
|
}
|
|
161
|
+
}
|
|
237
162
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
163
|
+
function handleCellSlotsChanged(event: Event) {
|
|
164
|
+
const detail = (event as CustomEvent<FrameworkSlotDelta<FrameworkCellSlot>>).detail;
|
|
165
|
+
const el = elementRef.current;
|
|
166
|
+
if (!el) return;
|
|
242
167
|
|
|
243
|
-
|
|
244
|
-
|
|
168
|
+
setSlots((prev) => {
|
|
169
|
+
const next = new Map(prev);
|
|
245
170
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
171
|
+
for (const slot of detail.removed) {
|
|
172
|
+
const entry = next.get(slot.slotName);
|
|
173
|
+
if (entry) {
|
|
174
|
+
entry.wrapper.remove();
|
|
175
|
+
next.delete(slot.slotName);
|
|
176
|
+
}
|
|
249
177
|
}
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
document.addEventListener('click', handleDocumentClick);
|
|
253
|
-
document.addEventListener('keydown', handleDocumentEscape);
|
|
254
|
-
|
|
255
|
-
return () => {
|
|
256
|
-
document.removeEventListener('click', handleDocumentClick);
|
|
257
|
-
document.removeEventListener('keydown', handleDocumentEscape);
|
|
258
|
-
};
|
|
259
|
-
}, [eventPathIncludesClass, openPinMenuColumn]);
|
|
260
|
-
|
|
261
|
-
const itemsToRender = virtualizationEnabled
|
|
262
|
-
? displayItems.slice(virtualScroll.visibleRange.start, virtualScroll.visibleRange.end)
|
|
263
|
-
: displayItems;
|
|
264
|
-
|
|
265
|
-
const onGridTableScroll = (event: React.UIEvent<HTMLDivElement>) => {
|
|
266
|
-
const bodyScrollTop = Math.max(0, event.currentTarget.scrollTop - stickyChromeHeight);
|
|
267
|
-
virtualScroll.setScrollTop(bodyScrollTop);
|
|
268
|
-
const startIndex = Math.floor(bodyScrollTop / rowSize);
|
|
269
|
-
state.onViewportScroll(startIndex);
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
function renderDisplayItem(item: DisplayItem) {
|
|
273
|
-
if (groupingFeature && state.isGroupItem(item)) {
|
|
274
|
-
return (
|
|
275
|
-
<button
|
|
276
|
-
key={item.id}
|
|
277
|
-
type="button"
|
|
278
|
-
className="group-row ui-grid-row ui-grid-group-row"
|
|
279
|
-
data-part="group-row"
|
|
280
|
-
role="row"
|
|
281
|
-
aria-expanded={!item.collapsed}
|
|
282
|
-
style={{ gridColumn: '1 / -1', paddingInlineStart: `${item.depth * 1.25 + 1}rem` }}
|
|
283
|
-
onClick={() => state.toggleGroup(item)}
|
|
284
|
-
>
|
|
285
|
-
<strong>
|
|
286
|
-
{item.field}: {item.label}
|
|
287
|
-
</strong>
|
|
288
|
-
<span>
|
|
289
|
-
{item.count} {labels.groupRowsSuffix}
|
|
290
|
-
</span>
|
|
291
|
-
<svg
|
|
292
|
-
className="toggle-icon group-disclosure-icon"
|
|
293
|
-
viewBox="0 0 24 24"
|
|
294
|
-
aria-hidden="true"
|
|
295
|
-
focusable={false}
|
|
296
|
-
>
|
|
297
|
-
<path d={item.collapsed ? 'M10 7l5 5-5 5z' : 'M7 10l5 5 5-5z'} />
|
|
298
|
-
</svg>
|
|
299
|
-
<span className="sr-only ui-grid-sr-only">{state.groupDisclosureLabel(item)}</span>
|
|
300
|
-
</button>
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
178
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
179
|
+
for (const slot of detail.added) {
|
|
180
|
+
const existing = next.get(slot.slotName);
|
|
181
|
+
if (existing) {
|
|
182
|
+
existing.wrapper.remove();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const wrapper = document.createElement('span');
|
|
186
|
+
wrapper.setAttribute('slot', slot.slotName);
|
|
187
|
+
el.appendChild(wrapper);
|
|
188
|
+
|
|
189
|
+
next.set(slot.slotName, {
|
|
190
|
+
slotName: slot.slotName,
|
|
191
|
+
columnName: slot.columnName,
|
|
192
|
+
rowId: slot.rowId,
|
|
193
|
+
context: slot.context,
|
|
194
|
+
wrapper,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
317
197
|
|
|
318
|
-
|
|
319
|
-
const rowItem = item as RowItem;
|
|
320
|
-
|
|
321
|
-
return visibleColumns.map((column) => {
|
|
322
|
-
const pinned = state.isPinned(column);
|
|
323
|
-
const pinOffset = pinned ? state.pinnedOffset(column) : null;
|
|
324
|
-
return (
|
|
325
|
-
<div
|
|
326
|
-
key={`${rowItem.row.id}-${column.name}`}
|
|
327
|
-
className={`${cellClassName(rowItem, column)}${pinned ? ' is-pinned' : ''}`}
|
|
328
|
-
data-part="body-cell"
|
|
329
|
-
role="gridcell"
|
|
330
|
-
tabIndex={0}
|
|
331
|
-
data-row-id={rowItem.row.id}
|
|
332
|
-
data-col-name={column.name}
|
|
333
|
-
onFocus={() => state.focusCell(rowItem.row, column)}
|
|
334
|
-
onClick={() => state.focusCell(rowItem.row, column)}
|
|
335
|
-
onDoubleClick={(e) => state.handleCellDoubleClick(rowItem.row, column, e)}
|
|
336
|
-
onKeyDown={(e) => state.handleCellKeyDown(rowItem.row, column, e)}
|
|
337
|
-
style={{
|
|
338
|
-
position: pinned ? 'sticky' : undefined,
|
|
339
|
-
left: pinOffset?.side === 'left' ? pinOffset.offset : undefined,
|
|
340
|
-
right: pinOffset?.side === 'right' ? pinOffset.offset : undefined,
|
|
341
|
-
zIndex: pinned ? 2 : undefined,
|
|
342
|
-
}}
|
|
343
|
-
>
|
|
344
|
-
<div
|
|
345
|
-
className="cell-shell"
|
|
346
|
-
style={{ paddingInlineStart: state.cellIndent(rowItem.row, column) }}
|
|
347
|
-
>
|
|
348
|
-
{treeViewFeature && state.showTreeToggle(rowItem.row, column) && (
|
|
349
|
-
<button
|
|
350
|
-
type="button"
|
|
351
|
-
className="row-toggle row-toggle-tree"
|
|
352
|
-
data-part="tree-toggle"
|
|
353
|
-
aria-label={state.treeToggleLabel(rowItem.row)}
|
|
354
|
-
aria-expanded={state.isTreeRowExpanded(rowItem.row)}
|
|
355
|
-
onClick={(e) => state.toggleTreeRow(rowItem.row, e)}
|
|
356
|
-
>
|
|
357
|
-
<svg
|
|
358
|
-
className="toggle-icon"
|
|
359
|
-
viewBox="0 0 24 24"
|
|
360
|
-
aria-hidden="true"
|
|
361
|
-
focusable={false}
|
|
362
|
-
>
|
|
363
|
-
<path
|
|
364
|
-
d={state.isTreeRowExpanded(rowItem.row) ? 'M7 10l5 5 5-5z' : 'M10 7l5 5-5 5z'}
|
|
365
|
-
/>
|
|
366
|
-
</svg>
|
|
367
|
-
</button>
|
|
368
|
-
)}
|
|
369
|
-
{expandableFeature && state.showExpandToggle(rowItem.row, column) && (
|
|
370
|
-
<button
|
|
371
|
-
type="button"
|
|
372
|
-
className="row-toggle row-toggle-expand"
|
|
373
|
-
data-part="expand-toggle"
|
|
374
|
-
aria-label={state.expandToggleLabel(rowItem.row)}
|
|
375
|
-
aria-expanded={rowItem.row.expanded}
|
|
376
|
-
onClick={(e) => state.toggleRowExpansion(rowItem.row, e)}
|
|
377
|
-
>
|
|
378
|
-
<svg
|
|
379
|
-
className="toggle-icon"
|
|
380
|
-
viewBox="0 0 24 24"
|
|
381
|
-
aria-hidden="true"
|
|
382
|
-
focusable={false}
|
|
383
|
-
>
|
|
384
|
-
<path d={rowItem.row.expanded ? 'M7 10l5 5 5-5z' : 'M10 7l5 5-5 5z'} />
|
|
385
|
-
</svg>
|
|
386
|
-
</button>
|
|
387
|
-
)}
|
|
388
|
-
<span className="cell-value">
|
|
389
|
-
{cellEditFeature && state.isEditingCell(rowItem.row, column) ? (
|
|
390
|
-
<input
|
|
391
|
-
className="cell-editor"
|
|
392
|
-
data-row-id={rowItem.row.id}
|
|
393
|
-
data-col-name={column.name}
|
|
394
|
-
aria-label={state.headerLabel(column)}
|
|
395
|
-
type={state.editorInputType(column)}
|
|
396
|
-
defaultValue={editingValue}
|
|
397
|
-
onChange={(e) => state.updateEditingValue(e.target.value)}
|
|
398
|
-
onKeyDown={(e) => state.handleEditorKeyDown(e)}
|
|
399
|
-
onBlur={(e) => state.handleEditorBlur(e)}
|
|
400
|
-
/>
|
|
401
|
-
) : cellRenderer ? (
|
|
402
|
-
(cellRenderer(state.cellContext(rowItem.row, column)) ??
|
|
403
|
-
state.displayValue(rowItem.row, column))
|
|
404
|
-
) : (
|
|
405
|
-
state.displayValue(rowItem.row, column)
|
|
406
|
-
)}
|
|
407
|
-
</span>
|
|
408
|
-
</div>
|
|
409
|
-
</div>
|
|
410
|
-
);
|
|
198
|
+
return next;
|
|
411
199
|
});
|
|
412
200
|
}
|
|
413
201
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function renderSortIcon(column: GridColumnDef) {
|
|
426
|
-
const direction = state.sortDirection(column);
|
|
427
|
-
switch (direction) {
|
|
428
|
-
case 'asc':
|
|
429
|
-
return (
|
|
430
|
-
<svg viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
431
|
-
<path d="M12 5l-6 6h4v8h4v-8h4z" />
|
|
432
|
-
</svg>
|
|
433
|
-
);
|
|
434
|
-
case 'desc':
|
|
435
|
-
return (
|
|
436
|
-
<svg viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
437
|
-
<path d="M12 19l6-6h-4V5h-4v8H6z" />
|
|
438
|
-
</svg>
|
|
439
|
-
);
|
|
440
|
-
default:
|
|
441
|
-
return (
|
|
442
|
-
<svg viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
443
|
-
<path d="M7 6h10v2H7V6Zm0 5h7v2H7v-2Zm0 5h4v2H7v-2Z" />
|
|
444
|
-
</svg>
|
|
445
|
-
);
|
|
202
|
+
// Render React portals into the slot wrappers
|
|
203
|
+
const portals: React.ReactNode[] = [];
|
|
204
|
+
const renderers = cellRenderers;
|
|
205
|
+
if (renderers) {
|
|
206
|
+
for (const [, entry] of slots) {
|
|
207
|
+
const renderer = renderers[entry.columnName];
|
|
208
|
+
if (renderer) {
|
|
209
|
+
portals.push(createPortal(renderer(entry.context), entry.wrapper, entry.slotName));
|
|
210
|
+
}
|
|
446
211
|
}
|
|
447
212
|
}
|
|
448
213
|
|
|
449
214
|
return (
|
|
450
|
-
<div
|
|
451
|
-
|
|
452
|
-
className="grid-frame ui-grid"
|
|
453
|
-
data-part="grid-frame"
|
|
454
|
-
role="grid"
|
|
455
|
-
aria-label={options.title ?? 'Data grid'}
|
|
456
|
-
>
|
|
457
|
-
<div
|
|
458
|
-
className="grid-table ui-grid-contents-wrapper"
|
|
459
|
-
data-part="grid-table"
|
|
460
|
-
style={
|
|
461
|
-
virtualizationEnabled ? { height: scrollContainerHeight, overflowY: 'auto' } : undefined
|
|
462
|
-
}
|
|
463
|
-
onScroll={virtualizationEnabled ? onGridTableScroll : undefined}
|
|
464
|
-
>
|
|
465
|
-
<div
|
|
466
|
-
className="header-grid ui-grid-header ui-grid-header-canvas"
|
|
467
|
-
data-part="header"
|
|
468
|
-
role="row"
|
|
469
|
-
ref={headerGridRef}
|
|
470
|
-
style={{ gridTemplateColumns }}
|
|
471
|
-
>
|
|
472
|
-
{visibleColumns.map((column) => {
|
|
473
|
-
const pinned = state.isPinned(column);
|
|
474
|
-
const pinOffset = pinned ? state.pinnedOffset(column) : null;
|
|
475
|
-
const pinMenuOpen = isPinMenuOpen(column);
|
|
476
|
-
return (
|
|
477
|
-
<div
|
|
478
|
-
key={column.name}
|
|
479
|
-
className={`header-cell ui-grid-header-cell${sortingFeature && state.sortDirection(column) !== 'none' ? ' is-active' : ''}${pinned ? ' is-pinned' : ''}${pinMenuOpen ? ' is-pin-menu-open' : ''}${draggedColumnName === column.name ? ' is-dragging' : ''}${dropTargetColumnName === column.name ? ' is-drag-target' : ''}`}
|
|
480
|
-
data-part="header-cell"
|
|
481
|
-
data-col-name={column.name}
|
|
482
|
-
aria-sort={sortingFeature ? (state.sortAriaSort(column) as any) : undefined}
|
|
483
|
-
draggable={columnMovingFeature}
|
|
484
|
-
onDragStart={(event) => handleHeaderDragStart(column, event)}
|
|
485
|
-
onDragOver={(event) => handleHeaderDragOver(column, event)}
|
|
486
|
-
onDrop={(event) => handleHeaderDrop(column, event)}
|
|
487
|
-
onDragEnd={handleHeaderDragEnd}
|
|
488
|
-
onDragLeave={() => {
|
|
489
|
-
if (dropTargetColumnName === column.name) {
|
|
490
|
-
setDropTargetColumnName(null);
|
|
491
|
-
}
|
|
492
|
-
}}
|
|
493
|
-
style={{
|
|
494
|
-
position: pinned ? 'sticky' : undefined,
|
|
495
|
-
left: pinOffset?.side === 'left' ? pinOffset.offset : undefined,
|
|
496
|
-
right: pinOffset?.side === 'right' ? pinOffset.offset : undefined,
|
|
497
|
-
zIndex: pinMenuOpen ? 8 : pinned ? 2 : undefined,
|
|
498
|
-
}}
|
|
499
|
-
>
|
|
500
|
-
<span className="header-label">{renderHeaderContent(column)}</span>
|
|
501
|
-
|
|
502
|
-
<div className="header-actions">
|
|
503
|
-
{sortingFeature && (
|
|
504
|
-
<button
|
|
505
|
-
type="button"
|
|
506
|
-
className={`header-action${!state.isColumnSortable(column) ? ' header-action-disabled' : ''}`}
|
|
507
|
-
disabled={!state.isColumnSortable(column)}
|
|
508
|
-
aria-label={state.sortButtonLabel(column)}
|
|
509
|
-
title={state.sortButtonLabel(column)}
|
|
510
|
-
onClick={() => state.toggleSort(column)}
|
|
511
|
-
>
|
|
512
|
-
{renderSortIcon(column)}
|
|
513
|
-
<span className="sr-only ui-grid-sr-only">
|
|
514
|
-
{state.sortButtonLabel(column)}
|
|
515
|
-
</span>
|
|
516
|
-
</button>
|
|
517
|
-
)}
|
|
518
|
-
|
|
519
|
-
{groupingFeature &&
|
|
520
|
-
state.isGroupingEnabled() &&
|
|
521
|
-
column.enableGrouping !== false && (
|
|
522
|
-
<button
|
|
523
|
-
type="button"
|
|
524
|
-
className={`chip-action${state.isGrouped(column) ? ' chip-action-active' : ''}`}
|
|
525
|
-
data-part="group-toggle"
|
|
526
|
-
aria-label={state.groupingButtonLabel(column)}
|
|
527
|
-
title={state.groupingButtonLabel(column)}
|
|
528
|
-
onClick={(e) => state.toggleGrouping(column, e)}
|
|
529
|
-
>
|
|
530
|
-
<svg viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
531
|
-
<path d="M4 6h8v4H4V6Zm0 8h8v4H4v-4Zm10-8h6v4h-6V6Zm0 8h6v4h-6v-4Z" />
|
|
532
|
-
</svg>
|
|
533
|
-
<span className="sr-only ui-grid-sr-only">
|
|
534
|
-
{state.groupingButtonLabel(column)}
|
|
535
|
-
</span>
|
|
536
|
-
</button>
|
|
537
|
-
)}
|
|
538
|
-
|
|
539
|
-
{state.pinningFeature &&
|
|
540
|
-
state.isPinningEnabled() &&
|
|
541
|
-
state.isColumnPinnable(column) && (
|
|
542
|
-
<div
|
|
543
|
-
className={`pin-control${pinMenuOpen ? ' pin-control-open' : ''}`}
|
|
544
|
-
onClick={(event) => event.stopPropagation()}
|
|
545
|
-
>
|
|
546
|
-
<button
|
|
547
|
-
type="button"
|
|
548
|
-
className={`chip-action pin-trigger${pinned || pinMenuOpen ? ' chip-action-active' : ''}`}
|
|
549
|
-
data-part="pin-toggle"
|
|
550
|
-
aria-label={pinButtonLabel(column)}
|
|
551
|
-
title={pinButtonLabel(column)}
|
|
552
|
-
aria-haspopup={pinned ? undefined : 'menu'}
|
|
553
|
-
aria-expanded={pinned ? undefined : pinMenuOpen}
|
|
554
|
-
onClick={(event) => onPinTrigger(column, event)}
|
|
555
|
-
>
|
|
556
|
-
<svg viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
557
|
-
<path d="M16 12V4h1V2H7v2h1v8l-2 2v2h5v6l1 1 1-1v-6h5v-2l-2-2z" />
|
|
558
|
-
</svg>
|
|
559
|
-
<span className="sr-only ui-grid-sr-only">{pinButtonLabel(column)}</span>
|
|
560
|
-
</button>
|
|
561
|
-
|
|
562
|
-
<div
|
|
563
|
-
className="pin-menu"
|
|
564
|
-
data-part="pin-menu"
|
|
565
|
-
role="menu"
|
|
566
|
-
aria-label="Pin options"
|
|
567
|
-
aria-hidden={!pinMenuOpen}
|
|
568
|
-
>
|
|
569
|
-
<button
|
|
570
|
-
type="button"
|
|
571
|
-
className="pin-menu-action"
|
|
572
|
-
data-part="pin-left-action"
|
|
573
|
-
role="menuitem"
|
|
574
|
-
aria-label={labels.pinLeft}
|
|
575
|
-
title={labels.pinLeft}
|
|
576
|
-
tabIndex={pinMenuOpen ? 0 : -1}
|
|
577
|
-
onClick={(event) => choosePinDirection(column, 'left', event)}
|
|
578
|
-
>
|
|
579
|
-
<svg viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
580
|
-
<path d="M10 6 4 12l6 6v-4h10v-4H10V6z" />
|
|
581
|
-
</svg>
|
|
582
|
-
<span className="sr-only ui-grid-sr-only">{labels.pinLeft}</span>
|
|
583
|
-
</button>
|
|
584
|
-
<button
|
|
585
|
-
type="button"
|
|
586
|
-
className="pin-menu-action"
|
|
587
|
-
data-part="pin-right-action"
|
|
588
|
-
role="menuitem"
|
|
589
|
-
aria-label={labels.pinRight}
|
|
590
|
-
title={labels.pinRight}
|
|
591
|
-
tabIndex={pinMenuOpen ? 0 : -1}
|
|
592
|
-
onClick={(event) => choosePinDirection(column, 'right', event)}
|
|
593
|
-
>
|
|
594
|
-
<svg viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
595
|
-
<path d="M14 6v4H4v4h10v4l6-6-6-6z" />
|
|
596
|
-
</svg>
|
|
597
|
-
<span className="sr-only ui-grid-sr-only">{labels.pinRight}</span>
|
|
598
|
-
</button>
|
|
599
|
-
</div>
|
|
600
|
-
</div>
|
|
601
|
-
)}
|
|
602
|
-
</div>
|
|
603
|
-
|
|
604
|
-
{state.canResizeColumns() && (
|
|
605
|
-
<button
|
|
606
|
-
type="button"
|
|
607
|
-
className="column-resizer"
|
|
608
|
-
data-col-name={column.name}
|
|
609
|
-
aria-label={`Resize ${state.headerLabel(column)} column`}
|
|
610
|
-
title="Drag to resize, double-click to auto fit"
|
|
611
|
-
onMouseDown={(event) => state.handleHeaderResizeMouseDown(column, event)}
|
|
612
|
-
onDoubleClick={(event) => state.autoSizeColumn(column, event)}
|
|
613
|
-
/>
|
|
614
|
-
)}
|
|
615
|
-
</div>
|
|
616
|
-
);
|
|
617
|
-
})}
|
|
618
|
-
</div>
|
|
619
|
-
|
|
620
|
-
{filteringFeature && state.isFilteringEnabled() && (
|
|
621
|
-
<div
|
|
622
|
-
className="filter-grid ui-grid-header"
|
|
623
|
-
data-part="filters"
|
|
624
|
-
ref={filterGridRef}
|
|
625
|
-
style={{
|
|
626
|
-
gridTemplateColumns,
|
|
627
|
-
['--ui-grid-header-sticky-top' as string]: `${headerStickyHeight}px`,
|
|
628
|
-
}}
|
|
629
|
-
>
|
|
630
|
-
{visibleColumns.map((column) => {
|
|
631
|
-
const pinned = state.isPinned(column);
|
|
632
|
-
const pinOffset = pinned ? state.pinnedOffset(column) : null;
|
|
633
|
-
return (
|
|
634
|
-
<label
|
|
635
|
-
key={column.name}
|
|
636
|
-
className={`filter-cell ui-grid-filter-container${pinned ? ' is-pinned' : ''}`}
|
|
637
|
-
data-part="filter-cell"
|
|
638
|
-
style={{
|
|
639
|
-
position: pinned ? 'sticky' : undefined,
|
|
640
|
-
left: pinOffset?.side === 'left' ? pinOffset.offset : undefined,
|
|
641
|
-
right: pinOffset?.side === 'right' ? pinOffset.offset : undefined,
|
|
642
|
-
zIndex: pinned ? 2 : undefined,
|
|
643
|
-
}}
|
|
644
|
-
>
|
|
645
|
-
<span className="sr-only ui-grid-sr-only">
|
|
646
|
-
{labels.filterColumn} {state.headerLabel(column)}
|
|
647
|
-
</span>
|
|
648
|
-
<input
|
|
649
|
-
className="ui-grid-filter-input"
|
|
650
|
-
type="text"
|
|
651
|
-
defaultValue={state.filterValue(column.name)}
|
|
652
|
-
placeholder={state.filterPlaceholder(column)}
|
|
653
|
-
disabled={state.isFilterInputDisabled(column)}
|
|
654
|
-
onChange={(e) => state.updateFilter(column.name, e.target.value)}
|
|
655
|
-
/>
|
|
656
|
-
</label>
|
|
657
|
-
);
|
|
658
|
-
})}
|
|
659
|
-
</div>
|
|
660
|
-
)}
|
|
661
|
-
|
|
662
|
-
{displayItems.length > 0 ? (
|
|
663
|
-
virtualizationEnabled ? (
|
|
664
|
-
<div className="grid-virtual-spacer" style={{ height: `${virtualScroll.totalHeight}px` }}>
|
|
665
|
-
<div
|
|
666
|
-
className="body-grid ui-grid-canvas grid-virtual-body"
|
|
667
|
-
data-part="body"
|
|
668
|
-
role="rowgroup"
|
|
669
|
-
style={{
|
|
670
|
-
gridTemplateColumns,
|
|
671
|
-
position: 'absolute',
|
|
672
|
-
top: `${virtualScroll.offsetY}px`,
|
|
673
|
-
left: 0,
|
|
674
|
-
}}
|
|
675
|
-
>
|
|
676
|
-
{itemsToRender.map(renderDisplayItem)}
|
|
677
|
-
</div>
|
|
678
|
-
</div>
|
|
679
|
-
) : (
|
|
680
|
-
<div className="body-grid ui-grid-canvas" data-part="body" role="rowgroup" style={{ gridTemplateColumns }}>
|
|
681
|
-
{displayItems.map(renderDisplayItem)}
|
|
682
|
-
</div>
|
|
683
|
-
)
|
|
684
|
-
) : (
|
|
685
|
-
<div className="empty-state ui-grid-no-row-overlay" data-part="empty-state">
|
|
686
|
-
<strong>{options.emptyMessage ?? labels.emptyHeading}</strong>
|
|
687
|
-
<p>{labels.emptyDescription}</p>
|
|
688
|
-
</div>
|
|
689
|
-
)}
|
|
690
|
-
</div>
|
|
691
|
-
|
|
692
|
-
{paginationFeature && state.showPaginationControls() && (
|
|
693
|
-
<footer
|
|
694
|
-
className="pagination-bar ui-grid-pagination"
|
|
695
|
-
data-part="pagination"
|
|
696
|
-
role="navigation"
|
|
697
|
-
aria-label={labels.paginationPage}
|
|
698
|
-
>
|
|
699
|
-
<p>{state.paginationSummary()}</p>
|
|
700
|
-
<div className="pagination-controls">
|
|
701
|
-
<button
|
|
702
|
-
type="button"
|
|
703
|
-
className="action action-secondary pagination-button"
|
|
704
|
-
aria-label={labels.paginationPrevious}
|
|
705
|
-
disabled={paginationCurrentPage <= 1}
|
|
706
|
-
onClick={() => state.previousPage()}
|
|
707
|
-
>
|
|
708
|
-
<svg className="pagination-icon" viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
709
|
-
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
|
|
710
|
-
</svg>
|
|
711
|
-
<span className="sr-only">{labels.paginationPrevious}</span>
|
|
712
|
-
</button>
|
|
713
|
-
<span>
|
|
714
|
-
{labels.paginationPage} {paginationCurrentPage} {labels.paginationOf} {paginationTotalPages}
|
|
715
|
-
</span>
|
|
716
|
-
<button
|
|
717
|
-
type="button"
|
|
718
|
-
className="action action-secondary pagination-button"
|
|
719
|
-
aria-label={labels.paginationNext}
|
|
720
|
-
disabled={paginationCurrentPage >= paginationTotalPages}
|
|
721
|
-
onClick={() => state.nextPage()}
|
|
722
|
-
>
|
|
723
|
-
<svg className="pagination-icon" viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
724
|
-
<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
|
|
725
|
-
</svg>
|
|
726
|
-
<span className="sr-only">{labels.paginationNext}</span>
|
|
727
|
-
</button>
|
|
728
|
-
{state.pageSizeOptions().length > 0 && (
|
|
729
|
-
<label className="pagination-size">
|
|
730
|
-
<span className="sr-only">{labels.paginationRows}</span>
|
|
731
|
-
<select
|
|
732
|
-
aria-label={labels.paginationRows}
|
|
733
|
-
value={paginationSelectedPageSize}
|
|
734
|
-
onChange={(e) => state.onPageSizeChange(e.target.value)}
|
|
735
|
-
>
|
|
736
|
-
{state.pageSizeOptions().map((size) => (
|
|
737
|
-
<option key={size} value={size}>
|
|
738
|
-
{size}
|
|
739
|
-
</option>
|
|
740
|
-
))}
|
|
741
|
-
</select>
|
|
742
|
-
</label>
|
|
743
|
-
)}
|
|
744
|
-
</div>
|
|
745
|
-
</footer>
|
|
746
|
-
)}
|
|
747
|
-
</section>
|
|
215
|
+
<div ref={containerRef} className={className} style={{ display: 'block', height: '100%', minHeight: 0 }}>
|
|
216
|
+
{portals}
|
|
748
217
|
</div>
|
|
749
218
|
);
|
|
750
219
|
}
|
|
220
|
+
|
|
221
|
+
function getNestedValue(obj: GridRecord, field: string): unknown {
|
|
222
|
+
const parts = field.split('.');
|
|
223
|
+
let current: unknown = obj;
|
|
224
|
+
for (const part of parts) {
|
|
225
|
+
if (current == null || typeof current !== 'object') return undefined;
|
|
226
|
+
current = (current as Record<string, unknown>)[part];
|
|
227
|
+
}
|
|
228
|
+
return current;
|
|
229
|
+
}
|