Skip to content

Commit 089eabd

Browse files
authored
feat: Virtualize column organization lists (#2568)
Fixes #1650 Further improvement for DH-18960 that wasn't part of the ticket, but will be appreciated. The original ticket was big enough I didn't want to add another feature to that PR. Fixes the issue when dragging lots of columns the drag overlay grew. It is now properly truncating. Also removed the drag handle icon from the drag overlay just to give a bit more room when dragging groups (and I think the handle doesn't make that much sense to show when you're already dragging) Also fixed the drop animation when dragging from/to a group. Same issue as the general `movedColumns` fixed in the first PR.
1 parent 2d5f930 commit 089eabd

20 files changed

+472
-245
lines changed

package-lock.json

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/iris-grid/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@dnd-kit/utilities": "^3.2.2",
5050
"@fortawesome/react-fontawesome": "^0.2.0",
5151
"@hello-pangea/dnd": "^18.0.1",
52+
"@tanstack/react-virtual": "^3.13.12",
5253
"classnames": "^2.3.1",
5354
"fast-deep-equal": "^3.1.3",
5455
"lodash.clamp": "^4.0.3",

packages/iris-grid/src/sidebar/visibility-ordering-builder/SearchItem.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ import { type FlattenedIrisGridTreeItem } from './sortable-tree/utilities';
88
type SearchItemProps = {
99
value: string;
1010
item: FlattenedIrisGridTreeItem;
11-
onClick: (name: string, event: React.MouseEvent<HTMLElement>) => void;
12-
onKeyDown: (name: string, event: React.KeyboardEvent<HTMLElement>) => void;
11+
onClick: (
12+
item: FlattenedIrisGridTreeItem,
13+
event: React.MouseEvent<HTMLElement>
14+
) => void;
15+
onKeyDown: (
16+
item: FlattenedIrisGridTreeItem,
17+
event: React.KeyboardEvent<HTMLElement>
18+
) => void;
1319
handleProps?: Record<string, unknown>;
1420
};
1521

@@ -19,16 +25,16 @@ const SearchItem = forwardRef<HTMLDivElement, SearchItemProps>(
1925

2026
const handleClick = useCallback(
2127
(event: React.MouseEvent<HTMLElement>) => {
22-
onClick(value, event);
28+
onClick(item, event);
2329
},
24-
[onClick, value]
30+
[onClick, item]
2531
);
2632

2733
const handleKeyDown = useCallback(
2834
(event: React.KeyboardEvent<HTMLElement>) => {
29-
onKeyDown(value, event);
35+
onKeyDown(item, event);
3036
},
31-
[onKeyDown, value]
37+
[onKeyDown, item]
3238
);
3339

3440
return (

packages/iris-grid/src/sidebar/visibility-ordering-builder/SearchWithModal.scss

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@
2424
flex-direction: column;
2525
max-height: inherit;
2626

27-
.tree-container {
28-
max-height: inherit;
27+
.search-tree-container {
28+
/**
29+
* Prevents the virtualized list from rendering all items
30+
* and then popper limiting the height to a reasonable amount
31+
* since popper needs to know the internal height to limit the container after
32+
*/
33+
max-height: 100vh;
2934
padding: $spacer-1;
3035
overflow: auto;
3136
flex-grow: 1;

packages/iris-grid/src/sidebar/visibility-ordering-builder/SearchWithModal.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ import './SearchWithModal.scss';
1414
import MemoizedSearchItem from './SearchItem';
1515

1616
interface SearchWithModalProps {
17-
items: FlattenedIrisGridTreeItem[];
17+
items: readonly FlattenedIrisGridTreeItem[];
1818
onModalOpenChange: (isOpen: boolean) => void;
19-
onClick: (name: string, event: React.MouseEvent<HTMLElement>) => void;
19+
onClick: (
20+
item: FlattenedIrisGridTreeItem,
21+
event: React.MouseEvent<HTMLElement>
22+
) => void;
2023
onDragStart?: (event: DragStartEvent) => void;
21-
setSelection: (columnNames: string[]) => void;
24+
setSelection: (items: readonly FlattenedIrisGridTreeItem[]) => void;
2225
}
2326

2427
export function SearchWithModal({
@@ -94,8 +97,8 @@ export function SearchWithModal({
9497
});
9598

9699
const handleClick = useCallback(
97-
(name: string, event: React.MouseEvent<HTMLElement>) => {
98-
onClick(name, event);
100+
(item: FlattenedIrisGridTreeItem, event: React.MouseEvent<HTMLElement>) => {
101+
onClick(item, event);
99102
if (!event.shiftKey && !GridUtils.isModifierKeyDown(event)) {
100103
handleModalClose();
101104
}
@@ -104,13 +107,16 @@ export function SearchWithModal({
104107
);
105108

106109
const handleItemKeyDown = useCallback(
107-
(name: string, event: React.KeyboardEvent<HTMLElement>) => {
110+
(
111+
item: FlattenedIrisGridTreeItem,
112+
event: React.KeyboardEvent<HTMLElement>
113+
) => {
108114
const { key } = event;
109115
if (key === 'Enter') {
110116
// Select item and close modal
111117
event.preventDefault();
112118
event.stopPropagation();
113-
setSelection([name]);
119+
setSelection([item]);
114120
handleModalClose();
115121
} else if (key === 'ArrowDown') {
116122
// Move focus to the next item
@@ -191,7 +197,7 @@ export function SearchWithModal({
191197
e.preventDefault();
192198
// Select the first item in the list
193199
const firstItem = filteredItems[0];
194-
setSelection([firstItem.id]);
200+
setSelection([firstItem]);
195201
handleModalClose();
196202
}
197203

@@ -208,8 +214,7 @@ export function SearchWithModal({
208214
);
209215

210216
const handleSelectMatching = useCallback(() => {
211-
const matchingNames = filteredItems.map(item => item.id);
212-
setSelection(matchingNames);
217+
setSelection(filteredItems);
213218
handleModalClose();
214219
}, [filteredItems, setSelection, handleModalClose]);
215220

@@ -266,11 +271,13 @@ export function SearchWithModal({
266271
<div className="no-results">No matching columns</div>
267272
) : (
268273
<>
269-
<SortableTree
270-
items={filteredItems}
271-
withDepthMarkers={false}
272-
renderItem={renderItem}
273-
/>
274+
<div className="search-tree-container">
275+
<SortableTree
276+
items={filteredItems}
277+
withDepthMarkers={false}
278+
renderItem={renderItem}
279+
/>
280+
</div>
274281
{showFooterButtons && (
275282
<div className="footer-buttons">
276283
{hasMultipleSelection && (

packages/iris-grid/src/sidebar/visibility-ordering-builder/VisibilityOrderingBuilder.scss

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
.visibility-ordering-list {
3131
overflow-y: auto;
3232
flex-grow: 1;
33+
padding: $spacer-1;
3334

3435
.btn {
3536
height: 28px;
@@ -72,8 +73,4 @@
7273
margin-left: $spacer-1;
7374
height: 1rem;
7475
}
75-
76-
.tree-container {
77-
padding: $spacer-1;
78-
}
7976
}

packages/iris-grid/src/sidebar/visibility-ordering-builder/VisibilityOrderingBuilder.test.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,31 @@ const NESTED_COLUMN_HEADER_GROUPS = [
5555
] satisfies (Omit<DhType.ColumnGroup, 'color'> & { color?: string | null })[];
5656

5757
window.HTMLElement.prototype.scroll = jest.fn();
58+
window.HTMLElement.prototype.scrollTo = jest.fn();
5859
window.HTMLElement.prototype.scrollIntoView = jest.fn();
5960

61+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
62+
jest.mock<typeof import('@tanstack/react-virtual')>(
63+
'@tanstack/react-virtual',
64+
() => {
65+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
66+
const actual = jest.requireActual<typeof import('@tanstack/react-virtual')>(
67+
'@tanstack/react-virtual'
68+
);
69+
return {
70+
...actual,
71+
useVirtualizer(options) {
72+
return actual.useVirtualizer({
73+
...options,
74+
observeElementRect: (instance, callback) => {
75+
if (instance.scrollElement) callback({ width: 300, height: 1000 });
76+
},
77+
});
78+
},
79+
};
80+
}
81+
);
82+
6083
function Builder({
6184
model = makeModel(),
6285
hiddenColumns = [],
@@ -1743,15 +1766,16 @@ describe('Search', () => {
17431766
).toBe(2 * COLUMNS.length)
17441767
);
17451768

1746-
// Globally mocked scrollIntoView for this test file
1747-
jest.mocked(window.HTMLElement.prototype.scrollIntoView).mockClear();
1769+
// Virtualizer calls scrollTo on the container element
1770+
const mockScrollTo = jest.fn();
1771+
HTMLElement.prototype.scrollTo = mockScrollTo;
17481772

17491773
// Click the search item
1750-
await user.click(screen.getAllByText(COLUMNS.at(-1)!.name)[1]);
1774+
await user.click(screen.getAllByText(COLUMNS[COLUMNS.length - 1].name)[1]);
17511775

17521776
await waitFor(() => {
1753-
const item = screen.getByText(COLUMNS.at(-1)!.name);
1754-
expect(item.scrollIntoView).toHaveBeenCalledTimes(1);
1777+
const item = screen.getByText(COLUMNS[COLUMNS.length - 1].name);
1778+
expect(mockScrollTo).toHaveBeenCalledTimes(1);
17551779
expect(item.closest('.tree-item')).toHaveFocus();
17561780
});
17571781
});

0 commit comments

Comments
 (0)