@crispy-seed/data-table 0.11.0 → 0.11.1
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/index.cjs +162 -49
- package/dist/index.d.ts +50 -47
- package/dist/index.js +162 -49
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -987,15 +987,33 @@ function sameCell$1(a, id, key) {
|
|
|
987
987
|
* first captured) nor keeps a ghost entry for one that was removed. Known keys keep
|
|
988
988
|
* their position from `order`; any key in `keys` missing from `order` is appended, in
|
|
989
989
|
* `keys`' own order; any entry in `order` no longer in `keys` is dropped.
|
|
990
|
-
|
|
990
|
+
*
|
|
991
|
+
* `actionsKey` (the styled layer's `actions`-flagged column, e.g. the row-menu `⋯`
|
|
992
|
+
* column) is kept LAST no matter what -- without this, a brand-new column added to
|
|
993
|
+
* `columns` after the table first mounted got appended past it (the "any key in `keys`
|
|
994
|
+
* missing from `order` is appended" rule above has no notion of "past the end" vs. "at
|
|
995
|
+
* the very end"), stranding it in the middle of the table since `stickyLast`/`actions`
|
|
996
|
+
* only sticks the actual trailing column. A real consumer hit this: their newly-added
|
|
997
|
+
* column landed to the right of the already-pinned-sticky `actions` column, covered by
|
|
998
|
+
* it under horizontal scroll. Column-layout's own `pinnedCellProps` independently
|
|
999
|
+
* targets the `actions` column by FLAG rather than by position (belt and suspenders --
|
|
1000
|
+
* see that file's own doc comment), but this still keeps the VISUAL order sane so nothing
|
|
1001
|
+
* renders hidden behind the sticky cell in the first place.
|
|
1002
|
+
*/ function reconcileColumnOrder(order, keys, actionsKey) {
|
|
991
1003
|
const keySet = new Set(keys);
|
|
992
1004
|
const known = order.filter((key)=>keySet.has(key));
|
|
993
1005
|
const knownSet = new Set(known);
|
|
994
1006
|
const added = keys.filter((key)=>!knownSet.has(key));
|
|
995
|
-
|
|
1007
|
+
const merged = [
|
|
996
1008
|
...known,
|
|
997
1009
|
...added
|
|
998
1010
|
];
|
|
1011
|
+
if (actionsKey === undefined || !keySet.has(actionsKey)) return merged;
|
|
1012
|
+
const withoutActions = merged.filter((key)=>key !== actionsKey);
|
|
1013
|
+
return [
|
|
1014
|
+
...withoutActions,
|
|
1015
|
+
actionsKey
|
|
1016
|
+
];
|
|
999
1017
|
}
|
|
1000
1018
|
/**
|
|
1001
1019
|
* Moves `key` one step toward `direction` in `order` -- a no-op (same array, well,
|
|
@@ -1174,7 +1192,7 @@ const EMPTY_RECORD = {};
|
|
|
1174
1192
|
* state this hook already owns, not independent state
|
|
1175
1193
|
* of their own.
|
|
1176
1194
|
*/ function useDataTableColumns(props) {
|
|
1177
|
-
const { columnKeys = EMPTY_STRINGS, columnOrder: columnOrderProp, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys: hiddenProp, defaultHiddenColumnKeys = EMPTY_STRINGS, onHiddenColumnKeysChange, columnWidths: widthsProp, defaultColumnWidths = EMPTY_RECORD, onColumnWidthsChange, columnLabels: labelsProp, defaultColumnLabels = EMPTY_RECORD, onColumnLabelsChange, pinnedColumnKeys: pinnedProp, defaultPinnedColumnKeys = EMPTY_STRINGS, onPinnedColumnKeysChange } = props;
|
|
1195
|
+
const { columnKeys = EMPTY_STRINGS, actionsColumnKey, columnOrder: columnOrderProp, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys: hiddenProp, defaultHiddenColumnKeys = EMPTY_STRINGS, onHiddenColumnKeysChange, columnWidths: widthsProp, defaultColumnWidths = EMPTY_RECORD, onColumnWidthsChange, columnLabels: labelsProp, defaultColumnLabels = EMPTY_RECORD, onColumnLabelsChange, pinnedColumnKeys: pinnedProp, defaultPinnedColumnKeys = EMPTY_STRINGS, onPinnedColumnKeysChange } = props;
|
|
1178
1196
|
const [rawOrder, setRawOrder] = useControllableState$1({
|
|
1179
1197
|
prop: columnOrderProp,
|
|
1180
1198
|
defaultProp: defaultColumnOrder ?? columnKeys,
|
|
@@ -1185,9 +1203,10 @@ const EMPTY_RECORD = {};
|
|
|
1185
1203
|
// consumer persisted before a new column existed must still show that new column
|
|
1186
1204
|
// (appended), and one holding a column that got removed from `columns` must not
|
|
1187
1205
|
// render a ghost header for it.
|
|
1188
|
-
const columnOrder = React.useMemo(()=>reconcileColumnOrder(rawOrder ?? columnKeys, columnKeys), [
|
|
1206
|
+
const columnOrder = React.useMemo(()=>reconcileColumnOrder(rawOrder ?? columnKeys, columnKeys, actionsColumnKey), [
|
|
1189
1207
|
rawOrder,
|
|
1190
|
-
columnKeys
|
|
1208
|
+
columnKeys,
|
|
1209
|
+
actionsColumnKey
|
|
1191
1210
|
]);
|
|
1192
1211
|
const [hiddenColumnKeys = EMPTY_STRINGS, setHiddenColumnKeys] = useControllableState$1({
|
|
1193
1212
|
prop: hiddenProp,
|
|
@@ -2169,7 +2188,7 @@ function useDataTableRowContext({ strict = true } = {}) {
|
|
|
2169
2188
|
return context;
|
|
2170
2189
|
}
|
|
2171
2190
|
const DataTableRoot = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
2172
|
-
const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
|
|
2191
|
+
const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, actionsColumnKey, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
|
|
2173
2192
|
const api = useDataTable({
|
|
2174
2193
|
sort,
|
|
2175
2194
|
defaultSort,
|
|
@@ -2189,6 +2208,7 @@ const DataTableRoot = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
|
2189
2208
|
defaultToolbarFilters,
|
|
2190
2209
|
onToolbarFiltersChange,
|
|
2191
2210
|
columnKeys,
|
|
2211
|
+
actionsColumnKey,
|
|
2192
2212
|
columnOrder,
|
|
2193
2213
|
defaultColumnOrder,
|
|
2194
2214
|
onColumnOrderChange,
|
|
@@ -3533,10 +3553,25 @@ function RatingCell({ value, maxRating = 5 }) {
|
|
|
3533
3553
|
width,
|
|
3534
3554
|
pinned,
|
|
3535
3555
|
pinOffset,
|
|
3536
|
-
pinnedEdge: false
|
|
3556
|
+
pinnedEdge: false,
|
|
3557
|
+
stickyLastTarget: false
|
|
3537
3558
|
};
|
|
3538
3559
|
});
|
|
3539
3560
|
if (pinnedCount > 0) entries[pinnedCount - 1].pinnedEdge = true;
|
|
3561
|
+
// `stickyLast` sticks the `actions`-flagged column specifically -- not whichever
|
|
3562
|
+
// column merely landed last in render order. Without this, `reconcileColumnOrder`
|
|
3563
|
+
// appending a brand-new column past `actions` (this file's own `<colgroup>` doc
|
|
3564
|
+
// comment on why order must stay in lockstep) silently moved the sticky-right
|
|
3565
|
+
// treatment onto that new column instead, stranding `actions` in the middle of the
|
|
3566
|
+
// table -- a real consumer hit this. Falls back to the genuine last entry when no
|
|
3567
|
+
// column is flagged `actions` at all, matching `stickyLast`'s original single-
|
|
3568
|
+
// column behavior.
|
|
3569
|
+
if (entries.length > 0) {
|
|
3570
|
+
const actionsColumn = columns.find((column)=>column.actions);
|
|
3571
|
+
const actionsIndex = actionsColumn ? entries.findIndex((entry)=>entry.column.key === actionsColumn.key) : -1;
|
|
3572
|
+
const stickyLastIndex = actionsIndex !== -1 ? actionsIndex : entries.length - 1;
|
|
3573
|
+
entries[stickyLastIndex].stickyLastTarget = true;
|
|
3574
|
+
}
|
|
3540
3575
|
return entries;
|
|
3541
3576
|
}, [
|
|
3542
3577
|
columns,
|
|
@@ -3576,9 +3611,12 @@ function RatingCell({ value, maxRating = 5 }) {
|
|
|
3576
3611
|
}
|
|
3577
3612
|
/** Sticky positioning for one DATA cell (header, body, or ColumnCalc footer) -- pinned-
|
|
3578
3613
|
* left (this column's own `pinOffset`, `data-sticky-edge` only on the group's
|
|
3579
|
-
* trailing-most member) takes priority over sticky-right (`stickyLast`,
|
|
3580
|
-
*
|
|
3581
|
-
* one edge.
|
|
3614
|
+
* trailing-most member) takes priority over sticky-right (`stickyLast`, the
|
|
3615
|
+
* `actions`-flagged column when one exists, else the genuine last VISIBLE column)
|
|
3616
|
+
* since a column can only stick to one edge. Reads `entry.stickyLastTarget`
|
|
3617
|
+
* (`useEffectiveColumns`) rather than an `isLast` index the caller computes itself --
|
|
3618
|
+
* that index is just "last in render order", which is exactly what stranded a
|
|
3619
|
+
* reordered-past `actions` column in the first place. */ function pinnedCellProps(entry, stickyLast) {
|
|
3582
3620
|
if (entry.pinned) {
|
|
3583
3621
|
return {
|
|
3584
3622
|
"data-sticky": "",
|
|
@@ -3590,7 +3628,7 @@ function RatingCell({ value, maxRating = 5 }) {
|
|
|
3590
3628
|
}
|
|
3591
3629
|
};
|
|
3592
3630
|
}
|
|
3593
|
-
if (stickyLast &&
|
|
3631
|
+
if (stickyLast && entry.stickyLastTarget) {
|
|
3594
3632
|
return {
|
|
3595
3633
|
"data-sticky": "",
|
|
3596
3634
|
"data-sticky-edge-end": "",
|
|
@@ -4749,26 +4787,87 @@ function ChipListEditor({ column, value, pending, onCommit, classNames }) {
|
|
|
4749
4787
|
}));
|
|
4750
4788
|
}
|
|
4751
4789
|
////////////////////////////////////////////////////////////////////////////////////
|
|
4752
|
-
// -- Feature C (#12): cell-expand "show full content" overlay --
|
|
4753
|
-
//
|
|
4754
|
-
//
|
|
4755
|
-
//
|
|
4756
|
-
//
|
|
4757
|
-
//
|
|
4758
|
-
//
|
|
4759
|
-
//
|
|
4760
|
-
|
|
4761
|
-
|
|
4790
|
+
// -- Feature C (#12): cell-expand "show full content" overlay -- a box that wraps
|
|
4791
|
+
// whatever a truncated cell would otherwise show inline: an editable column's OWN
|
|
4792
|
+
// editor node (unchanged, just relocated into this box instead of rendered flush in
|
|
4793
|
+
// the 36px row) for the editable case, or the plain full value for a read-only one.
|
|
4794
|
+
// `body-cells.tsx` decides WHEN to render this (a click on a cell whose content is
|
|
4795
|
+
// actually truncated, `truncate.ts`'s `isCellTruncated`); this component only
|
|
4796
|
+
// supplies the box.
|
|
4797
|
+
//
|
|
4798
|
+
// Portals to `document.body` (crispy-seed#1094's own pinned-column follow-up,
|
|
4799
|
+
// ported from the chip-list editor's popover, `body-cells.tsx`'s
|
|
4800
|
+
// `isChipListEditable` -- that one shipped this exact fix in 0.10.3) rather than
|
|
4801
|
+
// positioning `absolute` against its own cell: a `position: sticky` `<td>` clips ANY
|
|
4802
|
+
// descendant that grows past its own box, in every browser tested, regardless of
|
|
4803
|
+
// `overflow`/`z-index` on anything in between, so a pinned column's own truncated
|
|
4804
|
+
// cell used to clip this overlay's grown content instead of showing it in full. A
|
|
4805
|
+
// portal sidesteps that by not being a descendant of the sticky `<td>` at all once
|
|
4806
|
+
// mounted -- position is instead measured live off `anchorRef`'s own
|
|
4807
|
+
// `getBoundingClientRect()`, the same technique the chip-list popover already uses.
|
|
4808
|
+
function CellExpandOverlay({ className, anchorRef, contentRef, children }) {
|
|
4809
|
+
const [style, setStyle] = React__namespace.useState(null);
|
|
4810
|
+
// Grows 1px past the cell's own edges in every direction, matching this box's
|
|
4811
|
+
// pre-portal `top: -1px; insetInlineStart: -1px; insetInlineEnd: -1px` (the recipe's
|
|
4812
|
+
// `expandOverlay` class, `data-table.ts`) -- hides the 1px border seam against the
|
|
4813
|
+
// cell it's expanding out of. `minHeight`/`insetInlineEnd` from that same class no
|
|
4814
|
+
// longer apply once portaled (they resolved against the `<td>` as this box's own
|
|
4815
|
+
// containing block, meaningless once it's a `position: fixed` child of `<body>`
|
|
4816
|
+
// instead -- the same override the chip-list popover's own `measurePopoverPosition`,
|
|
4817
|
+
// `body-cells.tsx`, already makes), so this overrides both explicitly.
|
|
4818
|
+
const measure = React__namespace.useCallback(()=>{
|
|
4819
|
+
const cell = anchorRef.current;
|
|
4820
|
+
if (!cell) return;
|
|
4821
|
+
const rect = cell.getBoundingClientRect();
|
|
4822
|
+
setStyle({
|
|
4823
|
+
position: "fixed",
|
|
4824
|
+
top: rect.top - 1,
|
|
4825
|
+
insetInlineStart: rect.left - 1,
|
|
4826
|
+
insetInlineEnd: "auto",
|
|
4827
|
+
width: rect.width + 2,
|
|
4828
|
+
minHeight: "auto",
|
|
4829
|
+
zIndex: 1000
|
|
4830
|
+
});
|
|
4831
|
+
}, [
|
|
4832
|
+
anchorRef
|
|
4833
|
+
]);
|
|
4834
|
+
// Measured before paint, on mount -- see this component's own prop doc comment for
|
|
4835
|
+
// why it can't just trust a caller to have measured first.
|
|
4836
|
+
React__namespace.useLayoutEffect(()=>{
|
|
4837
|
+
measure();
|
|
4838
|
+
}, [
|
|
4839
|
+
measure
|
|
4840
|
+
]);
|
|
4841
|
+
// Same capture-phase listeners as the chip-list popover's own reposition effect
|
|
4842
|
+
// (`body-cells.tsx`) -- `scroll` doesn't bubble, so the table's own horizontal/
|
|
4843
|
+
// vertical scroll container (an arbitrary ancestor, not necessarily `window` itself)
|
|
4844
|
+
// is only observable this way.
|
|
4845
|
+
React__namespace.useEffect(()=>{
|
|
4846
|
+
window.addEventListener("scroll", measure, true);
|
|
4847
|
+
window.addEventListener("resize", measure);
|
|
4848
|
+
return ()=>{
|
|
4849
|
+
window.removeEventListener("scroll", measure, true);
|
|
4850
|
+
window.removeEventListener("resize", measure);
|
|
4851
|
+
};
|
|
4852
|
+
}, [
|
|
4853
|
+
measure
|
|
4854
|
+
]);
|
|
4855
|
+
// Nothing to portal until the first measurement lands (avoids a one-frame flash at
|
|
4856
|
+
// the origin corner before `useLayoutEffect` runs).
|
|
4857
|
+
if (!style) return null;
|
|
4858
|
+
return /*#__PURE__*/ reactDom.createPortal(// `stopPropagation` -- an overlay sits on top of (and past) its own cell/row;
|
|
4762
4859
|
// a click anywhere inside it must never ALSO fire the row's own `onRowClick`,
|
|
4763
4860
|
// the same guard every other interactive cell affordance in this directory
|
|
4764
4861
|
// already applies (`LinkCell`, `EntityCell`'s preview button, editors.tsx's
|
|
4765
4862
|
// `TextEditor` root).
|
|
4766
4863
|
// biome-ignore lint/a11y/noStaticElementInteractions: not a widget -- this div never responds to interaction on ITSELF, only stops propagation of events that already occurred on a real interactive descendant (the editor/text it wraps). No role fits it because it isn't a control.
|
|
4767
4864
|
/*#__PURE__*/ jsxRuntime.jsx("div", {
|
|
4865
|
+
ref: contentRef,
|
|
4768
4866
|
className: className,
|
|
4867
|
+
style: style,
|
|
4769
4868
|
onClick: (event)=>event.stopPropagation(),
|
|
4770
4869
|
children: children
|
|
4771
|
-
}));
|
|
4870
|
+
}), document.body);
|
|
4772
4871
|
}
|
|
4773
4872
|
|
|
4774
4873
|
// Row-narrowing for the toolbar's search box -- split out of DataTable.tsx the way
|
|
@@ -4864,21 +4963,17 @@ function isCellTruncated(cell) {
|
|
|
4864
4963
|
* close itself after the first tag commits, see `ChipListEditor`'s own doc
|
|
4865
4964
|
* comment).
|
|
4866
4965
|
*
|
|
4867
|
-
*
|
|
4868
|
-
*
|
|
4869
|
-
*
|
|
4870
|
-
*
|
|
4871
|
-
*
|
|
4872
|
-
* `<td>` clips ANY descendant content that grows past its own
|
|
4873
|
-
* scrollport, in EVERY browser tested, regardless of
|
|
4874
|
-
* element in between and regardless of how high its
|
|
4875
|
-
* of which is a stacking-context or overflow bug
|
|
4876
|
-
* INSIDE. A portal sidesteps it entirely by not being a
|
|
4877
|
-
* `<td>` at all once mounted.
|
|
4878
|
-
* OTHER gated editors' `CellExpandOverlay` too when their own column is pinned,
|
|
4879
|
-
* but porting all of Feature C to this same portal model is its own change,
|
|
4880
|
-
* out of scope here -- this fix is scoped to the one editor crispy-seed#1094
|
|
4881
|
-
* actually reported against a pinned column. */ function isChipListEditable(column) {
|
|
4966
|
+
* This one renders through its OWN REAL React portal to `document.body`
|
|
4967
|
+
* (`DataTableBodyCell`'s own `popoverStyle`) rather than `editors.tsx`'s shared
|
|
4968
|
+
* `CellExpandOverlay`, since its shape is different (Attio's own click-anywhere-
|
|
4969
|
+
* opens gesture, no truncation gate). `CellExpandOverlay` itself ALSO portals to
|
|
4970
|
+
* `document.body` now (ported from this exact fix) -- both exist because a
|
|
4971
|
+
* `position: sticky` `<td>` clips ANY descendant content that grows past its own
|
|
4972
|
+
* box to the table's scrollport, in EVERY browser tested, regardless of
|
|
4973
|
+
* `overflow: visible` on every element in between and regardless of how high its
|
|
4974
|
+
* `z-index` is raised -- neither of which is a stacking-context or overflow bug
|
|
4975
|
+
* fixable from the INSIDE. A portal sidesteps it entirely by not being a
|
|
4976
|
+
* descendant of the sticky `<td>` at all once mounted. */ function isChipListEditable(column) {
|
|
4882
4977
|
return !!column.editable && column.type === "chip-list";
|
|
4883
4978
|
}
|
|
4884
4979
|
function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyFirst, stickyLast, cellContextMenu, onViewEditHistory, cellContextMenuItems }) {
|
|
@@ -4887,10 +4982,9 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
4887
4982
|
stickyFirst
|
|
4888
4983
|
});
|
|
4889
4984
|
return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
|
|
4890
|
-
children: effective.map((entry
|
|
4985
|
+
children: effective.map((entry)=>{
|
|
4891
4986
|
const { column } = entry;
|
|
4892
|
-
const
|
|
4893
|
-
const sticky = pinnedCellProps(entry, stickyLast, isLast);
|
|
4987
|
+
const sticky = pinnedCellProps(entry, stickyLast);
|
|
4894
4988
|
return /*#__PURE__*/ jsxRuntime.jsx(DataTableBodyCell, {
|
|
4895
4989
|
column: column,
|
|
4896
4990
|
row: row,
|
|
@@ -5038,6 +5132,16 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
5038
5132
|
// mounted, in `document.body` -- see `isChipListEditable`'s doc comment) and by
|
|
5039
5133
|
// the reposition effect further down.
|
|
5040
5134
|
const popoverRef = React__namespace.useRef(null);
|
|
5135
|
+
// Feature C's own portaled overlay (`CellExpandOverlay`, editors.tsx) -- its own DOM
|
|
5136
|
+
// node, once mounted, same reasoning as `popoverRef` above: it lives outside
|
|
5137
|
+
// `cellRef`'s subtree (a sibling of the table in `document.body`, not a descendant of
|
|
5138
|
+
// the `<td>`), so the outside-click detector below needs a ref to it too. Without
|
|
5139
|
+
// this, `isInside` saw every click/keystroke INSIDE the portaled editor as "outside"
|
|
5140
|
+
// once the overlay actually escaped the `<td>` (this fix's whole point) -- e.g.
|
|
5141
|
+
// typing into the overlay's own `TextEditor` fires a pointerdown on an input that is
|
|
5142
|
+
// no longer `cellRef.current`'s descendant, so every keystroke closed the overlay and
|
|
5143
|
+
// cancelled the edit before the user finished typing.
|
|
5144
|
+
const expandOverlayRef = React__namespace.useRef(null);
|
|
5041
5145
|
// Closes the overlay on Escape or an outside click/pointerdown -- a gated
|
|
5042
5146
|
// editor's OWN Escape/blur handling already ends `isEditing` on its own (see
|
|
5043
5147
|
// `TextEditor`/`SelectEditor`'s existing onCancel/onOpenChange wiring), which
|
|
@@ -5050,7 +5154,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
5050
5154
|
React__namespace.useEffect(()=>{
|
|
5051
5155
|
if (!overlayOpen) return;
|
|
5052
5156
|
function isInside(target) {
|
|
5053
|
-
return (cellRef.current?.contains(target) ?? false) || (popoverRef.current?.contains(target) ?? false);
|
|
5157
|
+
return (cellRef.current?.contains(target) ?? false) || (popoverRef.current?.contains(target) ?? false) || (expandOverlayRef.current?.contains(target) ?? false);
|
|
5054
5158
|
}
|
|
5055
5159
|
function handlePointerDown(event) {
|
|
5056
5160
|
if (!isInside(event.target)) {
|
|
@@ -5123,7 +5227,9 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
5123
5227
|
rowId: row.id,
|
|
5124
5228
|
classNames: classNames,
|
|
5125
5229
|
locale: locale,
|
|
5126
|
-
overlayActive: overlayActive
|
|
5230
|
+
overlayActive: overlayActive,
|
|
5231
|
+
cellRef: cellRef,
|
|
5232
|
+
expandOverlayRef: expandOverlayRef
|
|
5127
5233
|
}),
|
|
5128
5234
|
cellContextMenu && contextMenuOpen && contextMenuPoint && // A `<td>` (unlike its own parent `<tr>`) can validly hold any flow
|
|
5129
5235
|
// content, so `CellContextMenu`'s own 0x0 `MenuAnchor` -- itself never
|
|
@@ -5168,7 +5274,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
5168
5274
|
* display (a custom `render` wins there, same as before `editable` existed; it never
|
|
5169
5275
|
* overrides an actual editing/always-live state, matching crisp's own split between
|
|
5170
5276
|
* `renderTyped`'s rest-state branch and the editing branches above it).
|
|
5171
|
-
*/ function CellContent({ column, row, rowId, classNames, locale, overlayActive }) {
|
|
5277
|
+
*/ function CellContent({ column, row, rowId, classNames, locale, overlayActive, cellRef, expandOverlayRef }) {
|
|
5172
5278
|
const { isEditing, isPending, getEditError, commitEdit, cancelEdit } = useDataTableContext();
|
|
5173
5279
|
const rawValue = getColumnValue(column, row);
|
|
5174
5280
|
const pending = isPending(rowId, column.key);
|
|
@@ -5262,6 +5368,8 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
5262
5368
|
children: [
|
|
5263
5369
|
overlayActive ? /*#__PURE__*/ jsxRuntime.jsx(CellExpandOverlay, {
|
|
5264
5370
|
className: classNames.expandOverlay,
|
|
5371
|
+
anchorRef: cellRef,
|
|
5372
|
+
contentRef: expandOverlayRef,
|
|
5265
5373
|
children: column.editable ? node : /*#__PURE__*/ jsxRuntime.jsx("span", {
|
|
5266
5374
|
className: classNames.expandOverlayText,
|
|
5267
5375
|
children: node
|
|
@@ -5425,10 +5533,9 @@ function ColumnCalcCells({ columns, rows, selectable, stickyFirst, stickyLast })
|
|
|
5425
5533
|
stickyFirst
|
|
5426
5534
|
});
|
|
5427
5535
|
return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
|
|
5428
|
-
children: effective.map((entry
|
|
5536
|
+
children: effective.map((entry)=>{
|
|
5429
5537
|
const { column } = entry;
|
|
5430
|
-
const
|
|
5431
|
-
const sticky = pinnedCellProps(entry, stickyLast, isLast);
|
|
5538
|
+
const sticky = pinnedCellProps(entry, stickyLast);
|
|
5432
5539
|
return /*#__PURE__*/ jsxRuntime.jsx(DataTableFooterCell, {
|
|
5433
5540
|
"data-sticky": sticky["data-sticky"],
|
|
5434
5541
|
"data-sticky-edge": sticky["data-sticky-edge"],
|
|
@@ -5651,10 +5758,9 @@ const DEFAULT_SORT_ICON = {
|
|
|
5651
5758
|
});
|
|
5652
5759
|
const [renamingKey, setRenamingKey] = React__namespace.useState(null);
|
|
5653
5760
|
return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
|
|
5654
|
-
children: effective.map((entry
|
|
5761
|
+
children: effective.map((entry)=>{
|
|
5655
5762
|
const { column, label, width } = entry;
|
|
5656
|
-
const
|
|
5657
|
-
const sticky = pinnedCellProps(entry, stickyLast, isLast);
|
|
5763
|
+
const sticky = pinnedCellProps(entry, stickyLast);
|
|
5658
5764
|
return /*#__PURE__*/ jsxRuntime.jsxs(DataTableHeaderCell, {
|
|
5659
5765
|
columnKey: column.key,
|
|
5660
5766
|
sortable: column.sortable,
|
|
@@ -11664,6 +11770,12 @@ function DataTableInner(props, ref) {
|
|
|
11664
11770
|
const columnKeys = React__namespace.useMemo(()=>columns.map((column)=>column.key), [
|
|
11665
11771
|
columns
|
|
11666
11772
|
]);
|
|
11773
|
+
// Threaded down to `reconcileColumnOrder` (react-headless's `dom.ts`) so a brand-new
|
|
11774
|
+
// column added to `columns` after mount gets appended BEFORE this one, not past it --
|
|
11775
|
+
// see that function's own doc comment for the bug this prevents.
|
|
11776
|
+
const actionsColumnKey = React__namespace.useMemo(()=>columns.find((column)=>column.actions)?.key, [
|
|
11777
|
+
columns
|
|
11778
|
+
]);
|
|
11667
11779
|
const searchable = React__namespace.useMemo(()=>columns.some((column)=>column.searchable), [
|
|
11668
11780
|
columns
|
|
11669
11781
|
]);
|
|
@@ -11706,6 +11818,7 @@ function DataTableInner(props, ref) {
|
|
|
11706
11818
|
toolbarFilters: resolvedToolbarFilters,
|
|
11707
11819
|
onToolbarFiltersChange: handleToolbarFiltersChange,
|
|
11708
11820
|
columnKeys: columnKeys,
|
|
11821
|
+
actionsColumnKey: actionsColumnKey,
|
|
11709
11822
|
columnOrder: columnOrder,
|
|
11710
11823
|
defaultColumnOrder: defaultColumnOrder,
|
|
11711
11824
|
onColumnOrderChange: onColumnOrderChange,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react from 'react';
|
|
2
2
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
3
|
|
|
4
4
|
declare interface DataTableVariant {
|
|
@@ -151,7 +151,7 @@ interface DataTableColumn<TRow> {
|
|
|
151
151
|
*/
|
|
152
152
|
key: string;
|
|
153
153
|
/** 헤더 셀에 표시할 내용입니다. */
|
|
154
|
-
header:
|
|
154
|
+
header: react.ReactNode;
|
|
155
155
|
/** 이 열을 클릭해 정렬할 수 있는지 여부입니다. */
|
|
156
156
|
sortable?: boolean;
|
|
157
157
|
/**
|
|
@@ -162,7 +162,7 @@ interface DataTableColumn<TRow> {
|
|
|
162
162
|
/** 셀 내용을 직접 그립니다. 지정하면 `type`의 기본 렌더러 대신 이것을 사용합니다.
|
|
163
163
|
* `editable`이어도 편집/always-live 상태에는 영향이 없습니다 -- 이 필드는 REST
|
|
164
164
|
* 상태의 표시만 바꿉니다. */
|
|
165
|
-
render?: (row: TRow) =>
|
|
165
|
+
render?: (row: TRow) => react.ReactNode;
|
|
166
166
|
/** `type: "currency"`의 통화 기호입니다. @default "$" */
|
|
167
167
|
symbol?: string;
|
|
168
168
|
/** `type: "rating"`의 별 개수입니다. @default 5 */
|
|
@@ -198,8 +198,11 @@ interface DataTableColumn<TRow> {
|
|
|
198
198
|
* 절대 스크롤되어 사라지지 않도록 오른쪽 끝에 고정합니다 -- 이 플래그가 있는 열은 테이블
|
|
199
199
|
* 전체의 `stickyLast`를 켜지 않아도 자동으로 그 고정 + 왼쪽 스크롤 그림자
|
|
200
200
|
* (`stickyProps`/`stickyLast`가 이미 구현한 것과 동일한 메커니즘, `data-sticky-edge-end`
|
|
201
|
-
* / `data-scrolled-end`)를 받습니다.
|
|
202
|
-
*
|
|
201
|
+
* / `data-scrolled-end`)를 받습니다. `stickyLast`는 이 플래그가 붙은 열을 직접 찾아
|
|
202
|
+
* 고정합니다 (`column-layout.ts`의 `useEffectiveColumns`) -- 더 이상 "마지막으로 보이는
|
|
203
|
+
* 열"이어야만 한다는 제약이 없습니다. 내부적으로도 `reconcileColumnOrder`(react-headless의
|
|
204
|
+
* `dom.ts`)가 이 열을 항상 맨 끝에 유지하므로, 마운트 이후 추가된 새 열이 이 열보다 뒤로
|
|
205
|
+
* 붙어 고정 셀 뒤에 가려지는 일도 없습니다.
|
|
203
206
|
*/
|
|
204
207
|
actions?: boolean;
|
|
205
208
|
/**
|
|
@@ -289,11 +292,11 @@ interface DataTableColumn<TRow> {
|
|
|
289
292
|
* 줄임표 처리되고, 아이콘은 `flex-shrink:0`). `type: "text"`(기본)와 `"url"`/
|
|
290
293
|
* `"email"` 열에서 동작합니다.
|
|
291
294
|
*/
|
|
292
|
-
suffixIcon?: (row: TRow) =>
|
|
295
|
+
suffixIcon?: (row: TRow) => react.ReactNode;
|
|
293
296
|
}
|
|
294
297
|
interface DataTableProps<TRow extends {
|
|
295
298
|
id: string;
|
|
296
|
-
}> extends DataTableVariantProps, Omit<PrimitiveProps, "asChild">, Omit<
|
|
299
|
+
}> extends DataTableVariantProps, Omit<PrimitiveProps, "asChild">, Omit<react.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
297
300
|
columns: DataTableColumn<TRow>[];
|
|
298
301
|
data: TRow[];
|
|
299
302
|
/** `type`이 있는 열의 number/currency/date/datetime 표시에 쓰는 로캘입니다. @default "en-US" */
|
|
@@ -311,9 +314,9 @@ interface DataTableProps<TRow extends {
|
|
|
311
314
|
* 씁니다. 생략하면 karrot 아이콘 셋의 chevron을 씁니다.
|
|
312
315
|
*/
|
|
313
316
|
sortIcon?: {
|
|
314
|
-
idle?:
|
|
315
|
-
asc?:
|
|
316
|
-
desc?:
|
|
317
|
+
idle?: react.ReactNode;
|
|
318
|
+
asc?: react.ReactNode;
|
|
319
|
+
desc?: react.ReactNode;
|
|
317
320
|
};
|
|
318
321
|
/** 왼쪽에 40px짜리 체크박스 선택 열을 추가합니다. */
|
|
319
322
|
selectable?: boolean;
|
|
@@ -329,10 +332,10 @@ interface DataTableProps<TRow extends {
|
|
|
329
332
|
getRowSelectionLabel?: (row: TRow) => string;
|
|
330
333
|
/** 선택 체크박스가 checked일 때 표시할 아이콘입니다. 생략하면 karrot의 체크 아이콘을
|
|
331
334
|
* 씁니다. */
|
|
332
|
-
selectionCheckedIcon?:
|
|
335
|
+
selectionCheckedIcon?: react.ReactNode;
|
|
333
336
|
/** 전체 선택 체크박스가 일부만 선택됐을 때(indeterminate) 표시할 아이콘입니다.
|
|
334
337
|
* 생략하면 karrot의 minus 아이콘을 씁니다. */
|
|
335
|
-
selectionIndeterminateIcon?:
|
|
338
|
+
selectionIndeterminateIcon?: react.ReactNode;
|
|
336
339
|
/**
|
|
337
340
|
* 행이 하나 이상 선택되면 테이블 하단 중앙에 뜨는 벌크 액션 바(Attio 스타일 floating
|
|
338
341
|
* pill, gap #4)에 넣을 소비자 액션입니다. 현재 선택된 행 id 목록을 받는 render prop --
|
|
@@ -341,7 +344,7 @@ interface DataTableProps<TRow extends {
|
|
|
341
344
|
* "크롬 vs 소비자 콘텐츠" 분리). 생략하면 바는 크롬만 보여줍니다. `selectable`이 꺼져
|
|
342
345
|
* 있으면 무시됩니다(선택 자체가 없으므로).
|
|
343
346
|
*/
|
|
344
|
-
bulkActions?: (selectedIds: string[]) =>
|
|
347
|
+
bulkActions?: (selectedIds: string[]) => react.ReactNode;
|
|
345
348
|
/** 벌크 액션 바의 지우기(x) 버튼 접근성 이름입니다. @default "Clear selection" */
|
|
346
349
|
clearSelectionLabel?: string;
|
|
347
350
|
/** 제어할 검색어입니다. */
|
|
@@ -380,7 +383,7 @@ interface DataTableProps<TRow extends {
|
|
|
380
383
|
* 행 개수 텍스트를 직접 만듭니다. 생략하면 crisp의 기본 형식을 씁니다: 검색/필터로
|
|
381
384
|
* 줄지 않았으면 "N rows"(단수는 "row"), 줄었으면 "N of M".
|
|
382
385
|
*/
|
|
383
|
-
countLabel?: (shown: number, total: number) =>
|
|
386
|
+
countLabel?: (shown: number, total: number) => react.ReactNode;
|
|
384
387
|
/**
|
|
385
388
|
* 툴바 오른쪽, 행 개수 옆에 렌더링할 커스텀 액션입니다(예: Download/내보내기 버튼).
|
|
386
389
|
* 이전에는 이 자리에 넣을 공식 prop이 없어서 소비자가 `.seed-data-table__toolbar`
|
|
@@ -389,7 +392,7 @@ interface DataTableProps<TRow extends {
|
|
|
389
392
|
* 렌더링됩니다). 검색/정렬/필터보다 뒤, 카운트보다 앞(같은 그룹 안에서 카운트
|
|
390
393
|
* 왼쪽)에 옵니다.
|
|
391
394
|
*/
|
|
392
|
-
toolbarActions?:
|
|
395
|
+
toolbarActions?: react.ReactNode;
|
|
393
396
|
/** 헤더의 열별 메뉴(이름 변경 / 숨기기 / 왼쪽·오른쪽으로 이동)를 켭니다, seed `Menu`. */
|
|
394
397
|
columnMenu?: boolean;
|
|
395
398
|
/** 헤더를 드래그해 열 순서를 바꿀 수 있게 합니다. @default true */
|
|
@@ -470,7 +473,7 @@ interface DataTableProps<TRow extends {
|
|
|
470
473
|
* consumer's own "Assign to…") -- rendered inside the SAME `MenuScrollArea` the
|
|
471
474
|
* built-in items are, so a `<MenuItem>` from this package's own `Menu` export
|
|
472
475
|
* composes the way `column-menu.tsx`'s own items do. */
|
|
473
|
-
cellContextMenuItems?: (ctx: DataTableCellContextMenuContext<TRow>) =>
|
|
476
|
+
cellContextMenuItems?: (ctx: DataTableCellContextMenuContext<TRow>) => react.ReactNode;
|
|
474
477
|
/** 행을 클릭할 수 있게 하는 호버 표시입니다 (crisp의 3%/4% 포그라운드 틴트). 커서
|
|
475
478
|
* 모양은 `onRowClick`이 있는지에 따로 달려 있습니다 -- 이 값과 독립적으로 켤 수
|
|
476
479
|
* 있습니다(crisp의 두 prop, `interactive`/`onRowClick`도 서로 독립적입니다). */
|
|
@@ -481,19 +484,19 @@ interface DataTableProps<TRow extends {
|
|
|
481
484
|
/** 주어지면 맨 아래에 crisp가 측정한 "+ New" 추가 행 버튼을 렌더링합니다. */
|
|
482
485
|
onAddRow?: () => void;
|
|
483
486
|
/** 추가 행 버튼의 라벨입니다. @default "New record" */
|
|
484
|
-
addRowLabel?:
|
|
487
|
+
addRowLabel?: react.ReactNode;
|
|
485
488
|
/** 전체 본문을 이 상태로 대체합니다(로딩 아이콘 + `loadingLabel`) -- 필터/정렬
|
|
486
489
|
* 재조회나 최초 로드처럼 행 전체가 아직 없을 때를 위한 것입니다. `error`/빈 상태
|
|
487
490
|
* 보다 우선합니다. 인라인 스크롤 로딩 표시기(`onLoadMore`)와는 별개입니다. */
|
|
488
491
|
loading?: boolean;
|
|
489
492
|
/** `loading`일 때 스피너 옆에 보이는 텍스트입니다. @default "Loading..." */
|
|
490
|
-
loadingLabel?:
|
|
493
|
+
loadingLabel?: react.ReactNode;
|
|
491
494
|
/** 주어지면 행 대신 이 내용을 본문에 렌더링합니다(crisp의 `.crisp-datatable-empty`
|
|
492
495
|
* 칸 재사용). `loading`보다는 아래, 빈 상태보다는 위 우선순위입니다. */
|
|
493
|
-
error?:
|
|
496
|
+
error?: react.ReactNode;
|
|
494
497
|
/** 검색/필터를 거치고도 행이 하나도 없을 때 본문에 렌더링할 내용입니다.
|
|
495
498
|
* @default "No results." */
|
|
496
|
-
empty?:
|
|
499
|
+
empty?: react.ReactNode;
|
|
497
500
|
/**
|
|
498
501
|
* Attio 스타일 무한 스크롤: 스크롤 컨테이너 바닥 근처의 sentinel 행이 뷰에 들어오면
|
|
499
502
|
* 호출됩니다. `Promise`를 반환하면 그동안 자동으로 `loadingMore`가 켜집니다(직접
|
|
@@ -517,7 +520,7 @@ interface DataTableProps<TRow extends {
|
|
|
517
520
|
* `style={{ height: "100%" }}` 같은 바운드를 주는 것만으로도 "전체 높이" 모드가
|
|
518
521
|
* 됩니다 -- 새 prop 없이 기존 `style` 전달만으로 두 모드 다 지원됩니다.
|
|
519
522
|
*/
|
|
520
|
-
scrollProps?: Omit<
|
|
523
|
+
scrollProps?: Omit<react.ComponentPropsWithoutRef<"div">, "ref" | "onScroll">;
|
|
521
524
|
}
|
|
522
525
|
/**
|
|
523
526
|
* @see https://seed-design.io/react/components/data-table
|
|
@@ -525,8 +528,8 @@ interface DataTableProps<TRow extends {
|
|
|
525
528
|
declare const DataTable: <TRow extends {
|
|
526
529
|
id: string;
|
|
527
530
|
}>(props: DataTableProps<TRow> & {
|
|
528
|
-
ref?:
|
|
529
|
-
}) =>
|
|
531
|
+
ref?: react.ForwardedRef<HTMLDivElement>;
|
|
532
|
+
}) => react.ReactElement;
|
|
530
533
|
|
|
531
534
|
interface DataTableViewsControlProps extends UseDataTableViewsProps {
|
|
532
535
|
/** The trigger's label when no view is active. @default "Views" */
|
|
@@ -536,7 +539,7 @@ interface DataTableViewsControlProps extends UseDataTableViewsProps {
|
|
|
536
539
|
* meaningful "no view" state to switch back to. */
|
|
537
540
|
showClearRow?: boolean;
|
|
538
541
|
/** The clear row's own label, when `showClearRow` is on. @default "label" */
|
|
539
|
-
clearLabel?:
|
|
542
|
+
clearLabel?: react.ReactNode;
|
|
540
543
|
/**
|
|
541
544
|
* Reads the table's CURRENT column order/visibility/widths/pins + toolbar sort/
|
|
542
545
|
* filter conditions -- called when the user picks "Save as new view" or "Update
|
|
@@ -578,7 +581,7 @@ interface DataTableBulkBarProps {
|
|
|
578
581
|
* onClick={() => addToList(ids)}>Add to list</ActionButton>}</DataTableBulkBar>`.
|
|
579
582
|
* Omit for just the bar's own chrome ("N selected" + clear).
|
|
580
583
|
*/
|
|
581
|
-
children?: (selectedIds: string[]) =>
|
|
584
|
+
children?: (selectedIds: string[]) => react.ReactNode;
|
|
582
585
|
}
|
|
583
586
|
/**
|
|
584
587
|
* The floating bulk-action pill (gap #4) -- renders nothing while no row is selected
|
|
@@ -594,7 +597,7 @@ declare function DataTableBulkBar({ clearLabel, children, }: DataTableBulkBarPro
|
|
|
594
597
|
type ResizablePanelSide = "left" | "right";
|
|
595
598
|
|
|
596
599
|
type ResizablePanelHandleVariant = "panel" | "column";
|
|
597
|
-
type ResizablePanelProps = Omit<
|
|
600
|
+
type ResizablePanelProps = Omit<react.ComponentPropsWithoutRef<"div">, "children" | "onResize"> & {
|
|
598
601
|
/** Which edge of its container the panel is anchored to. @default "left" */
|
|
599
602
|
side?: ResizablePanelSide;
|
|
600
603
|
/** Controlled width, in px. */
|
|
@@ -683,9 +686,9 @@ type ResizablePanelProps = Omit<React.ComponentPropsWithoutRef<"div">, "children
|
|
|
683
686
|
* observe `animationName` changes on the SAME node that carries
|
|
684
687
|
* `data-state`/the keyframe animation above.
|
|
685
688
|
*/
|
|
686
|
-
panelElementRef?:
|
|
689
|
+
panelElementRef?: react.Ref<HTMLDivElement>;
|
|
687
690
|
className?: string;
|
|
688
|
-
children?:
|
|
691
|
+
children?: react.ReactNode;
|
|
689
692
|
};
|
|
690
693
|
/**
|
|
691
694
|
* An adjustable-panel primitive with two layout models (`floating`'s own doc
|
|
@@ -701,7 +704,7 @@ type ResizablePanelProps = Omit<React.ComponentPropsWithoutRef<"div">, "children
|
|
|
701
704
|
* lives in `useResizablePanel` (`@seed-design/react-resizable-panel`), this
|
|
702
705
|
* component owns the DOM + recipe classes on top of it.
|
|
703
706
|
*/
|
|
704
|
-
declare const ResizablePanel:
|
|
707
|
+
declare const ResizablePanel: react.ForwardRefExoticComponent<Omit<Omit<react.DetailedHTMLProps<react.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "children" | "onResize"> & {
|
|
705
708
|
/** Which edge of its container the panel is anchored to. @default "left" */
|
|
706
709
|
side?: ResizablePanelSide;
|
|
707
710
|
/** Controlled width, in px. */
|
|
@@ -790,10 +793,10 @@ declare const ResizablePanel: React.ForwardRefExoticComponent<Omit<Omit<React.De
|
|
|
790
793
|
* observe `animationName` changes on the SAME node that carries
|
|
791
794
|
* `data-state`/the keyframe animation above.
|
|
792
795
|
*/
|
|
793
|
-
panelElementRef?:
|
|
796
|
+
panelElementRef?: react.Ref<HTMLDivElement>;
|
|
794
797
|
className?: string;
|
|
795
|
-
children?:
|
|
796
|
-
} &
|
|
798
|
+
children?: react.ReactNode;
|
|
799
|
+
} & react.RefAttributes<HTMLDivElement>>;
|
|
797
800
|
|
|
798
801
|
/** One Highlights card -- label, value and an optional icon, laid out the way
|
|
799
802
|
* Attio's own peek panel lays its own out (your-moon/crisp#448, measured
|
|
@@ -802,8 +805,8 @@ declare const ResizablePanel: React.ForwardRefExoticComponent<Omit<Omit<React.De
|
|
|
802
805
|
type RecordPreviewHighlight = {
|
|
803
806
|
key: string;
|
|
804
807
|
label: string;
|
|
805
|
-
value:
|
|
806
|
-
icon?:
|
|
808
|
+
value: react.ReactNode;
|
|
809
|
+
icon?: react.ComponentType<{
|
|
807
810
|
size?: number;
|
|
808
811
|
}>;
|
|
809
812
|
};
|
|
@@ -816,7 +819,7 @@ type RecordPreviewSection = {
|
|
|
816
819
|
key: string;
|
|
817
820
|
label: string;
|
|
818
821
|
count: number;
|
|
819
|
-
content?:
|
|
822
|
+
content?: react.ReactNode;
|
|
820
823
|
};
|
|
821
824
|
/** One tab of the panel's own mini detail view (a "tabs" capability with no
|
|
822
825
|
* your-moon/crisp equivalent -- crisp's own peek only ever shows Highlights
|
|
@@ -828,12 +831,12 @@ type RecordPreviewSection = {
|
|
|
828
831
|
* tab has been visited) -- see `Tabs.tsx`. */
|
|
829
832
|
type RecordPreviewTab = {
|
|
830
833
|
key: string;
|
|
831
|
-
label:
|
|
832
|
-
content:
|
|
834
|
+
label: react.ReactNode;
|
|
835
|
+
content: react.ReactNode;
|
|
833
836
|
/** Rendered right after `label` inside the tab trigger, e.g. an unread
|
|
834
837
|
* count -- purely presentational, this component attaches no meaning to
|
|
835
838
|
* it. */
|
|
836
|
-
badge?:
|
|
839
|
+
badge?: react.ReactNode;
|
|
837
840
|
};
|
|
838
841
|
/** The record this panel previews. Only `name` is read directly here --
|
|
839
842
|
* everything else the panel shows comes from `highlights`/`activity`/
|
|
@@ -845,7 +848,7 @@ type RecordPreviewRecord = {
|
|
|
845
848
|
name?: string;
|
|
846
849
|
[key: string]: unknown;
|
|
847
850
|
};
|
|
848
|
-
type RecordPreviewProps = Omit<
|
|
851
|
+
type RecordPreviewProps = Omit<react.ComponentPropsWithoutRef<"div">, "children" | "title" | "onResize"> & {
|
|
849
852
|
/** Whether the panel is mounted/visible. */
|
|
850
853
|
open: boolean;
|
|
851
854
|
onClose: () => void;
|
|
@@ -866,7 +869,7 @@ type RecordPreviewProps = Omit<React.ComponentPropsWithoutRef<"div">, "children"
|
|
|
866
869
|
nextRecordLabel?: string;
|
|
867
870
|
/** "2 of 37 in All Companies" (your-moon/crisp#413) -- a single
|
|
868
871
|
* pre-formatted string; omit for no counter at all. */
|
|
869
|
-
counterLabel?:
|
|
872
|
+
counterLabel?: react.ReactNode;
|
|
870
873
|
/**
|
|
871
874
|
* Lets a person drag the panel wider or narrower from its own left edge --
|
|
872
875
|
* this port reuses `ResizablePanel`'s own handle (`side="right"`, whose
|
|
@@ -882,7 +885,7 @@ type RecordPreviewProps = Omit<React.ComponentPropsWithoutRef<"div">, "children"
|
|
|
882
885
|
minWidth?: number;
|
|
883
886
|
maxWidth?: number;
|
|
884
887
|
/** The panel's heading, and so its accessible name. Defaults to `record.name`. */
|
|
885
|
-
title?:
|
|
888
|
+
title?: react.ReactNode;
|
|
886
889
|
/** Avatar image URL beside the title. Omit for an initials tile built from
|
|
887
890
|
* `record.name`'s first character. */
|
|
888
891
|
avatar?: string;
|
|
@@ -891,13 +894,13 @@ type RecordPreviewProps = Omit<React.ComponentPropsWithoutRef<"div">, "children"
|
|
|
891
894
|
record: RecordPreviewRecord | null;
|
|
892
895
|
/** Heading above the highlight cards (default "Highlights"). Omit the
|
|
893
896
|
* whole section by omitting `highlights`. */
|
|
894
|
-
highlightsLabel?:
|
|
897
|
+
highlightsLabel?: react.ReactNode;
|
|
895
898
|
highlights?: RecordPreviewHighlight[];
|
|
896
899
|
/** Heading above the activity feed (default "Activity"). */
|
|
897
|
-
activityLabel?:
|
|
898
|
-
viewAllActivityLabel?:
|
|
900
|
+
activityLabel?: react.ReactNode;
|
|
901
|
+
viewAllActivityLabel?: react.ReactNode;
|
|
899
902
|
onViewAllActivity?: () => void;
|
|
900
|
-
activity?:
|
|
903
|
+
activity?: react.ReactNode;
|
|
901
904
|
/** Collapsed sections below Activity (your-moon/crisp#413). Omit for no
|
|
902
905
|
* sections at all. */
|
|
903
906
|
sections?: RecordPreviewSection[];
|
|
@@ -923,9 +926,9 @@ type RecordPreviewProps = Omit<React.ComponentPropsWithoutRef<"div">, "children"
|
|
|
923
926
|
onTabChange?: (key: string) => void;
|
|
924
927
|
/** Label for the synthesized overview tab -- see `tabs`'s own doc
|
|
925
928
|
* comment. @default "Overview" */
|
|
926
|
-
overviewTabLabel?:
|
|
929
|
+
overviewTabLabel?: react.ReactNode;
|
|
927
930
|
/** Rendered below the panel's content, e.g. an Edit button. */
|
|
928
|
-
footer?:
|
|
931
|
+
footer?: react.ReactNode;
|
|
929
932
|
/**
|
|
930
933
|
* An optional dimming backdrop behind the panel -- OFF by default (this
|
|
931
934
|
* is a peek, not a modal: Attio's own peek never dims the table it
|
package/dist/index.js
CHANGED
|
@@ -968,15 +968,33 @@ function sameCell$1(a, id, key) {
|
|
|
968
968
|
* first captured) nor keeps a ghost entry for one that was removed. Known keys keep
|
|
969
969
|
* their position from `order`; any key in `keys` missing from `order` is appended, in
|
|
970
970
|
* `keys`' own order; any entry in `order` no longer in `keys` is dropped.
|
|
971
|
-
|
|
971
|
+
*
|
|
972
|
+
* `actionsKey` (the styled layer's `actions`-flagged column, e.g. the row-menu `⋯`
|
|
973
|
+
* column) is kept LAST no matter what -- without this, a brand-new column added to
|
|
974
|
+
* `columns` after the table first mounted got appended past it (the "any key in `keys`
|
|
975
|
+
* missing from `order` is appended" rule above has no notion of "past the end" vs. "at
|
|
976
|
+
* the very end"), stranding it in the middle of the table since `stickyLast`/`actions`
|
|
977
|
+
* only sticks the actual trailing column. A real consumer hit this: their newly-added
|
|
978
|
+
* column landed to the right of the already-pinned-sticky `actions` column, covered by
|
|
979
|
+
* it under horizontal scroll. Column-layout's own `pinnedCellProps` independently
|
|
980
|
+
* targets the `actions` column by FLAG rather than by position (belt and suspenders --
|
|
981
|
+
* see that file's own doc comment), but this still keeps the VISUAL order sane so nothing
|
|
982
|
+
* renders hidden behind the sticky cell in the first place.
|
|
983
|
+
*/ function reconcileColumnOrder(order, keys, actionsKey) {
|
|
972
984
|
const keySet = new Set(keys);
|
|
973
985
|
const known = order.filter((key)=>keySet.has(key));
|
|
974
986
|
const knownSet = new Set(known);
|
|
975
987
|
const added = keys.filter((key)=>!knownSet.has(key));
|
|
976
|
-
|
|
988
|
+
const merged = [
|
|
977
989
|
...known,
|
|
978
990
|
...added
|
|
979
991
|
];
|
|
992
|
+
if (actionsKey === undefined || !keySet.has(actionsKey)) return merged;
|
|
993
|
+
const withoutActions = merged.filter((key)=>key !== actionsKey);
|
|
994
|
+
return [
|
|
995
|
+
...withoutActions,
|
|
996
|
+
actionsKey
|
|
997
|
+
];
|
|
980
998
|
}
|
|
981
999
|
/**
|
|
982
1000
|
* Moves `key` one step toward `direction` in `order` -- a no-op (same array, well,
|
|
@@ -1155,7 +1173,7 @@ const EMPTY_RECORD = {};
|
|
|
1155
1173
|
* state this hook already owns, not independent state
|
|
1156
1174
|
* of their own.
|
|
1157
1175
|
*/ function useDataTableColumns(props) {
|
|
1158
|
-
const { columnKeys = EMPTY_STRINGS, columnOrder: columnOrderProp, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys: hiddenProp, defaultHiddenColumnKeys = EMPTY_STRINGS, onHiddenColumnKeysChange, columnWidths: widthsProp, defaultColumnWidths = EMPTY_RECORD, onColumnWidthsChange, columnLabels: labelsProp, defaultColumnLabels = EMPTY_RECORD, onColumnLabelsChange, pinnedColumnKeys: pinnedProp, defaultPinnedColumnKeys = EMPTY_STRINGS, onPinnedColumnKeysChange } = props;
|
|
1176
|
+
const { columnKeys = EMPTY_STRINGS, actionsColumnKey, columnOrder: columnOrderProp, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys: hiddenProp, defaultHiddenColumnKeys = EMPTY_STRINGS, onHiddenColumnKeysChange, columnWidths: widthsProp, defaultColumnWidths = EMPTY_RECORD, onColumnWidthsChange, columnLabels: labelsProp, defaultColumnLabels = EMPTY_RECORD, onColumnLabelsChange, pinnedColumnKeys: pinnedProp, defaultPinnedColumnKeys = EMPTY_STRINGS, onPinnedColumnKeysChange } = props;
|
|
1159
1177
|
const [rawOrder, setRawOrder] = useControllableState$1({
|
|
1160
1178
|
prop: columnOrderProp,
|
|
1161
1179
|
defaultProp: defaultColumnOrder ?? columnKeys,
|
|
@@ -1166,9 +1184,10 @@ const EMPTY_RECORD = {};
|
|
|
1166
1184
|
// consumer persisted before a new column existed must still show that new column
|
|
1167
1185
|
// (appended), and one holding a column that got removed from `columns` must not
|
|
1168
1186
|
// render a ghost header for it.
|
|
1169
|
-
const columnOrder = useMemo(()=>reconcileColumnOrder(rawOrder ?? columnKeys, columnKeys), [
|
|
1187
|
+
const columnOrder = useMemo(()=>reconcileColumnOrder(rawOrder ?? columnKeys, columnKeys, actionsColumnKey), [
|
|
1170
1188
|
rawOrder,
|
|
1171
|
-
columnKeys
|
|
1189
|
+
columnKeys,
|
|
1190
|
+
actionsColumnKey
|
|
1172
1191
|
]);
|
|
1173
1192
|
const [hiddenColumnKeys = EMPTY_STRINGS, setHiddenColumnKeys] = useControllableState$1({
|
|
1174
1193
|
prop: hiddenProp,
|
|
@@ -2150,7 +2169,7 @@ function useDataTableRowContext({ strict = true } = {}) {
|
|
|
2150
2169
|
return context;
|
|
2151
2170
|
}
|
|
2152
2171
|
const DataTableRoot = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
2153
|
-
const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
|
|
2172
|
+
const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, actionsColumnKey, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
|
|
2154
2173
|
const api = useDataTable({
|
|
2155
2174
|
sort,
|
|
2156
2175
|
defaultSort,
|
|
@@ -2170,6 +2189,7 @@ const DataTableRoot = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
|
2170
2189
|
defaultToolbarFilters,
|
|
2171
2190
|
onToolbarFiltersChange,
|
|
2172
2191
|
columnKeys,
|
|
2192
|
+
actionsColumnKey,
|
|
2173
2193
|
columnOrder,
|
|
2174
2194
|
defaultColumnOrder,
|
|
2175
2195
|
onColumnOrderChange,
|
|
@@ -3514,10 +3534,25 @@ function RatingCell({ value, maxRating = 5 }) {
|
|
|
3514
3534
|
width,
|
|
3515
3535
|
pinned,
|
|
3516
3536
|
pinOffset,
|
|
3517
|
-
pinnedEdge: false
|
|
3537
|
+
pinnedEdge: false,
|
|
3538
|
+
stickyLastTarget: false
|
|
3518
3539
|
};
|
|
3519
3540
|
});
|
|
3520
3541
|
if (pinnedCount > 0) entries[pinnedCount - 1].pinnedEdge = true;
|
|
3542
|
+
// `stickyLast` sticks the `actions`-flagged column specifically -- not whichever
|
|
3543
|
+
// column merely landed last in render order. Without this, `reconcileColumnOrder`
|
|
3544
|
+
// appending a brand-new column past `actions` (this file's own `<colgroup>` doc
|
|
3545
|
+
// comment on why order must stay in lockstep) silently moved the sticky-right
|
|
3546
|
+
// treatment onto that new column instead, stranding `actions` in the middle of the
|
|
3547
|
+
// table -- a real consumer hit this. Falls back to the genuine last entry when no
|
|
3548
|
+
// column is flagged `actions` at all, matching `stickyLast`'s original single-
|
|
3549
|
+
// column behavior.
|
|
3550
|
+
if (entries.length > 0) {
|
|
3551
|
+
const actionsColumn = columns.find((column)=>column.actions);
|
|
3552
|
+
const actionsIndex = actionsColumn ? entries.findIndex((entry)=>entry.column.key === actionsColumn.key) : -1;
|
|
3553
|
+
const stickyLastIndex = actionsIndex !== -1 ? actionsIndex : entries.length - 1;
|
|
3554
|
+
entries[stickyLastIndex].stickyLastTarget = true;
|
|
3555
|
+
}
|
|
3521
3556
|
return entries;
|
|
3522
3557
|
}, [
|
|
3523
3558
|
columns,
|
|
@@ -3557,9 +3592,12 @@ function RatingCell({ value, maxRating = 5 }) {
|
|
|
3557
3592
|
}
|
|
3558
3593
|
/** Sticky positioning for one DATA cell (header, body, or ColumnCalc footer) -- pinned-
|
|
3559
3594
|
* left (this column's own `pinOffset`, `data-sticky-edge` only on the group's
|
|
3560
|
-
* trailing-most member) takes priority over sticky-right (`stickyLast`,
|
|
3561
|
-
*
|
|
3562
|
-
* one edge.
|
|
3595
|
+
* trailing-most member) takes priority over sticky-right (`stickyLast`, the
|
|
3596
|
+
* `actions`-flagged column when one exists, else the genuine last VISIBLE column)
|
|
3597
|
+
* since a column can only stick to one edge. Reads `entry.stickyLastTarget`
|
|
3598
|
+
* (`useEffectiveColumns`) rather than an `isLast` index the caller computes itself --
|
|
3599
|
+
* that index is just "last in render order", which is exactly what stranded a
|
|
3600
|
+
* reordered-past `actions` column in the first place. */ function pinnedCellProps(entry, stickyLast) {
|
|
3563
3601
|
if (entry.pinned) {
|
|
3564
3602
|
return {
|
|
3565
3603
|
"data-sticky": "",
|
|
@@ -3571,7 +3609,7 @@ function RatingCell({ value, maxRating = 5 }) {
|
|
|
3571
3609
|
}
|
|
3572
3610
|
};
|
|
3573
3611
|
}
|
|
3574
|
-
if (stickyLast &&
|
|
3612
|
+
if (stickyLast && entry.stickyLastTarget) {
|
|
3575
3613
|
return {
|
|
3576
3614
|
"data-sticky": "",
|
|
3577
3615
|
"data-sticky-edge-end": "",
|
|
@@ -4730,26 +4768,87 @@ function ChipListEditor({ column, value, pending, onCommit, classNames }) {
|
|
|
4730
4768
|
}));
|
|
4731
4769
|
}
|
|
4732
4770
|
////////////////////////////////////////////////////////////////////////////////////
|
|
4733
|
-
// -- Feature C (#12): cell-expand "show full content" overlay --
|
|
4734
|
-
//
|
|
4735
|
-
//
|
|
4736
|
-
//
|
|
4737
|
-
//
|
|
4738
|
-
//
|
|
4739
|
-
//
|
|
4740
|
-
//
|
|
4741
|
-
|
|
4742
|
-
|
|
4771
|
+
// -- Feature C (#12): cell-expand "show full content" overlay -- a box that wraps
|
|
4772
|
+
// whatever a truncated cell would otherwise show inline: an editable column's OWN
|
|
4773
|
+
// editor node (unchanged, just relocated into this box instead of rendered flush in
|
|
4774
|
+
// the 36px row) for the editable case, or the plain full value for a read-only one.
|
|
4775
|
+
// `body-cells.tsx` decides WHEN to render this (a click on a cell whose content is
|
|
4776
|
+
// actually truncated, `truncate.ts`'s `isCellTruncated`); this component only
|
|
4777
|
+
// supplies the box.
|
|
4778
|
+
//
|
|
4779
|
+
// Portals to `document.body` (crispy-seed#1094's own pinned-column follow-up,
|
|
4780
|
+
// ported from the chip-list editor's popover, `body-cells.tsx`'s
|
|
4781
|
+
// `isChipListEditable` -- that one shipped this exact fix in 0.10.3) rather than
|
|
4782
|
+
// positioning `absolute` against its own cell: a `position: sticky` `<td>` clips ANY
|
|
4783
|
+
// descendant that grows past its own box, in every browser tested, regardless of
|
|
4784
|
+
// `overflow`/`z-index` on anything in between, so a pinned column's own truncated
|
|
4785
|
+
// cell used to clip this overlay's grown content instead of showing it in full. A
|
|
4786
|
+
// portal sidesteps that by not being a descendant of the sticky `<td>` at all once
|
|
4787
|
+
// mounted -- position is instead measured live off `anchorRef`'s own
|
|
4788
|
+
// `getBoundingClientRect()`, the same technique the chip-list popover already uses.
|
|
4789
|
+
function CellExpandOverlay({ className, anchorRef, contentRef, children }) {
|
|
4790
|
+
const [style, setStyle] = React.useState(null);
|
|
4791
|
+
// Grows 1px past the cell's own edges in every direction, matching this box's
|
|
4792
|
+
// pre-portal `top: -1px; insetInlineStart: -1px; insetInlineEnd: -1px` (the recipe's
|
|
4793
|
+
// `expandOverlay` class, `data-table.ts`) -- hides the 1px border seam against the
|
|
4794
|
+
// cell it's expanding out of. `minHeight`/`insetInlineEnd` from that same class no
|
|
4795
|
+
// longer apply once portaled (they resolved against the `<td>` as this box's own
|
|
4796
|
+
// containing block, meaningless once it's a `position: fixed` child of `<body>`
|
|
4797
|
+
// instead -- the same override the chip-list popover's own `measurePopoverPosition`,
|
|
4798
|
+
// `body-cells.tsx`, already makes), so this overrides both explicitly.
|
|
4799
|
+
const measure = React.useCallback(()=>{
|
|
4800
|
+
const cell = anchorRef.current;
|
|
4801
|
+
if (!cell) return;
|
|
4802
|
+
const rect = cell.getBoundingClientRect();
|
|
4803
|
+
setStyle({
|
|
4804
|
+
position: "fixed",
|
|
4805
|
+
top: rect.top - 1,
|
|
4806
|
+
insetInlineStart: rect.left - 1,
|
|
4807
|
+
insetInlineEnd: "auto",
|
|
4808
|
+
width: rect.width + 2,
|
|
4809
|
+
minHeight: "auto",
|
|
4810
|
+
zIndex: 1000
|
|
4811
|
+
});
|
|
4812
|
+
}, [
|
|
4813
|
+
anchorRef
|
|
4814
|
+
]);
|
|
4815
|
+
// Measured before paint, on mount -- see this component's own prop doc comment for
|
|
4816
|
+
// why it can't just trust a caller to have measured first.
|
|
4817
|
+
React.useLayoutEffect(()=>{
|
|
4818
|
+
measure();
|
|
4819
|
+
}, [
|
|
4820
|
+
measure
|
|
4821
|
+
]);
|
|
4822
|
+
// Same capture-phase listeners as the chip-list popover's own reposition effect
|
|
4823
|
+
// (`body-cells.tsx`) -- `scroll` doesn't bubble, so the table's own horizontal/
|
|
4824
|
+
// vertical scroll container (an arbitrary ancestor, not necessarily `window` itself)
|
|
4825
|
+
// is only observable this way.
|
|
4826
|
+
React.useEffect(()=>{
|
|
4827
|
+
window.addEventListener("scroll", measure, true);
|
|
4828
|
+
window.addEventListener("resize", measure);
|
|
4829
|
+
return ()=>{
|
|
4830
|
+
window.removeEventListener("scroll", measure, true);
|
|
4831
|
+
window.removeEventListener("resize", measure);
|
|
4832
|
+
};
|
|
4833
|
+
}, [
|
|
4834
|
+
measure
|
|
4835
|
+
]);
|
|
4836
|
+
// Nothing to portal until the first measurement lands (avoids a one-frame flash at
|
|
4837
|
+
// the origin corner before `useLayoutEffect` runs).
|
|
4838
|
+
if (!style) return null;
|
|
4839
|
+
return /*#__PURE__*/ createPortal(// `stopPropagation` -- an overlay sits on top of (and past) its own cell/row;
|
|
4743
4840
|
// a click anywhere inside it must never ALSO fire the row's own `onRowClick`,
|
|
4744
4841
|
// the same guard every other interactive cell affordance in this directory
|
|
4745
4842
|
// already applies (`LinkCell`, `EntityCell`'s preview button, editors.tsx's
|
|
4746
4843
|
// `TextEditor` root).
|
|
4747
4844
|
// biome-ignore lint/a11y/noStaticElementInteractions: not a widget -- this div never responds to interaction on ITSELF, only stops propagation of events that already occurred on a real interactive descendant (the editor/text it wraps). No role fits it because it isn't a control.
|
|
4748
4845
|
/*#__PURE__*/ jsx("div", {
|
|
4846
|
+
ref: contentRef,
|
|
4749
4847
|
className: className,
|
|
4848
|
+
style: style,
|
|
4750
4849
|
onClick: (event)=>event.stopPropagation(),
|
|
4751
4850
|
children: children
|
|
4752
|
-
}));
|
|
4851
|
+
}), document.body);
|
|
4753
4852
|
}
|
|
4754
4853
|
|
|
4755
4854
|
// Row-narrowing for the toolbar's search box -- split out of DataTable.tsx the way
|
|
@@ -4845,21 +4944,17 @@ function isCellTruncated(cell) {
|
|
|
4845
4944
|
* close itself after the first tag commits, see `ChipListEditor`'s own doc
|
|
4846
4945
|
* comment).
|
|
4847
4946
|
*
|
|
4848
|
-
*
|
|
4849
|
-
*
|
|
4850
|
-
*
|
|
4851
|
-
*
|
|
4852
|
-
*
|
|
4853
|
-
* `<td>` clips ANY descendant content that grows past its own
|
|
4854
|
-
* scrollport, in EVERY browser tested, regardless of
|
|
4855
|
-
* element in between and regardless of how high its
|
|
4856
|
-
* of which is a stacking-context or overflow bug
|
|
4857
|
-
* INSIDE. A portal sidesteps it entirely by not being a
|
|
4858
|
-
* `<td>` at all once mounted.
|
|
4859
|
-
* OTHER gated editors' `CellExpandOverlay` too when their own column is pinned,
|
|
4860
|
-
* but porting all of Feature C to this same portal model is its own change,
|
|
4861
|
-
* out of scope here -- this fix is scoped to the one editor crispy-seed#1094
|
|
4862
|
-
* actually reported against a pinned column. */ function isChipListEditable(column) {
|
|
4947
|
+
* This one renders through its OWN REAL React portal to `document.body`
|
|
4948
|
+
* (`DataTableBodyCell`'s own `popoverStyle`) rather than `editors.tsx`'s shared
|
|
4949
|
+
* `CellExpandOverlay`, since its shape is different (Attio's own click-anywhere-
|
|
4950
|
+
* opens gesture, no truncation gate). `CellExpandOverlay` itself ALSO portals to
|
|
4951
|
+
* `document.body` now (ported from this exact fix) -- both exist because a
|
|
4952
|
+
* `position: sticky` `<td>` clips ANY descendant content that grows past its own
|
|
4953
|
+
* box to the table's scrollport, in EVERY browser tested, regardless of
|
|
4954
|
+
* `overflow: visible` on every element in between and regardless of how high its
|
|
4955
|
+
* `z-index` is raised -- neither of which is a stacking-context or overflow bug
|
|
4956
|
+
* fixable from the INSIDE. A portal sidesteps it entirely by not being a
|
|
4957
|
+
* descendant of the sticky `<td>` at all once mounted. */ function isChipListEditable(column) {
|
|
4863
4958
|
return !!column.editable && column.type === "chip-list";
|
|
4864
4959
|
}
|
|
4865
4960
|
function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyFirst, stickyLast, cellContextMenu, onViewEditHistory, cellContextMenuItems }) {
|
|
@@ -4868,10 +4963,9 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
4868
4963
|
stickyFirst
|
|
4869
4964
|
});
|
|
4870
4965
|
return /*#__PURE__*/ jsx(Fragment, {
|
|
4871
|
-
children: effective.map((entry
|
|
4966
|
+
children: effective.map((entry)=>{
|
|
4872
4967
|
const { column } = entry;
|
|
4873
|
-
const
|
|
4874
|
-
const sticky = pinnedCellProps(entry, stickyLast, isLast);
|
|
4968
|
+
const sticky = pinnedCellProps(entry, stickyLast);
|
|
4875
4969
|
return /*#__PURE__*/ jsx(DataTableBodyCell, {
|
|
4876
4970
|
column: column,
|
|
4877
4971
|
row: row,
|
|
@@ -5019,6 +5113,16 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
5019
5113
|
// mounted, in `document.body` -- see `isChipListEditable`'s doc comment) and by
|
|
5020
5114
|
// the reposition effect further down.
|
|
5021
5115
|
const popoverRef = React.useRef(null);
|
|
5116
|
+
// Feature C's own portaled overlay (`CellExpandOverlay`, editors.tsx) -- its own DOM
|
|
5117
|
+
// node, once mounted, same reasoning as `popoverRef` above: it lives outside
|
|
5118
|
+
// `cellRef`'s subtree (a sibling of the table in `document.body`, not a descendant of
|
|
5119
|
+
// the `<td>`), so the outside-click detector below needs a ref to it too. Without
|
|
5120
|
+
// this, `isInside` saw every click/keystroke INSIDE the portaled editor as "outside"
|
|
5121
|
+
// once the overlay actually escaped the `<td>` (this fix's whole point) -- e.g.
|
|
5122
|
+
// typing into the overlay's own `TextEditor` fires a pointerdown on an input that is
|
|
5123
|
+
// no longer `cellRef.current`'s descendant, so every keystroke closed the overlay and
|
|
5124
|
+
// cancelled the edit before the user finished typing.
|
|
5125
|
+
const expandOverlayRef = React.useRef(null);
|
|
5022
5126
|
// Closes the overlay on Escape or an outside click/pointerdown -- a gated
|
|
5023
5127
|
// editor's OWN Escape/blur handling already ends `isEditing` on its own (see
|
|
5024
5128
|
// `TextEditor`/`SelectEditor`'s existing onCancel/onOpenChange wiring), which
|
|
@@ -5031,7 +5135,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
5031
5135
|
React.useEffect(()=>{
|
|
5032
5136
|
if (!overlayOpen) return;
|
|
5033
5137
|
function isInside(target) {
|
|
5034
|
-
return (cellRef.current?.contains(target) ?? false) || (popoverRef.current?.contains(target) ?? false);
|
|
5138
|
+
return (cellRef.current?.contains(target) ?? false) || (popoverRef.current?.contains(target) ?? false) || (expandOverlayRef.current?.contains(target) ?? false);
|
|
5035
5139
|
}
|
|
5036
5140
|
function handlePointerDown(event) {
|
|
5037
5141
|
if (!isInside(event.target)) {
|
|
@@ -5104,7 +5208,9 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
5104
5208
|
rowId: row.id,
|
|
5105
5209
|
classNames: classNames,
|
|
5106
5210
|
locale: locale,
|
|
5107
|
-
overlayActive: overlayActive
|
|
5211
|
+
overlayActive: overlayActive,
|
|
5212
|
+
cellRef: cellRef,
|
|
5213
|
+
expandOverlayRef: expandOverlayRef
|
|
5108
5214
|
}),
|
|
5109
5215
|
cellContextMenu && contextMenuOpen && contextMenuPoint && // A `<td>` (unlike its own parent `<tr>`) can validly hold any flow
|
|
5110
5216
|
// content, so `CellContextMenu`'s own 0x0 `MenuAnchor` -- itself never
|
|
@@ -5149,7 +5255,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
5149
5255
|
* display (a custom `render` wins there, same as before `editable` existed; it never
|
|
5150
5256
|
* overrides an actual editing/always-live state, matching crisp's own split between
|
|
5151
5257
|
* `renderTyped`'s rest-state branch and the editing branches above it).
|
|
5152
|
-
*/ function CellContent({ column, row, rowId, classNames, locale, overlayActive }) {
|
|
5258
|
+
*/ function CellContent({ column, row, rowId, classNames, locale, overlayActive, cellRef, expandOverlayRef }) {
|
|
5153
5259
|
const { isEditing, isPending, getEditError, commitEdit, cancelEdit } = useDataTableContext();
|
|
5154
5260
|
const rawValue = getColumnValue(column, row);
|
|
5155
5261
|
const pending = isPending(rowId, column.key);
|
|
@@ -5243,6 +5349,8 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
5243
5349
|
children: [
|
|
5244
5350
|
overlayActive ? /*#__PURE__*/ jsx(CellExpandOverlay, {
|
|
5245
5351
|
className: classNames.expandOverlay,
|
|
5352
|
+
anchorRef: cellRef,
|
|
5353
|
+
contentRef: expandOverlayRef,
|
|
5246
5354
|
children: column.editable ? node : /*#__PURE__*/ jsx("span", {
|
|
5247
5355
|
className: classNames.expandOverlayText,
|
|
5248
5356
|
children: node
|
|
@@ -5406,10 +5514,9 @@ function ColumnCalcCells({ columns, rows, selectable, stickyFirst, stickyLast })
|
|
|
5406
5514
|
stickyFirst
|
|
5407
5515
|
});
|
|
5408
5516
|
return /*#__PURE__*/ jsx(Fragment, {
|
|
5409
|
-
children: effective.map((entry
|
|
5517
|
+
children: effective.map((entry)=>{
|
|
5410
5518
|
const { column } = entry;
|
|
5411
|
-
const
|
|
5412
|
-
const sticky = pinnedCellProps(entry, stickyLast, isLast);
|
|
5519
|
+
const sticky = pinnedCellProps(entry, stickyLast);
|
|
5413
5520
|
return /*#__PURE__*/ jsx(DataTableFooterCell, {
|
|
5414
5521
|
"data-sticky": sticky["data-sticky"],
|
|
5415
5522
|
"data-sticky-edge": sticky["data-sticky-edge"],
|
|
@@ -5632,10 +5739,9 @@ const DEFAULT_SORT_ICON = {
|
|
|
5632
5739
|
});
|
|
5633
5740
|
const [renamingKey, setRenamingKey] = React.useState(null);
|
|
5634
5741
|
return /*#__PURE__*/ jsx(Fragment, {
|
|
5635
|
-
children: effective.map((entry
|
|
5742
|
+
children: effective.map((entry)=>{
|
|
5636
5743
|
const { column, label, width } = entry;
|
|
5637
|
-
const
|
|
5638
|
-
const sticky = pinnedCellProps(entry, stickyLast, isLast);
|
|
5744
|
+
const sticky = pinnedCellProps(entry, stickyLast);
|
|
5639
5745
|
return /*#__PURE__*/ jsxs(DataTableHeaderCell, {
|
|
5640
5746
|
columnKey: column.key,
|
|
5641
5747
|
sortable: column.sortable,
|
|
@@ -11645,6 +11751,12 @@ function DataTableInner(props, ref) {
|
|
|
11645
11751
|
const columnKeys = React.useMemo(()=>columns.map((column)=>column.key), [
|
|
11646
11752
|
columns
|
|
11647
11753
|
]);
|
|
11754
|
+
// Threaded down to `reconcileColumnOrder` (react-headless's `dom.ts`) so a brand-new
|
|
11755
|
+
// column added to `columns` after mount gets appended BEFORE this one, not past it --
|
|
11756
|
+
// see that function's own doc comment for the bug this prevents.
|
|
11757
|
+
const actionsColumnKey = React.useMemo(()=>columns.find((column)=>column.actions)?.key, [
|
|
11758
|
+
columns
|
|
11759
|
+
]);
|
|
11648
11760
|
const searchable = React.useMemo(()=>columns.some((column)=>column.searchable), [
|
|
11649
11761
|
columns
|
|
11650
11762
|
]);
|
|
@@ -11687,6 +11799,7 @@ function DataTableInner(props, ref) {
|
|
|
11687
11799
|
toolbarFilters: resolvedToolbarFilters,
|
|
11688
11800
|
onToolbarFiltersChange: handleToolbarFiltersChange,
|
|
11689
11801
|
columnKeys: columnKeys,
|
|
11802
|
+
actionsColumnKey: actionsColumnKey,
|
|
11690
11803
|
columnOrder: columnOrder,
|
|
11691
11804
|
defaultColumnOrder: defaultColumnOrder,
|
|
11692
11805
|
onColumnOrderChange: onColumnOrderChange,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crispy-seed/data-table",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Self-contained, publishable build of seed-design's Attio-measured DataTable, ResizablePanel and RecordPreview — one install, one CSS import.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|