@indico-data/design-system 2.47.3 → 2.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/index.d.ts +1 -0
- package/lib/components/pagination/Pagination.d.ts +2 -0
- package/lib/components/pagination/Pagination.stories.d.ts +6 -0
- package/lib/components/pagination/__tests__/Pagination.test.d.ts +1 -0
- package/lib/components/pagination/index.d.ts +1 -0
- package/lib/components/pagination/types.d.ts +6 -0
- package/lib/components/table/Table.stories.d.ts +1 -0
- package/lib/components/table/__tests__/Table.test.d.ts +1 -0
- package/lib/components/table/components/HorizontalStickyHeader.d.ts +10 -0
- package/lib/components/table/components/TablePagination.d.ts +9 -0
- package/lib/components/table/components/__tests__/HorizontalStickyHeader.test.d.ts +1 -0
- package/lib/components/table/components/__tests__/TablePagination.test.d.ts +1 -0
- package/lib/components/table/components/helpers.d.ts +6 -0
- package/lib/components/table/hooks/usePinnedColumnsManager.d.ts +8 -0
- package/lib/components/table/sampleData.d.ts +6 -0
- package/lib/components/table/types.d.ts +16 -5
- package/lib/components/table/utils/processColumns.d.ts +2 -0
- package/lib/index.css +78 -17
- package/lib/index.d.ts +16 -5
- package/lib/index.esm.css +78 -17
- package/lib/index.esm.js +305 -14
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +304 -13
- package/lib/index.js.map +1 -1
- package/lib/utils/getPreviousHeadersWidth.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/index.ts +1 -0
- package/src/components/pagination/Pagination.mdx +31 -0
- package/src/components/pagination/Pagination.stories.tsx +80 -0
- package/src/components/pagination/Pagination.tsx +117 -0
- package/src/components/pagination/__tests__/Pagination.test.tsx +91 -0
- package/src/components/pagination/index.ts +1 -0
- package/src/components/pagination/styles/Pagination.scss +22 -0
- package/src/components/pagination/types.ts +6 -0
- package/src/components/table/Table.mdx +136 -0
- package/src/components/table/Table.stories.tsx +91 -30
- package/src/components/table/Table.tsx +25 -2
- package/src/components/table/__tests__/Table.test.tsx +10 -0
- package/src/components/table/components/HorizontalStickyHeader.tsx +57 -0
- package/src/components/table/components/TablePagination.tsx +44 -0
- package/src/components/table/components/__tests__/HorizontalStickyHeader.test.tsx +104 -0
- package/src/components/table/components/__tests__/TablePagination.test.tsx +17 -0
- package/src/components/table/components/helpers.ts +90 -0
- package/src/components/table/hooks/usePinnedColumnsManager.ts +146 -0
- package/src/components/table/sampleData.tsx +436 -0
- package/src/components/table/styles/Table.scss +72 -24
- package/src/components/table/styles/_variables.scss +3 -0
- package/src/components/table/types.ts +19 -7
- package/src/components/table/utils/processColumns.tsx +35 -0
- package/src/setup/setupIcons.ts +4 -0
- package/src/setup/setupTests.ts +8 -0
- package/src/styles/index.scss +1 -0
- package/src/utils/getPreviousHeadersWidth.ts +12 -0
- package/src/components/table/sampleData.ts +0 -171
|
@@ -3,9 +3,10 @@ import DataTable, {
|
|
|
3
3
|
Direction as RDTDirection,
|
|
4
4
|
Alignment as RDTAlignment,
|
|
5
5
|
} from 'react-data-table-component';
|
|
6
|
-
|
|
7
6
|
import { LoadingComponent } from './LoadingComponent';
|
|
8
7
|
import { TableProps } from './types';
|
|
8
|
+
import { TablePagination } from './components/TablePagination';
|
|
9
|
+
import { usePinnedColumnsManager } from './hooks/usePinnedColumnsManager';
|
|
9
10
|
|
|
10
11
|
export const Table = <T,>(props: TableProps<T>) => {
|
|
11
12
|
const {
|
|
@@ -19,9 +20,22 @@ export const Table = <T,>(props: TableProps<T>) => {
|
|
|
19
20
|
isFullHeight = false,
|
|
20
21
|
subHeaderAlign = 'left',
|
|
21
22
|
className,
|
|
23
|
+
paginationTotalRows,
|
|
24
|
+
totalEntriesText,
|
|
25
|
+
data,
|
|
26
|
+
columns: initialColumns,
|
|
27
|
+
canPinColumns = false,
|
|
28
|
+
onPinnedColumnsChange,
|
|
22
29
|
...rest
|
|
23
30
|
} = props;
|
|
24
31
|
|
|
32
|
+
// Turns on/off column pinning.
|
|
33
|
+
const { columnsWithPinning } = usePinnedColumnsManager(
|
|
34
|
+
initialColumns,
|
|
35
|
+
canPinColumns,
|
|
36
|
+
onPinnedColumnsChange,
|
|
37
|
+
);
|
|
38
|
+
|
|
25
39
|
const combinedClassName = classNames(className, {
|
|
26
40
|
'table--striped': striped,
|
|
27
41
|
'table-body': true,
|
|
@@ -32,8 +46,10 @@ export const Table = <T,>(props: TableProps<T>) => {
|
|
|
32
46
|
});
|
|
33
47
|
|
|
34
48
|
return (
|
|
35
|
-
<div className={tableWrapperClassName}>
|
|
49
|
+
<div className={tableWrapperClassName} data-testid="table">
|
|
36
50
|
<DataTable
|
|
51
|
+
data={data}
|
|
52
|
+
columns={columnsWithPinning}
|
|
37
53
|
responsive={responsive}
|
|
38
54
|
direction={direction as RDTDirection}
|
|
39
55
|
subHeaderAlign={subHeaderAlign as RDTAlignment}
|
|
@@ -44,6 +60,13 @@ export const Table = <T,>(props: TableProps<T>) => {
|
|
|
44
60
|
noDataComponent={noDataComponent}
|
|
45
61
|
progressPending={isLoading}
|
|
46
62
|
progressComponent={<LoadingComponent />}
|
|
63
|
+
pagination
|
|
64
|
+
paginationComponent={(props) => (
|
|
65
|
+
<TablePagination {...props} totalEntriesText={totalEntriesText} />
|
|
66
|
+
)}
|
|
67
|
+
paginationTotalRows={paginationTotalRows}
|
|
68
|
+
highlightOnHover
|
|
69
|
+
pointerOnHover
|
|
47
70
|
{...rest}
|
|
48
71
|
/>
|
|
49
72
|
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { Table } from '../Table';
|
|
3
|
+
import { sampleData, columns } from '../sampleData';
|
|
4
|
+
|
|
5
|
+
describe('Table', () => {
|
|
6
|
+
it('renders the total entries text', () => {
|
|
7
|
+
render(<Table columns={columns} data={sampleData} totalEntriesText="100 entries" />);
|
|
8
|
+
expect(screen.getByTestId('table-pagination-total-entries')).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { Button } from '../../button/Button';
|
|
3
|
+
import {
|
|
4
|
+
getPreviousHeadersWidth,
|
|
5
|
+
applyStickyStylesToTableHeader,
|
|
6
|
+
clearStickyStyles,
|
|
7
|
+
} from './helpers';
|
|
8
|
+
interface HorizontalStickyHeaderProps {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
position: number;
|
|
11
|
+
onPinColumn?: (columnId: string) => void;
|
|
12
|
+
isPinned?: boolean;
|
|
13
|
+
forceUpdate?: number;
|
|
14
|
+
pinnedColumnIds: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const HorizontalStickyHeader = ({
|
|
18
|
+
children,
|
|
19
|
+
position,
|
|
20
|
+
onPinColumn,
|
|
21
|
+
isPinned = false,
|
|
22
|
+
pinnedColumnIds,
|
|
23
|
+
}: HorizontalStickyHeaderProps) => {
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const calculateWidth = async () => {
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
27
|
+
const header = document.querySelector(`[data-column-id="sticky-column-${position}"]`);
|
|
28
|
+
if (header) {
|
|
29
|
+
if (isPinned) {
|
|
30
|
+
const width = getPreviousHeadersWidth(position, pinnedColumnIds);
|
|
31
|
+
await applyStickyStylesToTableHeader(position, width);
|
|
32
|
+
} else {
|
|
33
|
+
clearStickyStyles(header as HTMLElement);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
calculateWidth();
|
|
39
|
+
}, [position, isPinned, pinnedColumnIds]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="table__header-cell" data-testid={`sticky-column-${position}`}>
|
|
43
|
+
<Button
|
|
44
|
+
data-testid={`sticky-header-pin-button-${position}`}
|
|
45
|
+
variant="link"
|
|
46
|
+
size="sm"
|
|
47
|
+
iconLeft="pin"
|
|
48
|
+
onClick={onPinColumn}
|
|
49
|
+
ariaLabel={isPinned ? 'Unpin column' : 'Pin column'}
|
|
50
|
+
className={`table__column--${isPinned ? 'is-pinned' : 'is-not-pinned'} table__column__pin-action`}
|
|
51
|
+
/>
|
|
52
|
+
<div className="table__header-content">{children}</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default HorizontalStickyHeader;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Pagination as PaginationComponent } from '../../pagination';
|
|
2
|
+
import { Row, Col } from '../../grid';
|
|
3
|
+
interface TablePaginationProps {
|
|
4
|
+
rowsPerPage: number;
|
|
5
|
+
rowCount: number;
|
|
6
|
+
onChangePage: (page: number, perPage: number) => void;
|
|
7
|
+
currentPage: number;
|
|
8
|
+
totalEntriesText?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const TablePagination = ({
|
|
12
|
+
rowsPerPage,
|
|
13
|
+
rowCount,
|
|
14
|
+
onChangePage,
|
|
15
|
+
currentPage,
|
|
16
|
+
totalEntriesText,
|
|
17
|
+
}: TablePaginationProps) => {
|
|
18
|
+
const totalPages = Math.ceil(rowCount / rowsPerPage);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="table__pagination">
|
|
22
|
+
<Row align="center" justify="between">
|
|
23
|
+
<Col xs="content">
|
|
24
|
+
{totalEntriesText && (
|
|
25
|
+
<span
|
|
26
|
+
data-testid="table-pagination-total-entries"
|
|
27
|
+
className="table__pagination-total-entries"
|
|
28
|
+
>
|
|
29
|
+
{totalEntriesText}
|
|
30
|
+
</span>
|
|
31
|
+
)}
|
|
32
|
+
</Col>
|
|
33
|
+
<Col xs="content">
|
|
34
|
+
<PaginationComponent
|
|
35
|
+
data-testid="table-pagination-component"
|
|
36
|
+
totalPages={totalPages}
|
|
37
|
+
currentPage={currentPage}
|
|
38
|
+
onChange={(page) => onChangePage(page, rowsPerPage)}
|
|
39
|
+
/>
|
|
40
|
+
</Col>
|
|
41
|
+
</Row>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import HorizontalStickyHeader from '../HorizontalStickyHeader';
|
|
3
|
+
import { sampleData } from '../../sampleData';
|
|
4
|
+
import { Table } from '../../Table';
|
|
5
|
+
import { columns } from '../../sampleData';
|
|
6
|
+
import userEvent from '@testing-library/user-event';
|
|
7
|
+
|
|
8
|
+
// Add ResizeObserver mock before tests
|
|
9
|
+
class ResizeObserverMock {
|
|
10
|
+
observe() {}
|
|
11
|
+
unobserve() {}
|
|
12
|
+
disconnect() {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Add the mock to the global object
|
|
16
|
+
global.ResizeObserver = ResizeObserverMock;
|
|
17
|
+
|
|
18
|
+
describe('HorizontalStickyHeader', () => {
|
|
19
|
+
// Store original implementation
|
|
20
|
+
const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
// Restore original implementation after each test
|
|
24
|
+
Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('correctly handles column pinning states based on column configuration', () => {
|
|
28
|
+
render(
|
|
29
|
+
<Table
|
|
30
|
+
responsive
|
|
31
|
+
columns={columns}
|
|
32
|
+
canPinColumns
|
|
33
|
+
data={sampleData}
|
|
34
|
+
totalEntriesText="100 entries"
|
|
35
|
+
/>,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
columns.forEach((column, index) => {
|
|
39
|
+
if (column.isPinned === undefined) {
|
|
40
|
+
// For columns where isPinned is not defined, the pin button should not exist
|
|
41
|
+
const pinButton = screen.queryByTestId(`sticky-header-pin-button-${index}`);
|
|
42
|
+
expect(pinButton).not.toBeInTheDocument();
|
|
43
|
+
} else {
|
|
44
|
+
// For columns where isPinned is defined, check for the correct class
|
|
45
|
+
const pinButton = screen.getByTestId(`sticky-header-pin-button-${index}`);
|
|
46
|
+
expect(pinButton).toHaveClass(
|
|
47
|
+
column.isPinned ? 'table__column--is-pinned' : 'table__column--is-not-pinned',
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('fires the onPinColumn callback when the pin button is clicked', () => {
|
|
54
|
+
const onPinColumn = jest.fn();
|
|
55
|
+
render(
|
|
56
|
+
<HorizontalStickyHeader position={0} pinnedColumnIds={[]} onPinColumn={onPinColumn}>
|
|
57
|
+
Test Content
|
|
58
|
+
</HorizontalStickyHeader>,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const pinButton = screen.getByTestId('sticky-header-pin-button-0');
|
|
62
|
+
fireEvent.click(pinButton);
|
|
63
|
+
expect(onPinColumn).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('correctly handles column pinning states based on column configuration', () => {
|
|
67
|
+
render(
|
|
68
|
+
<Table
|
|
69
|
+
responsive
|
|
70
|
+
columns={columns}
|
|
71
|
+
canPinColumns
|
|
72
|
+
data={sampleData}
|
|
73
|
+
totalEntriesText="100 entries"
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
columns.forEach((column, index) => {
|
|
78
|
+
if (column.isPinned === undefined) {
|
|
79
|
+
// For columns where isPinned is not defined, the pin button should not exist
|
|
80
|
+
const pinButton = screen.queryByTestId(`sticky-header-pin-button-${index}`);
|
|
81
|
+
expect(pinButton).not.toBeInTheDocument();
|
|
82
|
+
} else {
|
|
83
|
+
// For columns where isPinned is defined, check for the correct class
|
|
84
|
+
const pinButton = screen.getByTestId(`sticky-header-pin-button-${index}`);
|
|
85
|
+
expect(pinButton).toHaveClass(
|
|
86
|
+
column.isPinned ? 'table__column--is-pinned' : 'table__column--is-not-pinned',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('fires the onPinColumn callback when the pin button is clicked', () => {
|
|
93
|
+
const onPinColumn = jest.fn();
|
|
94
|
+
render(
|
|
95
|
+
<HorizontalStickyHeader position={0} pinnedColumnIds={[]} onPinColumn={onPinColumn}>
|
|
96
|
+
Test Content
|
|
97
|
+
</HorizontalStickyHeader>,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const pinButton = screen.getByTestId('sticky-header-pin-button-0');
|
|
101
|
+
fireEvent.click(pinButton);
|
|
102
|
+
expect(onPinColumn).toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { TablePagination } from '../TablePagination';
|
|
3
|
+
|
|
4
|
+
describe('TablePagination', () => {
|
|
5
|
+
it('renders total entries text', () => {
|
|
6
|
+
render(
|
|
7
|
+
<TablePagination
|
|
8
|
+
rowsPerPage={10}
|
|
9
|
+
rowCount={100}
|
|
10
|
+
onChangePage={() => {}}
|
|
11
|
+
currentPage={1}
|
|
12
|
+
totalEntriesText="100 entries"
|
|
13
|
+
/>,
|
|
14
|
+
);
|
|
15
|
+
expect(screen.getByTestId('table-pagination-total-entries')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { CSSObject } from 'styled-components';
|
|
2
|
+
|
|
3
|
+
// Gets the width of the previous pinned columns
|
|
4
|
+
export const getPreviousHeadersWidth = (position: number, pinnedColumnIds: string[]): number => {
|
|
5
|
+
let totalWidth = 0;
|
|
6
|
+
|
|
7
|
+
// Add checkbox column width if it's pinned
|
|
8
|
+
if (pinnedColumnIds.includes('checkbox-column')) {
|
|
9
|
+
const checkboxCell = document.querySelector('.rdt_TableCol:not([data-column-id])');
|
|
10
|
+
if (checkboxCell) {
|
|
11
|
+
totalWidth += (checkboxCell as HTMLElement).offsetWidth;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Add widths of other pinned columns before this position
|
|
16
|
+
const previousHeaders = Array.from({ length: position }, (_, i) => {
|
|
17
|
+
const header = document.querySelector(`[data-column-id="sticky-column-${i}"]`);
|
|
18
|
+
if (header && pinnedColumnIds.includes(`sticky-column-${i}`)) {
|
|
19
|
+
return header as HTMLElement;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}).filter((header): header is HTMLElement => header !== null);
|
|
23
|
+
|
|
24
|
+
// Calculate base width from previous columns
|
|
25
|
+
totalWidth = previousHeaders.reduce((acc, header) => {
|
|
26
|
+
return acc + header.offsetWidth;
|
|
27
|
+
}, totalWidth);
|
|
28
|
+
|
|
29
|
+
// Leave this for if we ever try to fix the auto width columns
|
|
30
|
+
// There is a bug where borders cause the offset to be wrong, this keeps it in sync.
|
|
31
|
+
// Add offset that increases by 1 every two columns after index 1
|
|
32
|
+
// if (position >= 2) {
|
|
33
|
+
// const additionalOffset = Math.floor((position - 2) / 2) + 1;
|
|
34
|
+
// totalWidth += additionalOffset;
|
|
35
|
+
// }
|
|
36
|
+
|
|
37
|
+
return totalWidth;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Applies sticky styles to the column header
|
|
41
|
+
export const applyStickyStylesToTableHeader = async (position: number, left: number) => {
|
|
42
|
+
const header = document.querySelector(`[data-column-id="sticky-column-${position}"]`);
|
|
43
|
+
if (header) {
|
|
44
|
+
(header as HTMLElement).style.position = 'sticky';
|
|
45
|
+
(header as HTMLElement).style.left = `${left}px`;
|
|
46
|
+
(header as HTMLElement).style.zIndex = '3';
|
|
47
|
+
(header as HTMLElement).style.backgroundColor =
|
|
48
|
+
'var(--pf-table-pinned-column-background-color)';
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Sorts the pinned columns so that any column that is pinned comes before any column that is not.
|
|
53
|
+
export const sortPinnedColumns = <T>(columns: T[], pinnedColumnIds: string[]): T[] => {
|
|
54
|
+
return [...columns].sort((a, b) => {
|
|
55
|
+
const aIsPinned = pinnedColumnIds.includes((a as any).id);
|
|
56
|
+
const bIsPinned = pinnedColumnIds.includes((b as any).id);
|
|
57
|
+
|
|
58
|
+
if (aIsPinned && !bIsPinned) return -1; // a comes first
|
|
59
|
+
if (!aIsPinned && bIsPinned) return 1; // b comes first
|
|
60
|
+
return 0; // maintain relative order for columns with same pinned state
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Gets the styles for the pinned columns
|
|
65
|
+
export const getPinnedColumnStyles = (
|
|
66
|
+
isPinned: boolean,
|
|
67
|
+
index: number,
|
|
68
|
+
pinnedColumnIds: string[],
|
|
69
|
+
): CSSObject => {
|
|
70
|
+
return isPinned
|
|
71
|
+
? {
|
|
72
|
+
position: 'sticky',
|
|
73
|
+
left: `${getPreviousHeadersWidth(index, pinnedColumnIds)}px`,
|
|
74
|
+
zIndex: 3,
|
|
75
|
+
backgroundColor: 'var(--pf-table-pinned-column-background-color)',
|
|
76
|
+
}
|
|
77
|
+
: {
|
|
78
|
+
position: undefined,
|
|
79
|
+
left: undefined,
|
|
80
|
+
zIndex: undefined,
|
|
81
|
+
backgroundColor: undefined,
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const clearStickyStyles = (header: HTMLElement) => {
|
|
86
|
+
header.style.position = '';
|
|
87
|
+
header.style.left = '';
|
|
88
|
+
header.style.zIndex = '';
|
|
89
|
+
header.style.backgroundColor = '';
|
|
90
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useEffect, useCallback, useMemo } from 'react';
|
|
2
|
+
import { TableColumn } from '../types';
|
|
3
|
+
import { sortPinnedColumns, getPreviousHeadersWidth } from '../components/helpers';
|
|
4
|
+
import { processColumns } from '../utils/processColumns';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook to manage pinned columns in a table
|
|
8
|
+
* Handles initialization, toggling, positioning and resizing of pinned columns
|
|
9
|
+
*/
|
|
10
|
+
export const usePinnedColumnsManager = <T>(
|
|
11
|
+
columns: TableColumn<T>[],
|
|
12
|
+
canPinColumns: boolean,
|
|
13
|
+
onPinnedColumnsChange?: (pinnedColumnIds: string[]) => void,
|
|
14
|
+
) => {
|
|
15
|
+
const pinnedColumnIds = columns.filter((column) => column.isPinned).map((column) => column.id);
|
|
16
|
+
|
|
17
|
+
// `dataColumnIds` is the list of IDs used as `data-column-id` attributes on the table headers and cells
|
|
18
|
+
const dataColumnIds = useMemo(() => {
|
|
19
|
+
const ids = columns
|
|
20
|
+
.map((column, index) => (column.isPinned ? `sticky-column-${index}` : null))
|
|
21
|
+
.filter((id): id is string => id !== null);
|
|
22
|
+
|
|
23
|
+
return ids.length > 0 ? ['checkbox-column', ...ids] : ids;
|
|
24
|
+
}, [columns]);
|
|
25
|
+
|
|
26
|
+
// Toggle individual column pin state
|
|
27
|
+
const togglePinnedColumn = useCallback(
|
|
28
|
+
(columnId: string) => {
|
|
29
|
+
const prevPinnedColumns = pinnedColumnIds;
|
|
30
|
+
|
|
31
|
+
// Handle unpinning
|
|
32
|
+
if (prevPinnedColumns.some((id) => id === columnId)) {
|
|
33
|
+
onPinnedColumnsChange?.(prevPinnedColumns.filter((id) => id !== columnId));
|
|
34
|
+
} else {
|
|
35
|
+
onPinnedColumnsChange?.(prevPinnedColumns.concat(columnId));
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
[pinnedColumnIds, onPinnedColumnsChange],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Handle resize events and recalculate pinned column positions
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!canPinColumns) return;
|
|
44
|
+
|
|
45
|
+
const recalculatePositions = () => {
|
|
46
|
+
// Reset all column styles and remove last-pinned-column class
|
|
47
|
+
const allCells = document.querySelectorAll('.rdt_TableCol, .rdt_TableCell');
|
|
48
|
+
allCells.forEach((cell) => {
|
|
49
|
+
(cell as HTMLElement).style.position = '';
|
|
50
|
+
(cell as HTMLElement).style.left = '';
|
|
51
|
+
(cell as HTMLElement).style.zIndex = '';
|
|
52
|
+
(cell as HTMLElement).style.backgroundColor = '';
|
|
53
|
+
(cell as HTMLElement).style.borderRight = '';
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Apply styles to pinned columns
|
|
57
|
+
dataColumnIds.forEach((column, index) => {
|
|
58
|
+
const isLastPinnedColumn = index === dataColumnIds.length - 1;
|
|
59
|
+
|
|
60
|
+
if (column === 'checkbox-column') {
|
|
61
|
+
// Handle header checkbox
|
|
62
|
+
const headerCheckbox = document.querySelector('.rdt_TableCol:not([data-column-id])');
|
|
63
|
+
if (headerCheckbox) {
|
|
64
|
+
(headerCheckbox as HTMLElement).style.position = 'sticky';
|
|
65
|
+
(headerCheckbox as HTMLElement).style.left = '0';
|
|
66
|
+
(headerCheckbox as HTMLElement).style.zIndex = '4';
|
|
67
|
+
(headerCheckbox as HTMLElement).style.backgroundColor =
|
|
68
|
+
'var(--pf-table-background-color)';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Handle cell checkboxes
|
|
72
|
+
const cellCheckboxes = document.querySelectorAll('.rdt_TableCell:first-child');
|
|
73
|
+
cellCheckboxes.forEach((cell) => {
|
|
74
|
+
(cell as HTMLElement).style.position = 'sticky';
|
|
75
|
+
(cell as HTMLElement).style.left = '0';
|
|
76
|
+
(cell as HTMLElement).style.zIndex = '2';
|
|
77
|
+
(cell as HTMLElement).style.backgroundColor =
|
|
78
|
+
'var(--pf-table-pinned-column-background-color)';
|
|
79
|
+
if (isLastPinnedColumn) {
|
|
80
|
+
(cell as HTMLElement).style.borderRight =
|
|
81
|
+
`2px solid var(--pf-table-pinned-column-border-color)`;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
const columnIndex = parseInt(column.split('-')[2]);
|
|
86
|
+
const left = getPreviousHeadersWidth(columnIndex, dataColumnIds);
|
|
87
|
+
|
|
88
|
+
// Headers
|
|
89
|
+
const headers = document.querySelectorAll(
|
|
90
|
+
`.rdt_TableCol[data-column-id="sticky-column-${columnIndex}"]`,
|
|
91
|
+
);
|
|
92
|
+
headers.forEach((header) => {
|
|
93
|
+
(header as HTMLElement).style.position = 'sticky';
|
|
94
|
+
(header as HTMLElement).style.left = `${left}px`;
|
|
95
|
+
(header as HTMLElement).style.zIndex = '2';
|
|
96
|
+
(header as HTMLElement).style.backgroundColor =
|
|
97
|
+
'var(--pf-table-pinned-column-background-color)';
|
|
98
|
+
if (isLastPinnedColumn) {
|
|
99
|
+
(header as HTMLElement).style.borderRight =
|
|
100
|
+
`2px solid var(--pf-table-pinned-column-border-color)`;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Cells
|
|
105
|
+
const cells = document.querySelectorAll(
|
|
106
|
+
`.rdt_TableCell[data-column-id="sticky-column-${columnIndex}"]`,
|
|
107
|
+
);
|
|
108
|
+
cells.forEach((cell) => {
|
|
109
|
+
(cell as HTMLElement).style.position = 'sticky';
|
|
110
|
+
(cell as HTMLElement).style.left = `${left}px`;
|
|
111
|
+
(cell as HTMLElement).style.zIndex = '2';
|
|
112
|
+
(cell as HTMLElement).style.backgroundColor =
|
|
113
|
+
'var(--pf-table-pinned-column-background-color)';
|
|
114
|
+
if (isLastPinnedColumn) {
|
|
115
|
+
(cell as HTMLElement).style.borderRight =
|
|
116
|
+
`2px solid var(--pf-table-pinned-column-border-color)`;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Set up resize observers
|
|
124
|
+
const table = document.querySelector('.rdt_Table');
|
|
125
|
+
const resizeObserver = new ResizeObserver(recalculatePositions);
|
|
126
|
+
|
|
127
|
+
if (table) {
|
|
128
|
+
resizeObserver.observe(table);
|
|
129
|
+
}
|
|
130
|
+
window.addEventListener('resize', recalculatePositions);
|
|
131
|
+
|
|
132
|
+
return () => {
|
|
133
|
+
resizeObserver.disconnect();
|
|
134
|
+
window.removeEventListener('resize', recalculatePositions);
|
|
135
|
+
};
|
|
136
|
+
}, [canPinColumns, dataColumnIds]);
|
|
137
|
+
|
|
138
|
+
// Process columns for rendering with pin state
|
|
139
|
+
const columnsWithPinning = canPinColumns
|
|
140
|
+
? sortPinnedColumns(processColumns(columns, dataColumnIds, togglePinnedColumn), dataColumnIds)
|
|
141
|
+
: columns;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
columnsWithPinning, // Columns with pin state and handlers applied
|
|
145
|
+
};
|
|
146
|
+
};
|