From 150882b2dfdb13275855a56954a86ceb396b0ca7 Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Mon, 6 Nov 2023 11:30:41 -0600 Subject: [PATCH] Split GridScrollbar into separate component --- cmp/grid/Grid.ts | 125 ++++----------------------------- cmp/grid/impl/GridScrollbar.ts | 119 +++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 110 deletions(-) create mode 100644 cmp/grid/impl/GridScrollbar.ts diff --git a/cmp/grid/Grid.ts b/cmp/grid/Grid.ts index 3aff657b30..3ba2711a4d 100644 --- a/cmp/grid/Grid.ts +++ b/cmp/grid/Grid.ts @@ -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 {gridScrollbar} from '@xh/hoist/cmp/grid/impl/GridScrollbar'; import {getAgGridMenuItems} from '@xh/hoist/cmp/grid/impl/MenuSupport'; -import {div, fragment, vframe} from '@xh/hoist/cmp/layout'; +import {div, frame, vframe} from '@xh/hoist/cmp/layout'; import { hoistCmp, HoistModel, @@ -38,14 +39,12 @@ import type { GridReadyEvent, ProcessCellForExportParams } from '@xh/hoist/kit/ag-grid'; -import {computed, makeObservable, observer} from '@xh/hoist/mobx'; +import {computed, observer} from '@xh/hoist/mobx'; import {wait} from '@xh/hoist/promise'; -import {consumeEvent, isDisplayed, logDebug, logWithDebug, observeResize} from '@xh/hoist/utils/js'; -import {getLayoutProps} from '@xh/hoist/utils/react'; +import {consumeEvent, isDisplayed, logDebug, logWithDebug} from '@xh/hoist/utils/js'; +import {createObservableRef, getLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; -import {debounce, isEmpty, isEqual, isNil, max, maxBy, merge, sumBy} from 'lodash'; -import {action, observable} from 'mobx'; -import {createRef} from 'react'; +import {debounce, isEmpty, isEqual, isNil, max, maxBy, merge} from 'lodash'; import './Grid.scss'; import {GridModel} from './GridModel'; import {columnGroupHeader} from './impl/ColumnGroupHeader'; @@ -96,7 +95,6 @@ export const [Grid, grid] = hoistCmp.withFactory({ const {store, treeMode, treeStyle, highlightRowOnClick, colChooserModel, filterModel} = model, impl = useLocalModel(GridLocalModel), - {scrollerRef, viewportWidth, visibleColumnWidth, SCROLLBAR_SIZE} = impl, platformColChooser = XH.isMobileApp ? mobileColChooser : desktopColChooser, maxDepth = impl.isHierarchical ? store.maxDepth : null; @@ -109,8 +107,10 @@ export const [Grid, grid] = hoistCmp.withFactory({ highlightRowOnClick ? 'xh-grid--highlight-row-on-click' : null ); - return fragment( - vframe({ + const container = model.experimental['enableFullWidthScroll'] ? vframe : frame; + + return container( + frame({ className, items: [ agGrid({ @@ -118,25 +118,9 @@ export const [Grid, grid] = hoistCmp.withFactory({ ...getLayoutProps(props), ...impl.agOptions }), - div({ - className: 'xh-grid__scroll-viewport', - omit: !impl.isFullWidthScrollEnabled || viewportWidth > visibleColumnWidth, - item: div({ - className: 'xh-grid__scroll-viewport__container', - style: { - height: SCROLLBAR_SIZE, - width: visibleColumnWidth - } - }), - onScroll: e => { - impl.scrollViewport((e.target as HTMLDivElement).scrollLeft); - }, - ref: scrollerRef, - style: { - height: SCROLLBAR_SIZE, - overflowX: 'auto', - overflowY: 'hidden' - } + gridScrollbar({ + omit: !model.experimental['enableFullWidthScroll'], + viewRef: impl.viewRef }) ], testId, @@ -160,7 +144,7 @@ class GridLocalModel extends HoistModel { @lookup(GridModel) private model: GridModel; agOptions: GridOptions; - viewRef = createRef(); + viewRef = createObservableRef(); private rowKeyNavSupport: RowKeyNavSupport; private prevRs: RecordSet; @@ -297,7 +281,7 @@ class GridLocalModel extends HoistModel { } // Support for FullWidthScroll - if (this.isFullWidthScrollEnabled) { + if (model.experimental['enableFullWidthScroll']) { ret.suppressHorizontalScroll = true; } @@ -870,83 +854,4 @@ class GridLocalModel extends HoistModel { consumeEvent(event); } }; - - //----------------------------- - // Support for FullWidthScroll - //----------------------------- - - readonly SCROLLBAR_SIZE = 10; - readonly scrollerRef = createRef(); - - @observable viewportWidth: number; - @observable private isVerticalScrollbarVisible = false; - - private viewportResizeObserver: ResizeObserver; - - @computed - get isFullWidthScrollEnabled(): boolean { - return this.model.experimental['enableFullWidthScroll']; - } - - get visibleColumnWidth(): number { - const {model, SCROLLBAR_SIZE} = this; - return ( - sumBy(model.columnState, it => - it.hidden ? 0 : it.width ?? model.getColumn(it.colId).minWidth ?? 0 - ) + (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'); - } - - constructor() { - super(); - makeObservable(this); - } - - scrollScroller(left: number) { - this.scrollerRef.current.scrollLeft = left; - } - - scrollViewport(left: number) { - this.agViewport.scrollLeft = left; - } - - override afterLinked() { - if (!this.isFullWidthScrollEnabled) return; - this.addReaction({ - track: () => this.model.isReady, - run: isReady => { - if (!isReady) return; - const {agViewport, viewportResizeObserver} = this; - this.viewportWidth = agViewport.clientWidth; - agViewport.addEventListener('scroll', e => - this.scrollScroller((e.target as HTMLDivElement).scrollLeft) - ); - viewportResizeObserver?.disconnect(); - this.viewportResizeObserver = observeResize( - rect => this.onViewResized(rect), - agViewport, - {debounce: 100} - ); - } - }); - } - - override destroy() { - super.destroy(); - this.viewportResizeObserver?.disconnect(); - } - - @action - private onViewResized({width}: DOMRect) { - this.viewportWidth = width; - this.isVerticalScrollbarVisible = !!this.agVerticalScrollContainer.clientHeight; - } } diff --git a/cmp/grid/impl/GridScrollbar.ts b/cmp/grid/impl/GridScrollbar.ts new file mode 100644 index 0000000000..ce00e1c783 --- /dev/null +++ b/cmp/grid/impl/GridScrollbar.ts @@ -0,0 +1,119 @@ +import {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'; + +export interface GridScrollbarProps extends HoistProps { + viewRef: RefObject; +} + +export const gridScrollbar = hoistCmp.factory({ + className: 'xh-grid__grid-scrollbar', + render({className}) { + const impl = useLocalModel(GridScrollbarModel), + {scrollerRef, viewportWidth, visibleColumnWidth, SCROLLBAR_SIZE} = impl; + + return div({ + className, + omit: viewportWidth > visibleColumnWidth, + 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, + overflowX: 'auto', + overflowY: 'hidden' + } + }); + } +}); + +class GridScrollbarModel extends HoistModel { + readonly SCROLLBAR_SIZE = 10; + readonly scrollerRef = createRef(); + + @observable viewportWidth: number; + @observable private isVerticalScrollbarVisible = false; + + private viewportResizeObserver: ResizeObserver; + + get visibleColumnWidth(): number { + const {model, SCROLLBAR_SIZE} = this; + return ( + sumBy(model.columnState, it => + it.hidden ? 0 : it.width ?? model.getColumn(it.colId).minWidth ?? 0 + ) + (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 model(): GridModel { + return this.componentProps.model as GridModel; + } + + private get viewRef(): RefObject { + return this.componentProps.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.model.isReady, + run: () => { + const {agViewport, viewportResizeObserver} = this; + this.viewportWidth = agViewport.clientWidth; + agViewport.addEventListener('scroll', e => + this.scrollScroller((e.target as HTMLDivElement).scrollLeft) + ); + viewportResizeObserver?.disconnect(); + this.viewportResizeObserver = observeResize( + rect => this.onViewResized(rect), + agViewport, + {debounce: 100} + ); + } + }); + } + + override destroy() { + super.destroy(); + this.viewportResizeObserver?.disconnect(); + } + + @action + private onViewResized({width}: DOMRect) { + this.viewportWidth = width; + this.isVerticalScrollbarVisible = !!this.agVerticalScrollContainer.clientHeight; + } +}