@purpurds/table 7.0.0 → 7.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@purpurds/table",
3
- "version": "7.0.0",
3
+ "version": "7.2.0",
4
4
  "license": "AGPL-3.0-only",
5
5
  "main": "./dist/table.cjs.js",
6
6
  "types": "./dist/table.d.ts",
@@ -17,22 +17,22 @@
17
17
  "dependencies": {
18
18
  "@tanstack/react-table": "~8.21.2",
19
19
  "classnames": "~2.5.0",
20
- "@purpurds/badge": "7.0.0",
21
- "@purpurds/cta-link": "7.0.0",
22
- "@purpurds/button": "7.0.0",
23
- "@purpurds/checkbox": "7.0.0",
24
- "@purpurds/drawer": "7.0.0",
25
- "@purpurds/heading": "7.0.0",
26
- "@purpurds/icon": "7.0.0",
27
- "@purpurds/link": "7.0.0",
28
- "@purpurds/paragraph": "7.0.0",
29
- "@purpurds/skeleton": "7.0.0",
30
- "@purpurds/select": "7.0.0",
31
- "@purpurds/text-field": "7.0.0",
32
- "@purpurds/tooltip": "7.0.0",
33
- "@purpurds/tokens": "7.0.0",
34
- "@purpurds/visually-hidden": "7.0.0",
35
- "@purpurds/toggle": "7.0.0"
20
+ "@purpurds/checkbox": "7.2.0",
21
+ "@purpurds/badge": "7.2.0",
22
+ "@purpurds/drawer": "7.2.0",
23
+ "@purpurds/icon": "7.2.0",
24
+ "@purpurds/link": "7.2.0",
25
+ "@purpurds/cta-link": "7.2.0",
26
+ "@purpurds/button": "7.2.0",
27
+ "@purpurds/paragraph": "7.2.0",
28
+ "@purpurds/skeleton": "7.2.0",
29
+ "@purpurds/select": "7.2.0",
30
+ "@purpurds/tokens": "7.2.0",
31
+ "@purpurds/tooltip": "7.2.0",
32
+ "@purpurds/visually-hidden": "7.2.0",
33
+ "@purpurds/heading": "7.2.0",
34
+ "@purpurds/text-field": "7.2.0",
35
+ "@purpurds/toggle": "7.2.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@rushstack/eslint-patch": "~1.10.0",
@@ -57,13 +57,13 @@
57
57
  "vitest": "^3.1.2",
58
58
  "vitest-axe": "~0.1.0",
59
59
  "vitest-canvas-mock": "~0.3.3",
60
- "@purpurds/autocomplete": "7.0.0",
60
+ "@purpurds/autocomplete": "7.2.0",
61
61
  "@purpurds/component-rig": "1.0.0",
62
- "@purpurds/grid": "7.0.0",
63
- "@purpurds/illustrative-icon": "7.0.0",
64
- "@purpurds/listbox": "7.0.0",
65
- "@purpurds/pagination": "7.0.0",
66
- "@purpurds/label": "7.0.0"
62
+ "@purpurds/grid": "7.2.0",
63
+ "@purpurds/illustrative-icon": "7.2.0",
64
+ "@purpurds/listbox": "7.2.0",
65
+ "@purpurds/label": "7.2.0",
66
+ "@purpurds/pagination": "7.2.0"
67
67
  },
