@indico-data/design-system 2.48.0 → 2.50.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/table/Table.stories.d.ts +1 -0
- package/lib/components/table/components/HorizontalStickyHeader.d.ts +10 -0
- package/lib/components/table/components/__tests__/HorizontalStickyHeader.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 +4 -0
- package/lib/components/table/types.d.ts +11 -1
- package/lib/components/table/utils/processColumns.d.ts +2 -0
- package/lib/index.css +188 -89
- package/lib/index.d.ts +12 -1
- package/lib/index.esm.css +188 -89
- package/lib/index.esm.js +238 -2
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +238 -2
- package/lib/index.js.map +1 -1
- package/lib/stylesAndAnimations/utilityClasses/UtilityClassesData.d.ts +7 -0
- package/lib/stylesAndAnimations/utilityClasses/UtilityClassesTable.d.ts +1 -0
- package/lib/stylesAndAnimations/utilityClasses/UtilityClassesTable.stories.d.ts +6 -0
- package/lib/utils/getPreviousHeadersWidth.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/table/Table.mdx +134 -0
- package/src/components/table/Table.stories.tsx +71 -2
- package/src/components/table/Table.tsx +16 -1
- package/src/components/table/components/HorizontalStickyHeader.tsx +57 -0
- package/src/components/table/components/__tests__/HorizontalStickyHeader.test.tsx +104 -0
- package/src/components/table/components/helpers.ts +90 -0
- package/src/components/table/hooks/usePinnedColumnsManager.ts +146 -0
- package/src/components/table/{sampleData.ts → sampleData.tsx} +156 -1
- package/src/components/table/styles/Table.scss +32 -15
- package/src/components/table/styles/_variables.scss +2 -0
- package/src/components/table/types.ts +13 -1
- package/src/components/table/utils/processColumns.tsx +35 -0
- package/src/setup/setupTests.ts +8 -0
- package/src/storybookDocs/Permafrost.mdx +22 -11
- package/src/styles/_borders.scss +2 -1
- package/src/stylesAndAnimations/borders/BorderColor.tsx +14 -6
- package/src/stylesAndAnimations/utilityClasses/UtilityClasses.mdx +24 -0
- package/src/stylesAndAnimations/utilityClasses/UtilityClassesData.ts +230 -0
- package/src/stylesAndAnimations/utilityClasses/UtilityClassesTable.stories.tsx +13 -0
- package/src/stylesAndAnimations/utilityClasses/UtilityClassesTable.tsx +146 -0
- package/src/utils/getPreviousHeadersWidth.ts +12 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const UtilityClassesTable: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { UtilityClassesTable } from './UtilityClassesTable';
|
|
3
|
+
declare const meta: Meta<typeof UtilityClassesTable>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof UtilityClassesTable>;
|
|
6
|
+
export declare const Default: Story;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getPreviousHeadersWidth: (position: number) => number;
|
package/package.json
CHANGED
|
@@ -21,6 +21,9 @@ The `conditionalRowStyles` prop allows you to apply custom styles to specific ro
|
|
|
21
21
|
|
|
22
22
|
- **`highlighted`**: This class is used to visually emphasize specific rows. When applied, the row will inherit styles defined in the design system, making the row stand out with distinct border and background colors.
|
|
23
23
|
|
|
24
|
+
### Issues with storybook
|
|
25
|
+
You may notice the stories may behave a little strangely on storybook, this is not the case in the application. One such bug would be the checkboxes pinning on the default example when toggling props. Another will be the select all checkbox not being locked on the second pinned column example. We have not investigated as this is likely a quirk with the storybook environment.
|
|
26
|
+
|
|
24
27
|
### Example Usage
|
|
25
28
|
|
|
26
29
|
Here's an example of how to use the `conditionalRowStyles` prop to apply the `checked` and `highlighted` classes:
|
|
@@ -44,3 +47,134 @@ const conditionalRowStyles = [
|
|
|
44
47
|
conditionalRowStyles={conditionalRowStyles}
|
|
45
48
|
/>;
|
|
46
49
|
```
|
|
50
|
+
|
|
51
|
+
## Pinned Columns
|
|
52
|
+
|
|
53
|
+
The `canPinColumns` prop allows you to pin columns to the left hand side of the table. This is useful for displaying important columns that should be visible at all times.
|
|
54
|
+
|
|
55
|
+
## Known Bugs
|
|
56
|
+
A limitation of the library has caused us to compromise on the column widths. We had to choose between allowing the pinning of Fixed Width columns, or allowing the pinning of Auto Widths columns. As it stands right now, we went with Fixed Width columns. As a result, you will experience pixel drift on the third+ columns if you do not use fixed width. An example on how to use the fixed width is the following.
|
|
57
|
+
```jsx
|
|
58
|
+
columns={[
|
|
59
|
+
{
|
|
60
|
+
isPinned: false,
|
|
61
|
+
name: 'Name',
|
|
62
|
+
selector: () => {},
|
|
63
|
+
width: '150px'
|
|
64
|
+
},
|
|
65
|
+
]}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Note
|
|
69
|
+
|
|
70
|
+
It will require both `canPinColumns` to be set to `true` and the columns that you wish to have the pin icon contain `isPinned: true` or `isPinned: false` to function. If `isPinned` is undefined, no icon will appear.
|
|
71
|
+
<Canvas of={TableStories.PinnedColumns} />
|
|
72
|
+
```jsx
|
|
73
|
+
<Table
|
|
74
|
+
canPinColumns
|
|
75
|
+
columns={[
|
|
76
|
+
{
|
|
77
|
+
isPinned: false,
|
|
78
|
+
name: 'Name',
|
|
79
|
+
selector: () => {},
|
|
80
|
+
width: '150px'
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
isPinned: true,
|
|
84
|
+
name: 'Class',
|
|
85
|
+
selector: () => {},
|
|
86
|
+
width: '150px'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
isPinned: true,
|
|
90
|
+
name: 'Age',
|
|
91
|
+
selector: () => {},
|
|
92
|
+
width: '150px'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'Weapon',
|
|
96
|
+
selector: () => {}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'Backstory',
|
|
100
|
+
selector: () => {}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'Favorite Meal',
|
|
104
|
+
selector: () => {}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'Homeland',
|
|
108
|
+
selector: () => {}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'Alignment',
|
|
112
|
+
selector: () => {}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
isPinned: false,
|
|
116
|
+
name: 'Special Ability',
|
|
117
|
+
selector: () => {}
|
|
118
|
+
}
|
|
119
|
+
]}
|
|
120
|
+
data={[
|
|
121
|
+
{
|
|
122
|
+
age: 120,
|
|
123
|
+
alignment: 'Neutral Good',
|
|
124
|
+
backstory: 'Raised by wolves in the deep forests.',
|
|
125
|
+
class: 'Ranger',
|
|
126
|
+
favoriteMeal: 'Venison stew',
|
|
127
|
+
homeland: 'Silverleaf Forest',
|
|
128
|
+
name: 'Thalion',
|
|
129
|
+
specialAbility: 'Beast Speech',
|
|
130
|
+
test: 'test',
|
|
131
|
+
weapon: 'Longbow'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
age: 35,
|
|
135
|
+
alignment: 'Lawful Good',
|
|
136
|
+
backstory: 'A former soldier seeking redemption.',
|
|
137
|
+
class: 'Fighter',
|
|
138
|
+
favoriteMeal: 'Roasted boar',
|
|
139
|
+
homeland: 'Kingdom of Valorhaven',
|
|
140
|
+
name: 'Brom',
|
|
141
|
+
specialAbility: 'Battle Master',
|
|
142
|
+
test: 'test',
|
|
143
|
+
weapon: 'Greatsword'
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
age: 60,
|
|
147
|
+
alignment: 'Lawful Good',
|
|
148
|
+
backstory: 'A devoted follower of the goddess of life.',
|
|
149
|
+
class: 'Cleric',
|
|
150
|
+
favoriteMeal: 'Vegetable soup',
|
|
151
|
+
homeland: 'Temple of Dawn',
|
|
152
|
+
name: 'Elysia',
|
|
153
|
+
specialAbility: 'Divine Healing',
|
|
154
|
+
test: 'test',
|
|
155
|
+
weapon: 'Mace'
|
|
156
|
+
},
|
|
157
|
+
//... more rows
|
|
158
|
+
]}
|
|
159
|
+
dense
|
|
160
|
+
direction="ltr"
|
|
161
|
+
fixedHeader
|
|
162
|
+
noDataComponent={null}
|
|
163
|
+
onPinnedColumnsChange={() => {}}
|
|
164
|
+
pagination
|
|
165
|
+
paginationPerPage={10}
|
|
166
|
+
paginationRowsPerPageOptions={[
|
|
167
|
+
5,
|
|
168
|
+
10,
|
|
169
|
+
15,
|
|
170
|
+
20
|
|
171
|
+
]}
|
|
172
|
+
responsive
|
|
173
|
+
selectableRows
|
|
174
|
+
subHeaderAlign="center"
|
|
175
|
+
subHeaderComponent={null}
|
|
176
|
+
subHeaderWrap
|
|
177
|
+
title="Character List"
|
|
178
|
+
totalEntriesText="Showing 12 of 12 entries."
|
|
179
|
+
/>
|
|
180
|
+
```
|
|
@@ -3,6 +3,7 @@ import { Table } from './Table';
|
|
|
3
3
|
import { columns, sampleData, SampleDataRow } from './sampleData';
|
|
4
4
|
import { registerFontAwesomeIcons } from '@/setup/setupIcons';
|
|
5
5
|
import { indiconDefinitions } from '@/components/icons/indicons';
|
|
6
|
+
import { useState } from 'react';
|
|
6
7
|
|
|
7
8
|
registerFontAwesomeIcons(...Object.values(indiconDefinitions));
|
|
8
9
|
|
|
@@ -12,7 +13,8 @@ const meta: Meta = {
|
|
|
12
13
|
argTypes: {
|
|
13
14
|
columns: {
|
|
14
15
|
control: false,
|
|
15
|
-
description:
|
|
16
|
+
description:
|
|
17
|
+
'The columns to display in the table. All columns require a unique id property. For pinned columns, please see the pinned example below.',
|
|
16
18
|
table: {
|
|
17
19
|
category: 'Data',
|
|
18
20
|
type: { summary: 'array' },
|
|
@@ -251,6 +253,21 @@ const meta: Meta = {
|
|
|
251
253
|
category: 'Styling',
|
|
252
254
|
},
|
|
253
255
|
},
|
|
256
|
+
canPinColumns: {
|
|
257
|
+
control: 'boolean',
|
|
258
|
+
description:
|
|
259
|
+
'Allows the pinning of columns to the left hand side. This is required when using the column pin API',
|
|
260
|
+
table: {
|
|
261
|
+
category: 'Add-Ons',
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
onPinnedColumnsChange: {
|
|
265
|
+
control: false,
|
|
266
|
+
description: 'Callback that receives the IDs of the pinned columns when they change.',
|
|
267
|
+
table: {
|
|
268
|
+
category: 'Add-Ons',
|
|
269
|
+
},
|
|
270
|
+
},
|
|
254
271
|
// hidden props
|
|
255
272
|
onRowDoubleClicked: {
|
|
256
273
|
table: {
|
|
@@ -521,6 +538,41 @@ type Story = StoryObj<typeof Table<SampleDataRow>>;
|
|
|
521
538
|
|
|
522
539
|
export const Default: Story = {
|
|
523
540
|
args: {
|
|
541
|
+
canPinColumns: false,
|
|
542
|
+
pagination: true,
|
|
543
|
+
selectableRows: true,
|
|
544
|
+
isDisabled: false,
|
|
545
|
+
isLoading: false,
|
|
546
|
+
direction: 'ltr',
|
|
547
|
+
striped: false,
|
|
548
|
+
subHeaderAlign: 'center',
|
|
549
|
+
subHeaderWrap: true,
|
|
550
|
+
paginationRowsPerPageOptions: [5, 10, 15, 20],
|
|
551
|
+
responsive: true,
|
|
552
|
+
title: 'Character List',
|
|
553
|
+
dense: true,
|
|
554
|
+
noHeader: false,
|
|
555
|
+
noTableHead: false,
|
|
556
|
+
noDataComponent: null,
|
|
557
|
+
expandableRows: false,
|
|
558
|
+
clearSelectedRows: false,
|
|
559
|
+
subHeader: false,
|
|
560
|
+
subHeaderComponent: null,
|
|
561
|
+
fixedHeader: true,
|
|
562
|
+
paginationPerPage: 10,
|
|
563
|
+
isFullHeight: false,
|
|
564
|
+
totalEntriesText: 'Showing 12 of 12 entries.',
|
|
565
|
+
columns: columns,
|
|
566
|
+
data: sampleData,
|
|
567
|
+
},
|
|
568
|
+
render: ({ ...args }) => {
|
|
569
|
+
return <Table {...args} columns={columns} />;
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
export const PinnedColumns: Story = {
|
|
574
|
+
args: {
|
|
575
|
+
canPinColumns: true,
|
|
524
576
|
pagination: true,
|
|
525
577
|
selectableRows: true,
|
|
526
578
|
isDisabled: false,
|
|
@@ -540,11 +592,28 @@ export const Default: Story = {
|
|
|
540
592
|
clearSelectedRows: false,
|
|
541
593
|
subHeader: false,
|
|
542
594
|
subHeaderComponent: null,
|
|
595
|
+
fixedHeader: true,
|
|
543
596
|
paginationPerPage: 10,
|
|
544
597
|
isFullHeight: false,
|
|
545
598
|
totalEntriesText: 'Showing 12 of 12 entries.',
|
|
546
599
|
columns: columns,
|
|
547
600
|
data: sampleData,
|
|
548
601
|
},
|
|
549
|
-
render: ({ ...args }) =>
|
|
602
|
+
render: ({ ...args }) => {
|
|
603
|
+
const initialPinnedColumnIds = ['name', 'class'];
|
|
604
|
+
const [pinnedColumnIds, setPinnedColumnIds] = useState<string[]>(initialPinnedColumnIds);
|
|
605
|
+
|
|
606
|
+
const columnsWithPinning = columns.map((column) => ({
|
|
607
|
+
...column,
|
|
608
|
+
isPinned: pinnedColumnIds.includes(column.id as string),
|
|
609
|
+
}));
|
|
610
|
+
|
|
611
|
+
return (
|
|
612
|
+
<Table
|
|
613
|
+
{...args}
|
|
614
|
+
columns={columnsWithPinning}
|
|
615
|
+
onPinnedColumnsChange={(ids) => setPinnedColumnIds(ids)}
|
|
616
|
+
/>
|
|
617
|
+
);
|
|
618
|
+
},
|
|
550
619
|
};
|
|
@@ -3,10 +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';
|
|
9
8
|
import { TablePagination } from './components/TablePagination';
|
|
9
|
+
import { usePinnedColumnsManager } from './hooks/usePinnedColumnsManager';
|
|
10
10
|
|
|
11
11
|
export const Table = <T,>(props: TableProps<T>) => {
|
|
12
12
|
const {
|
|
@@ -22,9 +22,20 @@ export const Table = <T,>(props: TableProps<T>) => {
|
|
|
22
22
|
className,
|
|
23
23
|
paginationTotalRows,
|
|
24
24
|
totalEntriesText,
|
|
25
|
+
data,
|
|
26
|
+
columns: initialColumns,
|
|
27
|
+
canPinColumns = false,
|
|
28
|
+
onPinnedColumnsChange,
|
|
25
29
|
...rest
|
|
26
30
|
} = props;
|
|
27
31
|
|
|
32
|
+
// Turns on/off column pinning.
|
|
33
|
+
const { columnsWithPinning } = usePinnedColumnsManager(
|
|
34
|
+
initialColumns,
|
|
35
|
+
canPinColumns,
|
|
36
|
+
onPinnedColumnsChange,
|
|
37
|
+
);
|
|
38
|
+
|
|
28
39
|
const combinedClassName = classNames(className, {
|
|
29
40
|
'table--striped': striped,
|
|
30
41
|
'table-body': true,
|
|
@@ -37,6 +48,8 @@ export const Table = <T,>(props: TableProps<T>) => {
|
|
|
37
48
|
return (
|
|
38
49
|
<div className={tableWrapperClassName} data-testid="table">
|
|
39
50
|
<DataTable
|
|
51
|
+
data={data}
|
|
52
|
+
columns={columnsWithPinning}
|
|
40
53
|
responsive={responsive}
|
|
41
54
|
direction={direction as RDTDirection}
|
|
42
55
|
subHeaderAlign={subHeaderAlign as RDTAlignment}
|
|
@@ -52,6 +65,8 @@ export const Table = <T,>(props: TableProps<T>) => {
|
|
|
52
65
|
<TablePagination {...props} totalEntriesText={totalEntriesText} />
|
|
53
66
|
)}
|
|
54
67
|
paginationTotalRows={paginationTotalRows}
|
|
68
|
+
highlightOnHover
|
|
69
|
+
pointerOnHover
|
|
55
70
|
{...rest}
|
|
56
71
|
/>
|
|
57
72
|
</div>
|
|
@@ -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,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,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
|
+
};
|