Skip to content

Commit

Permalink
Keep header and body in sync when the DataGrid columns are overflowing
Browse files Browse the repository at this point in the history
…microsoft#176

Previously, when using resizableColumns and resizableColumnsOptions.autoFitColumns = false, only the body or the header could be scrolled and they would be out of sync

This changes synchronies the scrolling between the two so scrolling horizontally on either affects the other as if you were scrolling on a shared parent
  • Loading branch information
rocketBANG committed Nov 21, 2024
1 parent 70b19d2 commit b3af165
Show file tree
Hide file tree
Showing 16 changed files with 278 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import * as React from 'react';
import { useDataGrid_unstable } from './useDataGrid';
import { renderDataGrid_unstable } from './renderDataGrid';

import {
renderDataGrid_unstable,
useDataGridStyles_unstable,
useDataGridContextValues_unstable,
DataGridProps,
} from '@fluentui/react-components';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import { useDataGridStyles_unstable } from './useDataGridStyles.styles';

export const DataGrid: ForwardRefComponent<DataGridProps> = React.forwardRef(
(props, ref) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import {
DataGridContextValues,
DataGridState,
renderDataGrid_unstable as baseRender,
} from '@fluentui/react-components';
import { HeaderRefContextProvider } from '../../contexts/headerRefContext';
import { BodyRefContextProvider } from '../../contexts/bodyRefContext';

/**
* Render the final JSX of DataGrid
*/
export const renderDataGrid_unstable = (
state: DataGridState,
contextValues: DataGridContextValues
) => {
const headerRef = React.useRef<HTMLDivElement | null>(null);
const bodyRef = React.useRef<HTMLDivElement | null>(null);

return (
<HeaderRefContextProvider value={headerRef}>
<BodyRefContextProvider value={bodyRef}>
{baseRender(state, contextValues)}
</BodyRefContextProvider>
</HeaderRefContextProvider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
DataGridState,
makeStyles,
mergeClasses,
useDataGridStyles_unstable as useDataGridStylesBase_unstable,
} from '@fluentui/react-components';

const useStyles = makeStyles({
root: {
// DataGrid gets min-width: fit-content applied directly to the element, thus the need for !important
// without auto width, the DataGrid will not scroll
minWidth: 'auto !important',
},
});

/**
* Apply styling to the DataGrid slots based on the state
*/
export const useDataGridStyles_unstable = (
state: DataGridState
): DataGridState => {
const classes = useStyles();
state.root.className = mergeClasses(classes.root, state.root.className);

useDataGridStylesBase_unstable(state);
return state;
};
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,6 @@ export type DataGridBodyState = Omit<DataGridBodyStateBase, 'renderRow'> &
virtualizedRow: (props: ListChildComponentProps) => React.ReactElement;
} & {
listProps?: Partial<FixedSizeListProps>;

outerRef?: React.Ref<unknown>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const renderDataGridBody_unstable = (state: DataGridBodyState) => {
height={state.height}
itemCount={state.rows.length}
direction={dir}
outerRef={state.outerRef}
{...state.listProps}
>
{state.virtualizedRow}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
} from '@fluentui/react-components';
import type { RowRenderFunction } from '@fluentui/react-table';
import { TableRowIndexContextProvider } from '../../contexts/rowIndexContext';
import { useBodyRefContext } from '../../contexts/bodyRefContext';
import { useHeaderRefContext } from '../../contexts/headerRefContext';

/**
* Create the state required to render DataGridBody.
Expand All @@ -33,6 +35,9 @@ export const useDataGridBody_unstable = (
listProps,
} = props;

const bodyRef = useBodyRefContext();
const headerRef = useHeaderRefContext();

// cast the row render function to work with unknown args
const renderRowWithUnknown = children as RowRenderFunction;
const baseState = useDataGridBodyBase_unstable(
Expand All @@ -54,6 +59,28 @@ export const useDataGridBody_unstable = (
[ariaRowIndexStart, children]
);

const onScroll = React.useCallback(() => {
if (bodyRef.current && headerRef.current) {
headerRef.current.scroll({
left: bodyRef.current.scrollLeft,
behavior: 'instant',
});
}
}, []);

// Use a onScroll callback on the outerElement since react-window's onScroll will only work for horizontal scrolls
const setupOuterRef = React.useCallback(
(outerElement: HTMLElement | null) => {
bodyRef.current?.removeEventListener('scroll', onScroll);

bodyRef.current = outerElement;
if (outerElement) {
outerElement.addEventListener('scroll', onScroll);
}
},
[]
);

return {
...baseState,
itemSize,
Expand All @@ -62,5 +89,6 @@ export const useDataGridBody_unstable = (
width,
ariaRowIndexStart,
listProps,
outerRef: setupOuterRef,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';
import {
renderDataGridHeader_unstable,
type DataGridHeaderProps,
type ForwardRefComponent,
} from '@fluentui/react-components';
import { useDataGridHeader_unstable } from './useDataGridHeader';
import { useDataGridHeaderStyles_unstable } from './useDataGridHeaderStyles.styles';

/**
* DataGridHeader component
*/
export const DataGridHeader: ForwardRefComponent<DataGridHeaderProps> &
((props: DataGridHeaderProps) => JSX.Element) = React.forwardRef(
(props, ref) => {
const state = useDataGridHeader_unstable(props, ref);

useDataGridHeaderStyles_unstable(state);
return renderDataGridHeader_unstable(state);
}
) as ForwardRefComponent<DataGridHeaderProps> &
((props: DataGridHeaderProps) => JSX.Element);

DataGridHeader.displayName = 'DataGridHeader';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './DataGridHeader';
export * from './useDataGridHeader';
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as React from 'react';
import {
useDataGridHeader_unstable as useBaseState,
DataGridHeaderProps,
DataGridHeaderState,
} from '@fluentui/react-components';
import { useBodyRefContext } from '../../contexts/bodyRefContext';
import { useHeaderRefContext } from '../../contexts/headerRefContext';

const setRef = (ref: React.Ref<HTMLElement>, value: HTMLElement | null) => {
if (typeof ref === 'function') {
ref(value);
} else if (ref) {
(ref as React.MutableRefObject<HTMLElement | null>).current = value;
}
};

/**
* Create the state required to render DataGridHeader.
*
* The returned state can be modified with hooks such as useDataGridHeaderStyles_unstable,
* before being passed to renderDataGridHeader_unstable.
*
* @param props - props from this instance of DataGridHeader
* @param ref - reference to root HTMLElement of DataGridHeader
*/
export const useDataGridHeader_unstable = (
props: DataGridHeaderProps,
ref: React.Ref<HTMLElement>
): DataGridHeaderState => {
const bodyRef = useBodyRefContext();
const headerRef = useHeaderRefContext();

const onScroll = React.useCallback(() => {
if (bodyRef.current && headerRef.current) {
bodyRef.current.scroll({
left: headerRef.current.scrollLeft,
behavior: 'instant',
});
}
}, []);

const setupRef = React.useCallback((element: HTMLElement | null) => {
setRef(ref, element);
headerRef.current?.removeEventListener('scroll', onScroll);

headerRef.current = element;
if (element) {
element.addEventListener('scroll', onScroll);
}
}, []);

const baseState = useBaseState(props, setupRef);

return {
...baseState,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
makeStyles,
mergeClasses,
useDataGridHeaderStyles_unstable as useDataGridHeaderStylesBase_unstable,
DataGridHeaderState,
} from '@fluentui/react-components';

const useStyles = makeStyles({
root: {
overflowX: 'auto',
// Hide the scrollbar in the header, it is synced to the scrollbar of the body so shouldn't be shown
scrollbarWidth: 'none',
'::-webkit-scrollbar': {
width: 0,
height: 0,
},
},
});

/**
* Apply styling to the DataGridHeader slots based on the state
*/
export const useDataGridHeaderStyles_unstable = (
state: DataGridHeaderState
): DataGridHeaderState => {
const classes = useStyles();
state.root.className = mergeClasses(classes.root, state.root.className);

useDataGridHeaderStylesBase_unstable(state);
return state;
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import * as React from 'react';
import {
useDataGridRowStyles_unstable,
renderDataGridRow_unstable,
DataGridRowProps,
} from '@fluentui/react-components';
import type { ForwardRefComponent } from '@fluentui/react-components';
import { useDataGridRow_unstable } from './useDataGridRow.styles';
import { useDataGridRow_unstable } from './useDataGridRow';
import { useDataGridRowStyles_unstable } from './useDataGridRow.styles';

/**
* DataGridRow component
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import * as React from 'react';
import type {
DataGridRowProps,
import {
makeStyles,
mergeClasses,
useDataGridRowStyles_unstable as useDataGridRowStylesBase_unstable,
DataGridRowState,
} from '@fluentui/react-components';
import { useDataGridRow_unstable as useBaseState } from '@fluentui/react-components';
import { useTableRowIndexContext } from '../../contexts/rowIndexContext';

const useStyles = makeStyles({
root: {
minWidth: 'fit-content',
},
});

/**
* Create the state required to render DataGridRow.
*
* The returned state can be modified with hooks such as useDataGridRowStyles_unstable,
* before being passed to renderDataGridRow_unstable.
*
* @param props - props from this instance of DataGridRow
* @param ref - reference to root HTMLElement of DataGridRow
* Apply styling to the DataGridRow slots based on the state
*/
export const useDataGridRow_unstable = (
props: DataGridRowProps,
ref: React.Ref<HTMLElement>
export const useDataGridRowStyles_unstable = (
state: DataGridRowState
): DataGridRowState => {
const rowIndex = useTableRowIndexContext();
return useBaseState({ ...props, 'aria-rowindex': rowIndex }, ref);
const classes = useStyles();
state.root.className = mergeClasses(classes.root, state.root.className);

useDataGridRowStylesBase_unstable(state);
return state;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';
import type {
DataGridRowProps,
DataGridRowState,
} from '@fluentui/react-components';
import { useDataGridRow_unstable as useBaseState } from '@fluentui/react-components';
import { useTableRowIndexContext } from '../../contexts/rowIndexContext';

/**
* Create the state required to render DataGridRow.
*
* The returned state can be modified with hooks such as useDataGridRowStyles_unstable,
* before being passed to renderDataGridRow_unstable.
*
* @param props - props from this instance of DataGridRow
* @param ref - reference to root HTMLElement of DataGridRow
*/
export const useDataGridRow_unstable = (
props: DataGridRowProps,
ref: React.Ref<HTMLElement>
): DataGridRowState => {
const rowIndex = useTableRowIndexContext();
return useBaseState({ ...props, 'aria-rowindex': rowIndex }, ref);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react';

const bodyRefContext: React.Context<
React.MutableRefObject<HTMLElement | null>
> = React.createContext<React.MutableRefObject<HTMLElement | null>>({
current: null,
});

export const bodyRefContextDefaultValue: React.MutableRefObject<HTMLElement | null> =
{ current: null };

export const useBodyRefContext = () =>
React.useContext(bodyRefContext) ?? bodyRefContextDefaultValue;

export const BodyRefContextProvider = bodyRefContext.Provider;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react';

const headerRefContext: React.Context<
React.MutableRefObject<HTMLElement | null>
> = React.createContext<React.MutableRefObject<HTMLElement | null>>({
current: null,
});

export const headerRefContextDefaultValue: React.MutableRefObject<HTMLElement | null> =
{ current: null };

export const useHeaderRefContext = () =>
React.useContext(headerRefContext) ?? headerRefContextDefaultValue;

export const HeaderRefContextProvider = headerRefContext.Provider;
2 changes: 1 addition & 1 deletion packages/react-data-grid-react-window/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
export { DataGridBody } from './components/DataGridBody';
export { DataGrid } from './components/DataGrid';
export { DataGridRow } from './components/DataGridRow';
export { DataGridHeader } from './components/DataGridHeader';

export {
DataGridCell,
DataGridHeader,
DataGridHeaderCell,
DataGridSelectionCell,
} from '@fluentui/react-components';
Expand Down

0 comments on commit b3af165

Please sign in to comment.