Skip to content

Commit

Permalink
Add experimental enableFullWidthScroll Grid feature (#3517)
Browse files Browse the repository at this point in the history
* Add experimental `enableFullWidthScroll` Grid feature

* Split GridScrollbar into separate component

* Changes from CR
  • Loading branch information
ghsolomon authored Nov 7, 2023
1 parent c4a1eb2 commit 5f87521
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

* Performance improvement to `HoistComponent`: Prevent unnecessary re-renderings resulting from
spurious model lookup changes.
* New flag `GridModel.experimental.enableFullWidthScroll` enables scrollbars to span pinned columns.

## 59.2.0 - 2023-10-16

Expand Down
36 changes: 25 additions & 11 deletions cmp/grid/Grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import composeRefs from '@seznam/compose-react-refs';
import {agGrid, AgGrid} from '@xh/hoist/cmp/ag-grid';
import {getTreeStyleClasses} from '@xh/hoist/cmp/grid';
import {gridHScrollbar} from '@xh/hoist/cmp/grid/impl/GridHScrollbar';
import {getAgGridMenuItems} from '@xh/hoist/cmp/grid/impl/MenuSupport';
import {div, fragment, frame} from '@xh/hoist/cmp/layout';
import {div, fragment, frame, vframe} from '@xh/hoist/cmp/layout';
import {
hoistCmp,
HoistModel,
Expand Down Expand Up @@ -41,10 +42,9 @@ import type {
import {computed, observer} from '@xh/hoist/mobx';
import {wait} from '@xh/hoist/promise';
import {consumeEvent, isDisplayed, logDebug, logWithDebug} from '@xh/hoist/utils/js';
import {getLayoutProps} from '@xh/hoist/utils/react';
import {createObservableRef, getLayoutProps} from '@xh/hoist/utils/react';
import classNames from 'classnames';
import {debounce, isEmpty, isEqual, isNil, max, maxBy, merge} from 'lodash';
import {createRef} from 'react';
import './Grid.scss';
import {GridModel} from './GridModel';
import {columnGroupHeader} from './impl/ColumnGroupHeader';
Expand Down Expand Up @@ -107,14 +107,23 @@ export const [Grid, grid] = hoistCmp.withFactory<GridProps>({
highlightRowOnClick ? 'xh-grid--highlight-row-on-click' : null
);

const {enableFullWidthScroll} = model.experimental,
container = enableFullWidthScroll ? vframe : frame;

return fragment(
frame({
container({
className,
item: agGrid({
model: model.agGridModel,
...getLayoutProps(props),
...impl.agOptions
}),
items: [
agGrid({
model: model.agGridModel,
...getLayoutProps(props),
...impl.agOptions
}),
gridHScrollbar({
omit: !enableFullWidthScroll,
gridLocalModel: impl
})
],
testId,
onKeyDown: impl.onKeyDown,
ref: composeRefs(impl.viewRef, ref)
Expand All @@ -130,13 +139,13 @@ export const [Grid, grid] = hoistCmp.withFactory<GridProps>({
//------------------------
// Implementation
//------------------------
class GridLocalModel extends HoistModel {
export class GridLocalModel extends HoistModel {
override xhImpl = true;

@lookup(GridModel)
private model: GridModel;
agOptions: GridOptions;
viewRef = createRef<HTMLElement>();
viewRef = createObservableRef<HTMLElement>();
private rowKeyNavSupport: RowKeyNavSupport;
private prevRs: RecordSet;

Expand Down Expand Up @@ -272,6 +281,11 @@ class GridLocalModel extends HoistModel {
};
}

// Support for FullWidthScroll
if (model.experimental.enableFullWidthScroll) {
ret.suppressHorizontalScroll = true;
}

return ret;
}

Expand Down
140 changes: 140 additions & 0 deletions cmp/grid/impl/GridHScrollbar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {GridLocalModel, GridModel} from '@xh/hoist/cmp/grid';
import {div} from '@xh/hoist/cmp/layout';
import {hoistCmp, HoistModel, HoistProps, useLocalModel} from '@xh/hoist/core';
import {makeObservable} from '@xh/hoist/mobx';
import {observeResize} from '@xh/hoist/utils/js';
import {sumBy} from 'lodash';
import {action, observable} from 'mobx';
import {createRef, RefObject} from 'react';

/**
* Implementation for Grid's full-width horizontal scrollbar, to span pinned columns
* @internal
*/

export interface GridHScrollbarProps extends HoistProps<GridModel> {
gridLocalModel: GridLocalModel;
}

export const gridHScrollbar = hoistCmp.factory<GridHScrollbarProps>({
className: 'xh-grid__grid-hscrollbar',
render({className}) {
const impl = useLocalModel(GridHScrollbarModel),
{scrollerRef, viewWidth, visibleColumnWidth, SCROLLBAR_SIZE} = impl;

if (viewWidth > visibleColumnWidth) return null;

return div({
className,
item: div({
className: `${className}__filler`,
style: {
height: SCROLLBAR_SIZE,
width: visibleColumnWidth
}
}),
onScroll: e => {
impl.scrollViewport((e.target as HTMLDivElement).scrollLeft);
},
ref: scrollerRef,
style: {
height: SCROLLBAR_SIZE, // TODO: make this a property on GridModel to apply to both scrollbars
overflowX: 'auto',
overflowY: 'hidden'
}
});
}
});

class GridHScrollbarModel extends HoistModel {
readonly SCROLLBAR_SIZE = 10;
readonly scrollerRef = createRef<HTMLDivElement>();

@observable viewWidth: number;
@observable private isVerticalScrollbarVisible = false;

/** Observe AG's viewport to detect when vertical scrollbar visibility changes */
private agViewportResizeObserver: ResizeObserver;
/** Observe overall view to detect when horizontal scrollbar is needed */
private viewResizeObserver: ResizeObserver;

get visibleColumnWidth(): number {
const {gridModel, SCROLLBAR_SIZE} = this;
return (
sumBy(gridModel.columnState, ({colId, hidden, width}) => {
if (hidden) return 0;
const minWidth = gridModel.getColumn(colId).minWidth ?? 0;
if (width) return Math.max(width, minWidth);
return minWidth;
}) + (this.isVerticalScrollbarVisible ? SCROLLBAR_SIZE : 0)
);
}

private get agViewport(): HTMLDivElement {
return this.viewRef.current.querySelector('.ag-center-cols-viewport');
}

private get agVerticalScrollContainer(): HTMLDivElement {
return this.viewRef.current.querySelector('.ag-body-vertical-scroll-container');
}

private get gridModel(): GridModel {
return this.componentProps.model as GridModel;
}

private get viewRef(): RefObject<HTMLElement> {
return this.componentProps.gridLocalModel.viewRef;
}

constructor() {
super();
makeObservable(this);
}

scrollScroller(left: number) {
this.scrollerRef.current.scrollLeft = left;
}

scrollViewport(left: number) {
this.agViewport.scrollLeft = left;
}

override afterLinked() {
this.addReaction({
when: () => this.viewRef.current && this.gridModel.isReady,
run: () => {
const {agViewport, viewRef} = this;
this.viewWidth = viewRef.current.clientWidth;
agViewport.addEventListener('scroll', e =>
this.scrollScroller((e.target as HTMLDivElement).scrollLeft)
);
this.agViewportResizeObserver = observeResize(
() => this.onAgViewportResized(),
agViewport,
{debounce: 100}
);
this.viewResizeObserver = observeResize(
rect => this.onViewResized(rect),
viewRef.current,
{debounce: 100}
);
}
});
}

override destroy() {
super.destroy();
this.agViewportResizeObserver?.disconnect();
this.viewResizeObserver?.disconnect();
}

@action
private onAgViewportResized() {
this.isVerticalScrollbarVisible = !!this.agVerticalScrollContainer.clientHeight;
}

@action
private onViewResized({width}: DOMRect) {
this.viewWidth = width;
}
}

0 comments on commit 5f87521

Please sign in to comment.