Skip to content

Commit e1c2dc4

Browse files
authored
feat: DH-18960: Improve column selection functionality for large tables (#2555)
1 parent 4ae31c3 commit e1c2dc4

28 files changed

Lines changed: 1659 additions & 994 deletions

package-lock.json

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

packages/components/src/popper/Popper.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ $animation-offset: 10px;
77
.popper-container {
88
position: absolute;
99
z-index: 5000;
10+
11+
.spectrum-theme-provider {
12+
max-height: inherit;
13+
}
1014
}
1115

1216
.popper.popper-tooltip {
@@ -25,9 +29,11 @@ $animation-offset: 10px;
2529
opacity $transition;
2630
pointer-events: none;
2731
outline: 0;
32+
max-height: inherit;
2833

2934
.popper-content {
3035
position: relative;
36+
max-height: inherit;
3137

3238
.tooltip-content {
3339
text-align: center;

packages/components/src/popper/Popper.tsx

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,48 @@ import { SpectrumThemeProvider } from '../theme/SpectrumThemeProvider';
2626

2727
const POPPER_CLASS_NAME = 'popper';
2828

29+
const KEEP_IN_PARENT_OPTIONS: PopperOptions = {
30+
placement: 'bottom-end',
31+
modifiers: {
32+
preventOverflow: {
33+
boundariesElement: 'scrollParent',
34+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
35+
fn: (data, options: any) => {
36+
const modified = PopperJs.Defaults.modifiers?.preventOverflow?.fn?.(
37+
data,
38+
options
39+
);
40+
41+
if (modified == null) {
42+
return data;
43+
}
44+
45+
modified.styles.maxHeight = `${
46+
document.documentElement.clientHeight -
47+
data.offsets.popper.top -
48+
2 * options.padding // Double padding because there is top and bottom to account for
49+
}px`;
50+
return modified ?? data;
51+
},
52+
},
53+
flip: {
54+
enabled: false,
55+
},
56+
},
57+
};
58+
2959
interface PopperProps {
3060
children: React.ReactNode;
3161
options: PopperOptions;
3262
className: string;
3363
timeout: number;
3464
onEntered: () => void;
3565
onExited: () => void;
66+
onBlur: (e: React.FocusEvent) => void;
3667
isShown: boolean;
3768
closeOnBlur: boolean;
3869
interactive: boolean;
70+
keepInParent: boolean;
3971
referenceObject: ReferenceObject | null;
4072
'data-testid'?: string;
4173
}
@@ -56,9 +88,13 @@ class Popper extends Component<PopperProps, PopperState> {
5688
onExited(): void {
5789
// no-op
5890
},
91+
onBlur(): void {
92+
// no-op
93+
},
5994
isShown: false,
6095
interactive: false,
6196
closeOnBlur: false,
97+
keepInParent: false,
6298
referenceObject: null,
6399
'data-testid': undefined,
64100
};
@@ -87,16 +123,21 @@ class Popper extends Component<PopperProps, PopperState> {
87123

88124
componentDidUpdate(prevProps: PopperProps): void {
89125
const { isShown } = this.props;
126+
const { popper } = this.state;
90127

91128
if (prevProps.isShown !== isShown) {
92-
if (isShown) {
93-
cancelAnimationFrame(this.rAF);
94-
this.rAF = window.requestAnimationFrame(() => {
129+
cancelAnimationFrame(this.rAF);
130+
this.rAF = window.requestAnimationFrame(() => {
131+
if (isShown) {
95132
this.show();
96-
});
97-
} else {
98-
this.hide();
99-
}
133+
} else {
134+
this.hide();
135+
}
136+
});
137+
}
138+
139+
if (popper) {
140+
popper.scheduleUpdate();
100141
}
101142
}
102143

@@ -138,12 +179,27 @@ class Popper extends Component<PopperProps, PopperState> {
138179
return;
139180
}
140181

141-
let { options } = this.props;
142-
options = {
143-
placement: 'auto',
144-
modifiers: { preventOverflow: { boundariesElement: 'viewport' } },
145-
...options,
146-
};
182+
const { options: optionsProp, keepInParent } = this.props;
183+
const defaultOptions = keepInParent
184+
? KEEP_IN_PARENT_OPTIONS
185+
: ({
186+
placement: 'auto',
187+
modifiers: { preventOverflow: { boundariesElement: 'viewport' } },
188+
} satisfies PopperOptions);
189+
190+
const options = {
191+
...defaultOptions,
192+
...optionsProp,
193+
modifiers: {
194+
...defaultOptions.modifiers,
195+
...optionsProp.modifiers,
196+
preventOverflow: {
197+
...defaultOptions.modifiers?.preventOverflow,
198+
...optionsProp.modifiers?.preventOverflow,
199+
},
200+
},
201+
} satisfies PopperOptions;
202+
147203
document.body.appendChild(this.element);
148204

149205
let parent = this.getVisibleElement(this.container.current);
@@ -223,11 +279,15 @@ class Popper extends Component<PopperProps, PopperState> {
223279
}
224280

225281
handleBlur(e: React.FocusEvent): void {
282+
const { closeOnBlur, onBlur } = this.props;
226283
if (!(e.relatedTarget instanceof HTMLElement)) {
227284
return;
228285
}
229286
if (!this.element.contains(e.relatedTarget)) {
230-
this.hide();
287+
onBlur?.(e);
288+
if (closeOnBlur) {
289+
this.hide();
290+
}
231291
}
232292
}
233293

@@ -274,7 +334,7 @@ class Popper extends Component<PopperProps, PopperState> {
274334
{ interactive },
275335
className
276336
)}
277-
onBlur={closeOnBlur ? this.handleBlur : undefined}
337+
onBlur={this.handleBlur}
278338
tabIndex={closeOnBlur ? -1 : undefined}
279339
role="presentation"
280340
>

packages/components/src/theme/theme-dark/theme-dark-components.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149

150150
/* Popovers */
151151
--dh-color-popover-bg: var(--dh-color-bg);
152+
--dh-color-popover-border: var(--dh-color-gray-400);
152153

153154
/* Tooltips */
154155
--dh-color-tooltip-bg: var(--dh-color-gray-400);

packages/components/src/theme/theme-light/theme-light-components.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149

150150
/* Popovers */
151151
--dh-color-popover-bg: var(--dh-color-gray-50);
152+
--dh-color-popover-border: var(--dh-color-gray-400);
152153

153154
/* Tooltips */
154155
--dh-color-tooltip-bg: var(--dh-color-gray-50);

packages/iris-grid/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
"@deephaven/storage": "file:../storage",
4545
"@deephaven/utils": "file:../utils",
4646
"@dnd-kit/core": "^6.1.0",
47-
"@dnd-kit/sortable": "^7.0.2",
47+
"@dnd-kit/modifiers": "^9.0.0",
48+
"@dnd-kit/sortable": "^10.0.0",
4849
"@dnd-kit/utilities": "^3.2.2",
4950
"@fortawesome/react-fontawesome": "^0.2.0",
5051
"@hello-pangea/dnd": "^18.0.1",
@@ -60,8 +61,8 @@
6061
"react-transition-group": "^4.4.2"
6162
},
6263
"peerDependencies": {
63-
"react": ">=16.8.0",
64-
"react-dom": ">=16.8.0"
64+
"react": ">=18.0.0",
65+
"react-dom": ">=18.0.0"
6566
},
6667
"devDependencies": {
6768
"@deephaven/jsapi-shim": "file:../jsapi-shim",

packages/iris-grid/src/IrisGrid.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2647,12 +2647,12 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
26472647
modelIndexes.forEach(modelIndex => {
26482648
const defaultWidth =
26492649
metricCalculator.initialColumnWidths.get(modelIndex);
2650-
const calculatedWidth = getOrThrow(
2651-
metrics.calculatedColumnWidths,
2652-
modelIndex
2653-
);
2650+
const calculatedWidth = metrics.calculatedColumnWidths.get(modelIndex);
26542651

2655-
if (defaultWidth !== calculatedWidth) {
2652+
// If we haven't scrolled to the column, the calculated width will be undefined.
2653+
// This means the function was triggered by the visibility ordering menu,
2654+
// so just reset the column width to default.
2655+
if (defaultWidth !== calculatedWidth && calculatedWidth != null) {
26562656
metricCalculator.setColumnWidth(modelIndex, calculatedWidth);
26572657
} else {
26582658
metricCalculator.resetColumnWidth(modelIndex);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { forwardRef, memo, useCallback } from 'react';
2+
import classNames from 'classnames';
3+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4+
import { vsGripper } from '@deephaven/icons';
5+
import { Tooltip } from '@deephaven/components';
6+
import { type FlattenedIrisGridTreeItem } from './sortable-tree/utilities';
7+
8+
type SearchItemProps = {
9+
value: string;
10+
item: FlattenedIrisGridTreeItem;
11+
onClick: (name: string, event: React.MouseEvent<HTMLElement>) => void;
12+
onKeyDown: (name: string, event: React.KeyboardEvent<HTMLElement>) => void;
13+
handleProps?: Record<string, unknown>;
14+
};
15+
16+
const SearchItem = forwardRef<HTMLDivElement, SearchItemProps>(
17+
function SearchItem(props, ref) {
18+
const { value, item, onClick, onKeyDown, handleProps } = props;
19+
20+
const handleClick = useCallback(
21+
(event: React.MouseEvent<HTMLElement>) => {
22+
onClick(value, event);
23+
},
24+
[onClick, value]
25+
);
26+
27+
const handleKeyDown = useCallback(
28+
(event: React.KeyboardEvent<HTMLElement>) => {
29+
onKeyDown(value, event);
30+
},
31+
[onKeyDown, value]
32+
);
33+
34+
return (
35+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
36+
<div
37+
ref={ref}
38+
className={classNames('tree-item', {
39+
isSelected: item.selected,
40+
})}
41+
onClick={handleClick}
42+
onKeyDownCapture={handleKeyDown}
43+
data-index={item.index}
44+
// eslint-disable-next-line react/jsx-props-no-spreading
45+
{...handleProps}
46+
>
47+
<span title={value} className={classNames('column-name')}>
48+
{value}
49+
</span>
50+
<div>
51+
<Tooltip>Drag to re-order</Tooltip>
52+
<FontAwesomeIcon icon={vsGripper} />
53+
</div>
54+
</div>
55+
);
56+
}
57+
);
58+
59+
const MemoizedSearchItem = memo(SearchItem);
60+
61+
export default MemoizedSearchItem;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
@import '../../IrisGrid.scss';
2+
3+
.visibility-search-list {
4+
width: 0.75 * $table-sidebar-max-width;
5+
border: 1px solid var(--dh-color-popover-border);
6+
7+
.popper-arrow {
8+
display: none;
9+
}
10+
11+
.tree-item {
12+
padding: $spacer-1;
13+
}
14+
15+
.no-results {
16+
padding: $spacer-1;
17+
margin: auto;
18+
color: var(--dh-color-text-disabled);
19+
}
20+
}
21+
22+
.visibility-search-list-inner {
23+
display: flex;
24+
flex-direction: column;
25+
max-height: inherit;
26+
27+
.tree-container {
28+
max-height: inherit;
29+
padding: $spacer-1;
30+
overflow: auto;
31+
flex-grow: 1;
32+
flex-shrink: 1;
33+
}
34+
35+
.footer-buttons {
36+
border-top: 1px solid var(--dh-color-popover-border);
37+
flex-grow: 0;
38+
flex-shrink: 0;
39+
display: flex;
40+
flex-direction: row;
41+
justify-content: space-between;
42+
43+
& > * {
44+
flex-grow: 1;
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)