) {
42 | const MemoizedComponent = (React.memo(Component) as any) as React.FC<
43 | InnerProps
44 | >;
45 |
46 | return function(props: P) {
47 | const contextValue = useContext(DataScrollerContext);
48 |
49 | const propsWithState = mapContextValueToProps
50 | ? mapContextValueToProps(props, contextValue.data)
51 | : (defaultMapContextDataValueToProps(
52 | props,
53 | contextValue.data,
54 | ) as InnerProps);
55 |
56 | return ;
57 | };
58 | };
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/DataScrollerContext/index.ts:
--------------------------------------------------------------------------------
1 | export { DataScrollerContextProvider, getRowData } from './DataScrollerContext';
2 |
--------------------------------------------------------------------------------
/src/components/Group/Group.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export type Props = {
4 | children?: React.ReactNode;
5 | groupData: any;
6 | headerRenderer?: React.ComponentType<{
7 | groupData: GroupData;
8 | width: number;
9 | }>;
10 | };
11 |
12 | // @ts-ignore
13 | function Group(props: Props) {
14 | return null;
15 | }
16 |
17 | // Used for identifying the component type when iterating through children
18 | Group.__Group__ = true;
19 |
20 | export default Group;
21 |
--------------------------------------------------------------------------------
/src/components/Group/index.ts:
--------------------------------------------------------------------------------
1 | export { default, Props } from './Group';
2 |
--------------------------------------------------------------------------------
/src/components/Headers/Headers.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ColumnProps } from '../../types';
3 |
4 | export type Props = {
5 | headerHeight: number;
6 | columns: ColumnProps[];
7 | };
8 |
9 | export default function({ headerHeight, columns }: Props) {
10 | return (
11 |
12 | {columns.map((column, index) => (
13 |
14 | {(column.headerRenderer && column.headerRenderer(column)) ||
}
15 |
16 | ))}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Headers/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Headers';
2 |
--------------------------------------------------------------------------------
/src/components/InfiniteLoader/InfiniteLoader.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import InfiniteLoader from './InfiniteLoader';
3 | import { render } from 'react-testing-library';
4 |
5 | describe('InfiniteLoader', () => {
6 | it('calls fetchMore when rowCount is larger than offset', () => {
7 | const props = {
8 | nodes: [],
9 | rowCount: 10,
10 | fetchMore: jest.fn(),
11 | children: ({ onTableRowsRendered }: any) => {
12 | onTableRowsRendered({ startIndex: 0, stopIndex: 20 });
13 |
14 | return ;
15 | },
16 | };
17 | render();
18 | expect(props.fetchMore).toBeCalled();
19 | });
20 |
21 | it('does not call fetchMore when startIndex is out of range of the rowCount', () => {
22 | const props = {
23 | nodes: [],
24 | rowCount: 3,
25 | fetchMore: jest.fn(),
26 | children: ({ onTableRowsRendered }: any) => {
27 | onTableRowsRendered({ startIndex: 3, stopIndex: 20 });
28 |
29 | return ;
30 | },
31 | };
32 | render();
33 | expect(props.fetchMore).not.toBeCalled();
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/components/InfiniteLoader/InfiniteLoader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export type OnTableRowsRenderedArg = {
4 | startIndex: number;
5 | stopIndex: number;
6 | };
7 |
8 | export type Props = {
9 | nodes: Node[];
10 | rowCount: number;
11 | fetchMore: ({ offset }: { offset: number }) => void;
12 | children: (arg: {
13 | onTableRowsRendered: (arg: OnTableRowsRenderedArg) => void;
14 | }) => React.ReactElement;
15 | onTableRowsRendered: (arg: OnTableRowsRenderedArg) => void;
16 | isNodeNotAvailable: (node: Node) => boolean;
17 | };
18 |
19 | function defaultIsNodeNotAvailable(product: Node) {
20 | return !Boolean(product);
21 | }
22 |
23 | function InfiniteLoader(props: Props) {
24 | const onTableRowsRendered = ({
25 | startIndex,
26 | stopIndex,
27 | }: {
28 | startIndex: number;
29 | stopIndex: number;
30 | }) => {
31 | props.onTableRowsRendered({ startIndex, stopIndex });
32 |
33 | let shouldFetch = false;
34 | let offset = startIndex;
35 |
36 | for (let i = startIndex; i < stopIndex && i < props.rowCount - 1; i += 1) {
37 | if (props.isNodeNotAvailable(props.nodes[i])) {
38 | shouldFetch = true;
39 | offset = i;
40 | break;
41 | }
42 | }
43 |
44 | if (shouldFetch) {
45 | props.fetchMore({ offset });
46 | }
47 | };
48 |
49 | return props.children({
50 | onTableRowsRendered,
51 | });
52 | }
53 |
54 | const noop = () => {};
55 |
56 | InfiniteLoader.defaultProps = {
57 | isNodeNotAvailable: defaultIsNodeNotAvailable,
58 | onTableRowsRendered: noop,
59 | fetchMore: noop,
60 | };
61 |
62 | export default InfiniteLoader;
63 |
--------------------------------------------------------------------------------
/src/components/InfiniteLoader/index.ts:
--------------------------------------------------------------------------------
1 | export { default, Props } from './InfiniteLoader';
2 |
--------------------------------------------------------------------------------
/src/components/Row/Row.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { RowProps } from '../../types';
3 |
4 | const RowRenderer = (props: RowProps) => {
5 | return <>{props.children}>;
6 | };
7 |
8 | export default RowRenderer;
9 |
--------------------------------------------------------------------------------
/src/components/Row/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Row';
2 |
--------------------------------------------------------------------------------
/src/components/RowChildren/RowChildren.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { RowChildrenProps } from '../../types';
3 |
4 | const RowChildren = (props: RowChildrenProps) => {
5 | return (
6 | <>
7 | {props.columns.map((column, columnIndex) => {
8 | const CellRenderer = column.cellRenderer;
9 | const adjustedColumnIndex = props.columnIndexOffset
10 | ? props.columnIndexOffset + columnIndex
11 | : columnIndex;
12 |
13 | return (
14 |
15 | {CellRenderer ? (
16 |
24 | ) : (
25 |
{props.rowData[column.dataKey]}
26 | )}
27 |
28 | );
29 | })}
30 | >
31 | );
32 | };
33 |
34 | export default React.memo(RowChildren);
35 |
--------------------------------------------------------------------------------
/src/components/RowChildren/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './RowChildren';
2 |
--------------------------------------------------------------------------------
/src/components/Rows/Rows.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import RowChildren from '../RowChildren';
3 |
4 | import { RowGetter, RowProps, ColumnProps, GetRowKey } from '../../types';
5 |
6 | export type Props = {
7 | // used to offset columnIndex when there are frozen colums
8 | columnIndexOffset?: number;
9 | columns: ColumnProps[];
10 | getRowKey: GetRowKey;
11 | rowCount: number;
12 | rowGetter: RowGetter;
13 | rowHeight: number;
14 | rowRenderer: React.FC;
15 | topRowIndex: number;
16 | totalVisibleRows: number;
17 | };
18 |
19 | function Rows({
20 | columnIndexOffset,
21 | columns,
22 | getRowKey,
23 | rowCount,
24 | rowGetter,
25 | rowHeight,
26 | rowRenderer,
27 | topRowIndex,
28 | totalVisibleRows,
29 | }: Props) {
30 | const RowRenderer = rowRenderer;
31 |
32 | return (
33 |
34 | {Array.apply(null, new Array(totalVisibleRows)).map((_, renderIndex) => {
35 | const rowIndex = topRowIndex + renderIndex;
36 | const row = rowGetter({ index: rowIndex });
37 | if (rowIndex > rowCount - 1) {
38 | return null;
39 | }
40 |
41 | return (
42 |
46 |
47 |
53 |
54 |
55 | );
56 | })}
57 |
58 | );
59 | }
60 |
61 | export default React.memo(Rows);
62 |
--------------------------------------------------------------------------------
/src/components/Rows/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Rows';
2 |
--------------------------------------------------------------------------------
/src/hooks/useTableScrollDimensions.test.ts:
--------------------------------------------------------------------------------
1 | import { renderHook } from 'react-hooks-testing-library';
2 | import useTableScrollDimensions from './useTableScrollDimensions';
3 |
4 | describe('useTableScrollDimensions()', () => {
5 | it('is a function', () => {
6 | expect(typeof useTableScrollDimensions).toBe('function');
7 | });
8 |
9 | describe('returning the total scroll width of frozen columns', () => {
10 | const props = {
11 | columns: [],
12 | frozenColumns: [{ width: 10 }, { width: 20 }],
13 | groupHeaderHeight: 0,
14 | headerHeight: 0,
15 | rowCount: 0,
16 | rowHeight: 0,
17 | };
18 |
19 | it('returns the sum of all individual frozen column widths', () => {
20 | const { result } = renderHook(() => useTableScrollDimensions(props));
21 | expect(result.current.frozenColumnsScrollWidth).toBe(30);
22 | });
23 | });
24 |
25 | describe('returning the total scroll width of table columns', () => {
26 | const props = {
27 | columns: [{ width: 10 }, { width: 20 }],
28 | frozenColumns: [],
29 | groupHeaderHeight: 0,
30 | headerHeight: 0,
31 | rowCount: 0,
32 | rowHeight: 0,
33 | };
34 |
35 | it('returns the sum of all individual table column widths', () => {
36 | const { result } = renderHook(() => useTableScrollDimensions(props));
37 | expect(result.current.tableScrollWidth).toBe(30);
38 | });
39 | });
40 |
41 | describe('returning the total scroll height of the table', () => {
42 | const props = {
43 | columns: [],
44 | frozenColumns: [],
45 | groupHeaderHeight: 3,
46 | headerHeight: 4,
47 | rowCount: 1,
48 | rowHeight: 10,
49 | };
50 |
51 | it('returns the sum height of (rowCount + 1) rows and all headers', () => {
52 | const { result } = renderHook(() => useTableScrollDimensions(props));
53 | expect(result.current.tableScrollHeight).toBe(27);
54 | });
55 | });
56 |
57 | describe('updating as props change', () => {
58 | const initialProps = {
59 | columns: [{ width: 10 }, { width: 20 }],
60 | frozenColumns: [{ width: 10 }, { width: 20 }],
61 | groupHeaderHeight: 3,
62 | headerHeight: 4,
63 | rowCount: 1,
64 | rowHeight: 10,
65 | };
66 |
67 | describe('when props.rowHeight changes', () => {
68 | const nextProps = {
69 | ...initialProps,
70 | rowHeight: 20,
71 | };
72 |
73 | it('updates the tableScrollHeight', () => {
74 | const { result, rerender } = renderHook(
75 | (props: any) => useTableScrollDimensions(props),
76 | { initialProps },
77 | );
78 | expect(result.current.tableScrollHeight).toBe(27);
79 |
80 | rerender(nextProps);
81 | expect(result.current.tableScrollHeight).toBe(47);
82 | });
83 | });
84 |
85 | describe('when props.rowCount changes', () => {
86 | const nextProps = {
87 | ...initialProps,
88 | rowCount: 2,
89 | };
90 |
91 | it('updates the tableScrollHeight', () => {
92 | const { result, rerender } = renderHook(
93 | (props: any) => useTableScrollDimensions(props),
94 | { initialProps },
95 | );
96 | expect(result.current.tableScrollHeight).toBe(27);
97 |
98 | rerender(nextProps);
99 | expect(result.current.tableScrollHeight).toBe(37);
100 | });
101 | });
102 |
103 | describe('when props.columns changes', () => {
104 | const nextProps = {
105 | ...initialProps,
106 | columns: [{ width: 10 }],
107 | };
108 |
109 | it('updates the tableScrollWidth', () => {
110 | const { result, rerender } = renderHook(
111 | (props: any) => useTableScrollDimensions(props),
112 | { initialProps },
113 | );
114 | expect(result.current.tableScrollWidth).toBe(30);
115 |
116 | rerender(nextProps);
117 | expect(result.current.tableScrollWidth).toBe(10);
118 | });
119 | });
120 |
121 | describe('when props.frozenColumns changes', () => {
122 | const nextProps = {
123 | ...initialProps,
124 | frozenColumns: [{ width: 10 }],
125 | };
126 |
127 | it('updates the tableScrollWidth', () => {
128 | const { result, rerender } = renderHook(
129 | (props: any) => useTableScrollDimensions(props),
130 | { initialProps },
131 | );
132 | expect(result.current.frozenColumnsScrollWidth).toBe(30);
133 |
134 | rerender(nextProps);
135 | expect(result.current.frozenColumnsScrollWidth).toBe(10);
136 | });
137 | });
138 | });
139 | });
140 |
--------------------------------------------------------------------------------
/src/hooks/useTableScrollDimensions.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { ColumnProps, DataTableProps } from '../types';
3 |
4 | type Column = Pick;
5 |
6 | type Props = Pick<
7 | DataTableProps,
8 | 'rowCount' | 'headerHeight' | 'rowHeight' | 'groupHeaderHeight'
9 | > & {
10 | columns: Column[];
11 | frozenColumns: Column[];
12 | };
13 |
14 | function getTableScrollHeight(props: Props) {
15 | const newTableScrollHeight =
16 | (props.rowCount + 1) * props.rowHeight +
17 | props.headerHeight +
18 | props.groupHeaderHeight;
19 |
20 | return newTableScrollHeight;
21 | }
22 |
23 | function sumColumnWidths(width: number, column: Column) {
24 | return width + column.width;
25 | }
26 |
27 | function getTotalColumnsWidth(columns: Column[]) {
28 | const totalWidth = columns.reduce(sumColumnWidths, 0);
29 | return totalWidth;
30 | }
31 |
32 | export default function useTableScrollDimensions(props: Props) {
33 | const frozenColumnsScrollWidth = useMemo(
34 | () => getTotalColumnsWidth(props.frozenColumns),
35 | [props.frozenColumns],
36 | );
37 |
38 | const tableScrollHeight = useMemo(() => getTableScrollHeight(props), [
39 | props.rowCount,
40 | props.rowHeight,
41 | ]);
42 |
43 | const tableScrollWidth = useMemo(() => getTotalColumnsWidth(props.columns), [
44 | props.columns,
45 | ]);
46 |
47 | return {
48 | frozenColumnsScrollWidth,
49 | tableScrollHeight,
50 | tableScrollWidth,
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/src/hooks/useTotalVisibleRows.test.ts:
--------------------------------------------------------------------------------
1 | import { renderHook } from 'react-hooks-testing-library';
2 | import useTotalVisibleRows from './useTotalVisibleRows';
3 |
4 | describe('useTotalVisibleRows()', () => {
5 | it('is a function', () => {
6 | expect(typeof useTotalVisibleRows).toBe('function');
7 | });
8 |
9 | describe('returning the number of rows that can fit', () => {
10 | describe('when all rows would be completely visible', () => {
11 | it('returns only the number of rows that are visible', () => {
12 | const props = {
13 | groupHeaderHeight: 30,
14 | height: 100,
15 | headerHeight: 20,
16 | rowHeight: 10,
17 | };
18 |
19 | const { result } = renderHook(() => useTotalVisibleRows(props));
20 | expect(result.current).toBe(5);
21 | });
22 | });
23 |
24 | describe('when a row would only be partially visible', () => {
25 | it('rounds up to include that row', () => {
26 | const props = {
27 | groupHeaderHeight: 35,
28 | height: 100,
29 | headerHeight: 20,
30 | rowHeight: 10,
31 | };
32 |
33 | const { result } = renderHook(() => useTotalVisibleRows(props));
34 | expect(result.current).toBe(4);
35 | });
36 | });
37 |
38 | describe('when no rows would be visible', () => {
39 | it('defaults to 0 instead of negative number', () => {
40 | const props = {
41 | groupHeaderHeight: 35,
42 | height: 10,
43 | headerHeight: 20,
44 | rowHeight: 10,
45 | };
46 |
47 | const { result } = renderHook(() => useTotalVisibleRows(props));
48 | expect(result.current).toBe(0);
49 | });
50 | });
51 | });
52 |
53 | describe('updating as props change', () => {
54 | const initialProps = {
55 | groupHeaderHeight: 30,
56 | height: 100,
57 | headerHeight: 20,
58 | rowHeight: 10,
59 | };
60 |
61 | it('updates when props.groupHeaderHeight changes', async () => {
62 | const nextProps = {
63 | ...initialProps,
64 | groupHeaderHeight: 40,
65 | };
66 |
67 | const { result, rerender } = renderHook(
68 | (props: any) => useTotalVisibleRows(props),
69 | { initialProps },
70 | );
71 | expect(result.current).toBe(5);
72 |
73 | rerender(nextProps);
74 | expect(result.current).toBe(4);
75 | });
76 |
77 | it('updates when props.headerHeight changes', async () => {
78 | const nextProps = {
79 | ...initialProps,
80 | headerHeight: 30,
81 | };
82 |
83 | const { result, rerender } = renderHook(
84 | (props: any) => useTotalVisibleRows(props),
85 | { initialProps },
86 | );
87 | expect(result.current).toBe(5);
88 |
89 | rerender(nextProps);
90 | expect(result.current).toBe(4);
91 | });
92 |
93 | it('updates when props.height changes', async () => {
94 | const nextProps = {
95 | ...initialProps,
96 | height: 90,
97 | };
98 |
99 | const { result, rerender } = renderHook(
100 | (props: any) => useTotalVisibleRows(props),
101 | { initialProps },
102 | );
103 | expect(result.current).toBe(5);
104 |
105 | rerender(nextProps);
106 | expect(result.current).toBe(4);
107 | });
108 |
109 | it('updates when props.rowHeight changes', async () => {
110 | const nextProps = {
111 | ...initialProps,
112 | rowHeight: 20,
113 | };
114 |
115 | const { result, rerender } = renderHook(
116 | (props: any) => useTotalVisibleRows(props),
117 | { initialProps },
118 | );
119 | expect(result.current).toBe(5);
120 |
121 | rerender(nextProps);
122 | expect(result.current).toBe(2);
123 | });
124 | });
125 | });
126 |
--------------------------------------------------------------------------------
/src/hooks/useTotalVisibleRows.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { DataTableProps } from '../types';
3 |
4 | type Props = Pick<
5 | DataTableProps,
6 | 'groupHeaderHeight' | 'headerHeight' | 'height' | 'rowHeight'
7 | >;
8 |
9 | function getTotalVisibleRows(props: Props) {
10 | const totalTableHeight =
11 | props.height - props.headerHeight - props.groupHeaderHeight;
12 | const totalRowsThatFit = totalTableHeight / props.rowHeight;
13 |
14 | return Math.max(0, Math.floor(totalRowsThatFit));
15 | }
16 |
17 | export default function useTotalVisibleRows(props: Props) {
18 | const totalVisibleRows = useMemo(() => getTotalVisibleRows(props), [
19 | props.groupHeaderHeight,
20 | props.headerHeight,
21 | props.height,
22 | props.rowHeight,
23 | ]);
24 |
25 | return totalVisibleRows;
26 | }
27 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /* DataScroller */
2 | export { default } from './DataScroller';
3 | export {
4 | default as InfiniteLoader,
5 | Props as InfiniteLoaderProps,
6 | } from './components/InfiniteLoader';
7 | export { default as Group, Props as GroupProps } from './components/Group';
8 | export { default as Column } from './components/Column';
9 | export { default as Row } from './components/Row';
10 | export {
11 | DataScrollerContextProvider,
12 | getRowData,
13 | } from './components/DataScrollerContext';
14 |
15 | /* Types */
16 | export * from './types';
17 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | .sticky {
2 | position: -webkit-sticky;
3 | position: sticky;
4 | }
5 |
6 | .scroll {
7 | overflow-y: scroll;
8 | -webkit-overflow-scrolling: touch;
9 | }
10 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export type CellRendererArgs = {
4 | cellData: any;
5 | columnData: ColumnData;
6 | columnIndex: number;
7 | dataKey: string;
8 | // @deprecated: Prefer getRowData with DataScrollerContext
9 | rowData?: any;
10 | rowIndex: number;
11 | };
12 | export type HeaderRendererArgs = {
13 | columnData?: any;
14 | dataKey: string;
15 | label: any;
16 | };
17 |
18 | export type OnRowsRenderedArgs = {
19 | startIndex: number;
20 | overscanStartIndex: number;
21 | overscanStopIndex: number;
22 | stopIndex: number;
23 | };
24 |
25 | export type RowGetterArgs = {
26 | index: number;
27 | };
28 | export type RowGetter = (arg: RowGetterArgs) => any;
29 |
30 | export type ColumnAndGroup = React.ReactFragment | React.ReactNode;
31 |
32 | export type DataTableProps = {
33 | columns: React.ReactNode;
34 | frozenColumns: React.ReactNode;
35 | groupHeaderHeight: number;
36 | headerHeight: number;
37 | height: number;
38 | initialTopRowIndex: number;
39 | onRowsRendered: (arg: OnRowsRenderedArgs) => void;
40 | rowCount: number;
41 | rowGetter: RowGetter;
42 | rowHeight: number;
43 | rowRenderer: React.FC;
44 | scrollToIndex: number | null;
45 | width: number;
46 | getRowKey: GetRowKey;
47 | };
48 |
49 | export type GetRowKey = (args: {
50 | renderIndex: number;
51 | topRowIndex: number;
52 | }) => number;
53 |
54 | export type DataTableState = {
55 | tableScrollHeight: number;
56 | tableScrollWidth: number;
57 | topRowIndex: number;
58 | leftColumnIndex: number;
59 | totalVisibleRows: number;
60 | frozenColumnsWidth: number;
61 | };
62 |
63 | export type RowProps = {
64 | rowIndex: number;
65 | children: React.ReactNode;
66 | };
67 |
68 | export type RowChildrenProps = {
69 | rowIndex: number;
70 | rowData: any;
71 | columns: ColumnProps[];
72 | columnIndexOffset?: number;
73 | };
74 |
75 | export type CellRenderer =
76 | | React.FC>
77 | | undefined;
78 |
79 | export type ColumnProps = {
80 | cellRenderer?: CellRenderer;
81 | headerRenderer?: (arg: HeaderRendererArgs) => React.ReactNode;
82 | width: number;
83 | columnData?: any;
84 | dataKey: string;
85 | label: any;
86 | };
87 |
--------------------------------------------------------------------------------
/stories/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as faker from 'faker';
2 | import React, { useState, useEffect } from 'react';
3 | import { Column } from '../src/';
4 | import Group from '../src/components/Group';
5 | import Row from '../src/components/Row';
6 | import { RowProps, ColumnProps, GetRowKey } from '../src/types';
7 | import {
8 | getRowData,
9 | DataScrollerContextProvider,
10 | } from '../src/components/DataScrollerContext/DataScrollerContext';
11 |
12 | import { storiesOf } from '@storybook/react';
13 | import DataScroller, {
14 | CellRendererArgs,
15 | HeaderRendererArgs,
16 | RowGetterArgs,
17 | } from '../src';
18 |
19 | const IndexCell = ({ rowData }: CellRendererArgs) => {
20 | return (
21 |
26 | {rowData.index}
27 |
28 | );
29 | };
30 |
31 | const CustomInput = function(props: { value: string }) {
32 | const [inputValue, setInputValue] = useState(props.value);
33 | const handleOnChange = (e: React.ChangeEvent) => {
34 | setInputValue(e.target.value);
35 | };
36 |
37 | return ;
38 | };
39 |
40 | const FirstNameCell = ({ rowData }: CellRendererArgs) => {
41 | return ;
42 | };
43 |
44 | const LastNameCell = ({ rowData }: CellRendererArgs) => {
45 | return {rowData.lastName}
;
46 | };
47 |
48 | const initialColumns = [
49 | {
50 | cellRenderer: IndexCell,
51 | columnData: {},
52 | dataKey: 'lastName',
53 | headerRenderer: ({ columnData }: HeaderRendererArgs) => (
54 | Header {columnData.columnIndex}
55 | ),
56 | label: 'index',
57 | width: 200,
58 | },
59 | {
60 | cellRenderer: LastNameCell,
61 | columnData: {},
62 | dataKey: 'lastName',
63 | headerRenderer: ({ columnData }: HeaderRendererArgs) => (
64 | Header {columnData.columnIndex}
65 | ),
66 | label: 'last name',
67 | width: 200,
68 | },
69 | {
70 | cellRenderer: FirstNameCell,
71 | columnData: {},
72 | dataKey: 'firstName',
73 | headerRenderer: ({ columnData }: HeaderRendererArgs) => (
74 | Header{columnData.columnIndex}
75 | ),
76 | label: 'first name',
77 | width: 200,
78 | },
79 | ];
80 |
81 | const generateRows = (n: number) => {
82 | const arr = Array.apply(null, Array(n));
83 | return arr.map((_, index) => {
84 | return {
85 | index,
86 | avatar: faker.image.imageUrl(100, 100, 'people'),
87 | firstName: faker.name.firstName(),
88 | lastName: faker.name.lastName(),
89 | };
90 | });
91 | };
92 | const rowCount = 5000;
93 | const rows = generateRows(rowCount);
94 |
95 | const rowGetter = ({ index }: RowGetterArgs) => rows[index];
96 |
97 | let columns: ColumnProps[] = [];
98 | for (let counter = 0; counter < 10; counter += 1) {
99 | columns = [...initialColumns, ...(columns || [])];
100 | }
101 |
102 | columns = columns.map((column, index) => ({
103 | ...column,
104 | columnData: { ...(column.columnData || {}), columnIndex: index },
105 | }));
106 |
107 | let frozenColumns: ColumnProps[] = [];
108 | for (let counter = 0; counter < 2; counter += 1) {
109 | frozenColumns = [...initialColumns, ...(frozenColumns || [])];
110 | }
111 |
112 | frozenColumns = frozenColumns.map((column, index) => ({
113 | ...column,
114 | columnData: { ...(column.columnData || {}), columnIndex: index },
115 | }));
116 |
117 | const GroupHeaderA = (props: { width: number }) => {
118 | return (
119 |
120 | First Group
121 |
122 | );
123 | };
124 |
125 | const GroupHeaderB = (props: { width: number }) => {
126 | return (
127 |
128 | Second Group
129 |
130 | );
131 | };
132 |
133 | storiesOf('react-data-scroller', module).add('default', () => (
134 |
145 | {columns.map((column, index) => (
146 |
147 | ))}
148 | ,
149 |
150 | {columns.map((column, index) => (
151 |
152 | ))}
153 | ,
154 | ]}
155 | frozenColumns={frozenColumns.map((column, index) => (
156 |
157 | ))}
158 | />
159 | ));
160 |
161 | const customGetRowKey: GetRowKey = ({ renderIndex }) => renderIndex;
162 |
163 | storiesOf('react-data-scroller', module).add('custom row key', () => (
164 |
175 | {columns.map((column, index) => (
176 |
177 | ))}
178 | ,
179 |
180 | {columns.map((column, index) => (
181 |
182 | ))}
183 | ,
184 | ]}
185 | frozenColumns={frozenColumns.map((column, index) => (
186 |
187 | ))}
188 | getRowKey={customGetRowKey}
189 | />
190 | ));
191 |
192 | const ScrollToIndexDataScroller = (props: {
193 | children: (arg: {
194 | scrollToIndex: number;
195 | handleScrollToIndex: () => void;
196 | }) => JSX.Element;
197 | }) => {
198 | const [scrollToIndex, setScrollToIndex] = useState();
199 | useEffect(() => {
200 | setScrollToIndex(null);
201 | }, [scrollToIndex]);
202 |
203 | const handleScrollToIndex = () => {
204 | setScrollToIndex(20);
205 | };
206 |
207 | return props.children({ scrollToIndex, handleScrollToIndex });
208 | };
209 |
210 | storiesOf('react-data-scroller', module).add('scroll to index', () => (
211 |
212 | {({
213 | scrollToIndex,
214 | handleScrollToIndex,
215 | }: {
216 | scrollToIndex: number;
217 | handleScrollToIndex: () => void;
218 | }) => (
219 |
220 |
221 |
233 | {columns.map((column, index) => (
234 |
235 | ))}
236 | ,
237 |
238 | {columns.map((column, index) => (
239 |
240 | ))}
241 | ,
242 | ]}
243 | frozenColumns={frozenColumns.map((column, index) => (
244 |
245 | ))}
246 | />
247 |
248 | )}
249 |
250 | ));
251 |
252 | storiesOf('react-data-scroller', module).add('custom rowRenderer', () => {
253 | const rowRenderer = ({ rowIndex, children }: RowProps) => {
254 | // Render AMOUNT_OF_PADDING_ROWS empty rows
255 | if (rowIndex === 0) {
256 | return My Custom Row!
;
257 | }
258 |
259 | return {children}
;
260 | };
261 |
262 | return (
263 |
275 | {columns.map((column, index) => (
276 |
277 | ))}
278 | ,
279 |
280 | {columns.map((column, index) => (
281 |
282 | ))}
283 | ,
284 | ]}
285 | frozenColumns={frozenColumns.map((column, index) => (
286 |
287 | ))}
288 | />
289 | );
290 | });
291 |
292 | storiesOf('react-data-scroller', module).add('DataScrollerContext', () => {
293 | const DetachedIndexCell = ({ index }: { index: number }) => {
294 | return (
295 |
300 | {index}
301 |
302 | );
303 | };
304 |
305 | const DetachedFirstNameCell = ({ firstName }: { firstName: string }) => {
306 | return ;
307 | };
308 |
309 | const DetachedLastNameCell = ({ lastName }: { lastName: string }) => {
310 | return {lastName}
;
311 | };
312 |
313 | const InjectedIndexCell = getRowData((props: any, data: any) => ({
314 | ...props,
315 | index: data[props.rowIndex].index,
316 | }))(DetachedIndexCell);
317 |
318 | const InjectedFirstNameCell = getRowData((props: any, data: any) => ({
319 | ...props,
320 | firstName: data[props.rowIndex].firstName,
321 | }))(DetachedFirstNameCell);
322 |
323 | const InjectedLastNameCell = getRowData((props: any, data: any) => ({
324 | ...props,
325 | lastName: data[props.rowIndex].lastName,
326 | }))(DetachedLastNameCell);
327 |
328 | const initialContextColumns = [
329 | {
330 | cellRenderer: InjectedIndexCell,
331 | columnData: {},
332 | dataKey: 'lastName',
333 | headerRenderer: ({ columnData }: HeaderRendererArgs) => (
334 |
335 | Header {columnData.columnIndex}
336 |
337 | ),
338 | label: 'index',
339 | width: 200,
340 | },
341 | {
342 | cellRenderer: InjectedLastNameCell,
343 | columnData: {},
344 | dataKey: 'lastName',
345 | headerRenderer: ({ columnData }: HeaderRendererArgs) => (
346 |
347 | Header {columnData.columnIndex}
348 |
349 | ),
350 | label: 'last name',
351 | width: 200,
352 | },
353 | {
354 | cellRenderer: InjectedFirstNameCell,
355 | columnData: {},
356 | dataKey: 'firstName',
357 | headerRenderer: ({ columnData }: HeaderRendererArgs) => (
358 | Header{columnData.columnIndex}
359 | ),
360 | label: 'first name',
361 | width: 200,
362 | },
363 | ];
364 |
365 | const rowGetter = () => {};
366 |
367 | let contextColumns: any[] = [];
368 | for (let counter = 0; counter < 10; counter += 1) {
369 | contextColumns = [...initialContextColumns, ...(contextColumns || [])];
370 | }
371 |
372 | contextColumns = contextColumns.map((column, index) => ({
373 | ...column,
374 | columnData: { ...(column.columnData || {}), columnIndex: index },
375 | }));
376 |
377 | let frozenContextColumns: any[] = [];
378 | for (let counter = 0; counter < 2; counter += 1) {
379 | frozenContextColumns = [
380 | ...initialContextColumns,
381 | ...(frozenContextColumns || []),
382 | ];
383 | }
384 |
385 | frozenContextColumns = frozenContextColumns.map((column, index) => ({
386 | ...column,
387 | columnData: { ...(column.columnData || {}), columnIndex: index },
388 | }));
389 |
390 | return (
391 |
392 |
403 | {contextColumns.map((column, index) => (
404 |
405 | ))}
406 | ,
407 |
408 | {contextColumns.map((column, index) => (
409 |
410 | ))}
411 | ,
412 | ]}
413 | frozenColumns={frozenContextColumns.map((column, index) => (
414 |
415 | ))}
416 | />
417 |
418 | );
419 | });
420 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src", "types"],
3 | "compilerOptions": {
4 | "alwaysStrict": true,
5 | "baseUrl": "./",
6 | "declaration": true,
7 | "declaration": true,
8 | "declarationDir": "./dist/types",
9 | "esModuleInterop": true,
10 | "importHelpers": true,
11 | "jsx": "react",
12 | "lib": ["dom", "esnext"],
13 | "module": "esnext",
14 | "moduleResolution": "node",
15 | "noFallthroughCasesInSwitch": true,
16 | "noImplicitAny": true,
17 | "noImplicitReturns": true,
18 | "noImplicitThis": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "paths": {
22 | "*": ["src/*", "node_modules/*"]
23 | },
24 | "rootDirs": ["src", "stories"],
25 | "sourceMap": true,
26 | "strict": true,
27 | "strictFunctionTypes": true,
28 | "strictNullChecks": true,
29 | "strictPropertyInitialization": true,
30 | "target": "es5"
31 | },
32 | "awesomeTypescriptLoaderOptions": {
33 | "typeRoots": ["types"]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint-config-airbnb",
5 | "tslint-plugin-prettier",
6 | "tslint-config-prettier",
7 | "tslint-react"
8 | ],
9 | "jsRules": {},
10 | "rules": {
11 | "align": false,
12 | "array-type": [true, "array"],
13 | "function-name": false,
14 | "import-name": false,
15 | "interface-over-type-literal": false,
16 | "jsx-no-multiline-js": false,
17 | "jsx-wrap-multiline": false,
18 | "max-line-length": ["error", { "ignoreStrings": true }],
19 | "prefer-array-literal": false,
20 | "prettier": [
21 | true,
22 | {
23 | "singleQuote": true,
24 | "trailingComma": "all"
25 | }
26 | ],
27 | "ter-arrow-parens": false,
28 | "variable-name": [false, "check-format", "allow-pascal-case"],
29 | "one-variable-per-declaration":false
30 | },
31 | "rulesDirectory": []
32 | }
33 |
--------------------------------------------------------------------------------