68
68
  "scripts": {
69
69
  "build:dev": "vite",
@@ -0,0 +1,65 @@
1
+ import React from "react";
2
+ import { Cell } from "@tanstack/react-table";
3
+ import { render, screen } from "@testing-library/react";
4
+ import { vi } from "vitest";
5
+
6
+ import { BodyTextCell } from "./body-text-cell";
7
+
8
+ // Mock the useTruncatedTooltip hook
9
+ vi.mock("../use-truncated-hook", () => ({
10
+ default: vi.fn(() => ({
11
+ showPopover: false,
12
+ containerRef: { current: null },
13
+ onMouseEnter: vi.fn(),
14
+ onMouseLeave: vi.fn(),
15
+ })),
16
+ }));
17
+
18
+ const createMockCell = (value: unknown) =>
19
+ ({
20
+ getValue: vi.fn().mockReturnValue(value),
21
+ getContext: vi.fn(),
22
+ id: "test-id",
23
+ renderValue: vi.fn(),
24
+ row: { original: {} },
25
+ column: {
26
+ getSize: vi.fn().mockReturnValue(100),
27
+ },
28
+ } as unknown as Cell<unknown, unknown>);
29
+
30
+ describe("BodyTextCell", () => {
31
+ it("renders text value correctly", () => {
32
+ const cell = createMockCell("Test Value");
33
+ render(<BodyTextCell cell={cell} />);
34
+
35
+ expect(screen.getByText("Test Value")).toBeInTheDocument();
36
+ });
37
+
38
+ it("renders number 0 correctly", () => {
39
+ const cell = createMockCell(0);
40
+ render(<BodyTextCell cell={cell} />);
41
+
42
+ expect(screen.getByText(0)).toBeInTheDocument();
43
+ });
44
+
45
+ it("renders EmptyCell when value is null", () => {
46
+ const cell = createMockCell(null);
47
+ const { container } = render(<BodyTextCell cell={cell} />);
48
+
49
+ expect(container.querySelector("p")).toHaveTextContent("-");
50
+ });
51
+
52
+ it("renders EmptyCell when value is undefined", () => {
53
+ const cell = createMockCell(undefined);
54
+ const { container } = render(<BodyTextCell cell={cell} />);
55
+
56
+ expect(container.querySelector("p")).toHaveTextContent("-");
57
+ });
58
+
59
+ it("renders EmptyCell when value is an empty string", () => {
60
+ const cell = createMockCell("");
61
+ const { container } = render(<BodyTextCell cell={cell} />);
62
+
63
+ expect(container.querySelector("p")).toHaveTextContent("-");
64
+ });
65
+ });
@@ -18,7 +18,7 @@ export const BodyTextCell = <TData extends RowData>({ cell }: BodyTextCellProps<
18
18
  const value = cell.getValue<string>();
19
19
  const { showPopover, containerRef, onMouseEnter, onMouseLeave } = useTruncatedTooltip();
20
20
 
21
- if (!value) {
21
+ if (value === null || value === undefined || value === "") {
22
22
  return <EmptyCell />;
23
23
  }
24
24
 
@@ -0,0 +1,65 @@
1
+ import React from "react";
2
+ import { Cell } from "@tanstack/react-table";
3
+ import { render, screen } from "@testing-library/react";
4
+ import { vi } from "vitest";
5
+
6
+ import { LeadTextCell } from "./lead-text-cell";
7
+
8
+ // Mock the useTruncatedTooltip hook
9
+ vi.mock("../use-truncated-hook", () => ({
10
+ default: vi.fn(() => ({
11
+ showPopover: false,
12
+ containerRef: { current: null },
13
+ onMouseEnter: vi.fn(),
14
+ onMouseLeave: vi.fn(),
15
+ })),
16
+ }));
17
+
18
+ const createMockCell = (value: unknown) =>
19
+ ({
20
+ getValue: vi.fn().mockReturnValue(value),
21
+ getContext: vi.fn(),
22
+ id: "test-id",
23
+ renderValue: vi.fn(),
24
+ row: { original: {} },
25
+ column: {
26
+ getSize: vi.fn().mockReturnValue(100),
27
+ },
28
+ } as unknown as Cell<unknown, unknown>);
29
+
30
+ describe("LeadTextCell", () => {
31
+ it("renders text value correctly", () => {
32
+ const cell = createMockCell("Test Value");
33
+ render(<LeadTextCell cell={cell} />);
34
+
35
+ expect(screen.getByText("Test Value")).toBeInTheDocument();
36
+ });
37
+
38
+ it("renders number 0 correctly", () => {
39
+ const cell = createMockCell(0);
40
+ render(<LeadTextCell cell={cell} />);
41
+
42
+ expect(screen.getByText(0)).toBeInTheDocument();
43
+ });
44
+
45
+ it("renders EmptyCell when value is null", () => {
46
+ const cell = createMockCell(null);
47
+ const { container } = render(<LeadTextCell cell={cell} />);
48
+
49
+ expect(container.querySelector("p")).toHaveTextContent("-");
50
+ });
51
+
52
+ it("renders EmptyCell when value is undefined", () => {
53
+ const cell = createMockCell(undefined);
54
+ const { container } = render(<LeadTextCell cell={cell} />);
55
+
56
+ expect(container.querySelector("p")).toHaveTextContent("-");
57
+ });
58
+
59
+ it("renders EmptyCell when value is an empty string", () => {
60
+ const cell = createMockCell("");
61
+ const { container } = render(<LeadTextCell cell={cell} />);
62
+
63
+ expect(container.querySelector("p")).toHaveTextContent("-");
64
+ });
65
+ });
@@ -11,7 +11,7 @@ export type LeadTextCellProps<TData> = {
11
11
  export const LeadTextCell = <TData extends RowData>({ cell }: LeadTextCellProps<TData>) => {
12
12
  const value = cell.getValue<string>();
13
13
 
14
- if (!value) {
14
+ if (value === null || value === undefined || value === "") {
15
15
  return <EmptyCell />;
16
16
  }
17
17
 
@@ -46,7 +46,7 @@ describe("Data table - Export drawer", () => {
46
46
  const closeButton = within(screen.getByTestId(Selectors.EXPORT_DRAWER.HEADER_ROW)).getByRole(
47
47
  "button"
48
48
  );
49
- expect(closeButton).toHaveAttribute("aria-label", "Close drawer");
49
+ expect(closeButton).toBeInTheDocument();
50
50
  });
51
51
 
52
52
  it("should send an 'onClose' event when closeButton clicked", async () => {
@@ -87,10 +87,6 @@ describe("Data Table - Settings drawer", () => {
87
87
  ).toHaveTextContent("Reset settings");
88
88
  });
89
89
 
90
- it("should have a close drawer button", () => {
91
- expect(closeButton).toHaveAttribute("aria-label", "Close drawer");
92
- });
93
-
94
90
  it("should be possible to close the drawer", async () => {
95
91
  await userEvent.click(closeButton);
96
92
  expect(screen.queryByTestId(Selectors.SETTINGS_DRAWER.CONTENT)).toBeNull();
@@ -7,7 +7,6 @@
7
7
  overflow: hidden;
8
8
  flex-direction: column;
9
9
  gap: var(--purpur-spacing-200);
10
- z-index: 1;
11
10
  background-color: inherit;
12
11
  }
13
12
 
@@ -156,23 +155,32 @@
156
155
 
157
156
  &:has(.purpur-table-row-cell__row-selection),
158
157
  &:has(.purpur-table-row-cell__row-toggle) {
159
- .purpur-table-row-cell:hover {
160
- &:first-of-type {
161
- &::before {
162
- content: "";
163
- position: absolute;
164
- left: calc(-1 * var(--purpur-border-width-xs));
165
- top: 0;
166
- bottom: 0;
167
- width: var(--purpur-border-width-md);
168
- background-color: var(--purpur-color-border-weak);
169
- }
158
+ .purpur-table-row-cell:first-of-type {
159
+ transform: translateZ(0);
160
+
161
+ &::after {
162
+ content: "";
163
+ position: absolute;
164
+ left: calc(-1 * var(--purpur-border-width-xs));
165
+ top: 0;
166
+ bottom: 0;
167
+ width: var(--purpur-border-width-md);
168
+ opacity: 0;
169
+ transition: opacity var(--purpur-motion-duration-150) ease;
170
+ background-color: var(--purpur-color-border-weak);
171
+ pointer-events: none;
172
+ }
173
+
174
+ &:hover::after {
175
+ opacity: 1;
170
176
  }
171
177
  }
172
178
  }
173
179
 
174
- &--selected td:first-of-type {
175
- &::before {
180
+ &.purpur-table-row--selected .purpur-table-row-cell:first-of-type {
181
+ transform: translateZ(0);
182
+
183
+ &::after {
176
184
  content: "";
177
185
  position: absolute;
178
186
  left: calc(-1 * var(--purpur-border-width-xs));
@@ -180,6 +188,8 @@
180
188
  bottom: 0;
181
189
  width: var(--purpur-border-width-md);
182
190
  background-color: var(--purpur-color-border-interactive-primary);
191
+ opacity: 1;
192
+ pointer-events: none;
183
193
  }
184
194
  }
185
195
  }
@@ -156,6 +156,14 @@ const meta = {
156
156
  control: "boolean",
157
157
  description: "Full width of the table",
158
158
  },
159
+ stickyFirstColumn: {
160
+ control: "boolean",
161
+ description: "Sticky first column",
162
+ },
163
+ stickyHeaders: {
164
+ control: "boolean",
165
+ description: "Sticky headers",
166
+ },
159
167
  },
160
168
  } satisfies Meta<typeof Table<TableData>>;
161
169
 
@@ -280,6 +288,8 @@ export const Showcase: StoryTableData = {
280
288
  columns: columnDef,
281
289
  loading: true,
282
290
  fullWidth: true,
291
+ stickyFirstColumn: true,
292
+ stickyHeaders: true,
283
293
  },
284
294
  parameters: {
285
295
  docs: {
@@ -392,6 +402,8 @@ manages filtering, sorting and pagination internally.
392
402
  onSecondaryButtonClick={() => console.log("Secondary button clicked")}
393
403
  rowSelectionAriaLabels={args.rowSelectionAriaLabels}
394
404
  fullWidth={args.fullWidth}
405
+ stickyFirstColumn={args.stickyFirstColumn}
406
+ stickyHeaders={args.stickyHeaders}
395
407
  />
396
408
  );
397
409
  },
@@ -282,7 +282,14 @@ describe("Data Table", () => {
282
282
 
283
283
  describe("When loading is false", () => {
284
284
  beforeEach(() => {
285
- rerender(<Table columns={createColumnDefSmall()} data={tableDataSmall} loading={false} />);
285
+ rerender(
286
+ <Table
287
+ columns={createColumnDefSmall()}
288
+ data={tableDataSmall}
289
+ loading={false}
290
+ skeletonRows={5}
291
+ />
292
+ );
286
293
  });
287
294
 
288
295
  it("should show table data", () => {
@@ -304,6 +311,92 @@ describe("Data Table", () => {
304
311
  });
305
312
  });
306
313
 
314
+ describe("Sticky Table Features", () => {
315
+ describe("Default values", () => {
316
+ beforeEach(() => {
317
+ render(<Table columns={createColumnDefSmall()} data={tableDataSmall} />);
318
+
319
+ table = screen.getByRole("table");
320
+ });
321
+
322
+ it("should have sticky header by default", () => {
323
+ const tableHeader = getTableHead(table);
324
+ const headerCells = within(tableHeader).getAllByRole("columnheader");
325
+
326
+ headerCells.forEach((cell) => {
327
+ expect(cell.className).toContain("purpur-table-column-header-cell__sticky-header");
328
+ });
329
+ });
330
+
331
+ it("should have sticky first column by default", () => {
332
+ const tableBody = getTableBody(table);
333
+ const rows = within(tableBody).getAllByRole("row");
334
+
335
+ rows.forEach((row) => {
336
+ const firstCell = within(row).getAllByRole("cell")[0];
337
+ expect(firstCell.className).toContain("purpur-table-row-cell__sticky-column");
338
+ });
339
+ });
340
+ });
341
+
342
+ it("should disable sticky headers when stickyHeaders is false", () => {
343
+ render(
344
+ <Table
345
+ columns={createColumnDefSmall()}
346
+ data={tableDataSmall}
347
+ enableToolbar={true}
348
+ stickyHeaders={false}
349
+ settingsDrawerCopy={copy.settingsDrawer}
350
+ toolbarCopy={{
351
+ buttons: {
352
+ settings: copy.toolbar.buttons.settings,
353
+ },
354
+ ariaLabels: {
355
+ settings: copy.toolbar.ariaLabels.settings,
356
+ },
357
+ }}
358
+ />
359
+ );
360
+
361
+ table = screen.getByRole("table");
362
+ const tableHeader = getTableHead(table);
363
+ const headerCells = within(tableHeader).getAllByRole("columnheader");
364
+
365
+ headerCells.forEach((cell) => {
366
+ expect(cell.className).not.toContain("purpur-table-column-header-cell__sticky-header");
367
+ });
368
+ });
369
+
370
+ it("should disable sticky first column when stickyFirstColumn is false", () => {
371
+ render(
372
+ <Table
373
+ columns={createColumnDefSmall()}
374
+ data={tableDataSmall}
375
+ enableToolbar={true}
376
+ stickyFirstColumn={false}
377
+ settingsDrawerCopy={copy.settingsDrawer}
378
+ toolbarCopy={{
379
+ buttons: {
380
+ settings: copy.toolbar.buttons.settings,
381
+ },
382
+ ariaLabels: {
383
+ settings: copy.toolbar.ariaLabels.settings,
384
+ },
385
+ }}
386
+ />
387
+ );
388
+
389
+ table = screen.getByRole("table");
390
+ const tableBody = getTableBody(table);
391
+ const rows = within(tableBody).getAllByRole("row");
392
+
393
+ rows.forEach((row) => {
394
+ const firstCell = within(row).getAllByRole("cell")[0];
395
+ expect(firstCell).not.toHaveClass("purpur-table__row-cell--sticky");
396
+ });
397
+ });
398
+ });
399
+
307
400
  describe("Accessibility", () => {
308
401
  it("is accessible", async () => {
309
402
  const { container } = render(
package/src/table.tsx CHANGED
@@ -71,6 +71,8 @@ export type TableProps<TData extends RowData> = {
71
71
  data: TData[];
72
72
  paginationComponent?: ReactElement<PaginationProps>;
73
73
  fullWidth?: boolean;
74
+ stickyHeaders?: boolean;
75
+ stickyFirstColumn?: boolean;
74
76
  onRowsCountChange?: (rowsCount: number) => void;
75
77
  } & (WithToolbarProps | WithoutToolbarProps) &
76
78
  (WithSortingProps | WithoutSortingProps) &
@@ -104,6 +106,8 @@ export const Table = <TData extends RowData>({
104
106
  skeletonRows,
105
107
  sortingAriaLabels,
106
108
  state,
109
+ stickyFirstColumn: stickyFirstColumnProp = true,
110
+ stickyHeaders: stickyHeadersProp = true,
107
111
  toolbarCopy,
108
112
  toolbarTotalRowCount,
109
113
  variant = "primary",
@@ -123,8 +127,8 @@ export const Table = <TData extends RowData>({
123
127
  const [showColumnFiltersEnabled, setShowColumnFiltersEnabled] = useState(
124
128
  Boolean(props.enableFilters)
125
129
  );
126
- const [stickyFirstColumn, setStickyFirstColumn] = useState(true);
127
- const [stickyHeaders, setStickyHeaders] = useState(true);
130
+ const [stickyFirstColumn, setStickyFirstColumn] = useState(stickyFirstColumnProp);
131
+ const [stickyHeaders, setStickyHeaders] = useState(stickyHeadersProp);
128
132
  const prevShowOnlySelectedRows = useRef(showOnlySelectedRows);
129
133
  const [isScrolled, setIsScrolled] = useState(false);
130
134
  const tableContainerRef = useRef<HTMLTableElement>(null);
@@ -286,7 +290,7 @@ export const Table = <TData extends RowData>({
286
290
 
287
291
  const showBorder = (index: number) => {
288
292
  // Only show the border for sticky columns when the table is scrolled
289
- return isScrolled && getStickyColumn(index);
293
+ return isScrolled && stickyFirstColumn && getStickyColumn(index);
290
294
  };
291
295
 
292
296
  const tableRows = tanstackTable.getRowModel().rows;