Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {
rules: {
"react-refresh/only-export-components": "warn",
"no-multiple-empty-lines": "error",
quotes: ["error", "double"],
quotes: ["error", "double", { "avoidEscape": true }],
"arrow-parens": ["error", "as-needed"],
"prefer-arrow-functions/prefer-arrow-functions": [
"warn",
Expand Down
1 change: 0 additions & 1 deletion src/components/Grid/useColumns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ describe("useColumns", () => {
result.current.initColumnSize(300);
result.current.onColumnResize(1, 0, "auto");
});
// eslint-disable-next-line quotes
expect(mockQuerySelector).toHaveBeenCalledWith('[data-grid-column="1"]');
expect(result.current.columnWidth(1)).toBe(122);
});
Expand Down
74 changes: 74 additions & 0 deletions src/components/Table/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,77 @@ export const Sortable: StoryObj<typeof Table> = {
);
},
};

export const ConfigurableColumns: StoryObj<typeof Table> = {
args: {
headers: [
{ id: "company", label: "Company", mandatory: true },
{ id: "contact", label: "Contact" },
{ id: "country", label: "Country" },
],
rows,
enableColumnVisibility: true,
tableId: "demo-table",
onLoadColumnVisibility: undefined,
onSaveColumnVisibility: undefined,
},
render: props => {
return (
<div>
<p style={{ marginBottom: "1rem", fontSize: "14px", color: "#666" }}>
Click the settings icon in the top-right corner to configure visible columns.
The &quot;Company&quot; column is mandatory and cannot be hidden. Uses default
localStorage with key &quot;click-ui-table-column-visibility-demo-table&quot;.
</p>
<Table {...props} />
</div>
);
},
};

export const ConfigurableColumnsCustomStorage: StoryObj<typeof Table> = {
args: {
headers: [
{ id: "company", label: "Company", mandatory: true },
{ id: "contact", label: "Contact" },
{ id: "country", label: "Country" },
],
rows,
enableColumnVisibility: true,
tableId: "custom-table",
},
render: props => {
// Example: Custom storage implementation
const handleLoad = (tableId: string): Record<string, boolean> => {
try {
const stored = localStorage.getItem(`my-app-columns-${tableId}`);
return stored ? JSON.parse(stored) : {};
} catch {
return {};
}
};

const handleSave = (tableId: string, visibility: Record<string, boolean>) => {
try {
localStorage.setItem(`my-app-columns-${tableId}`, JSON.stringify(visibility));
} catch {
// Handle error silently
}
};

return (
<div>
<p style={{ marginBottom: "1rem", fontSize: "14px", color: "#666" }}>
This example uses custom storage with key prefix &quot;my-app-columns-&quot;.
You can provide your own onLoadColumnVisibility and onSaveColumnVisibility for
custom storage (API, IndexedDB, sessionStorage, etc.).
</p>
<Table
{...props}
onLoadColumnVisibility={handleLoad}
onSaveColumnVisibility={handleSave}
/>
</div>
);
},
};
139 changes: 138 additions & 1 deletion src/components/Table/Table.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fireEvent } from "@testing-library/react";
import { fireEvent, waitFor } from "@testing-library/react";
import { Table, TableProps } from "./Table";
import { renderCUI } from "@/utils/test-utils";

Expand Down Expand Up @@ -98,4 +98,141 @@ describe("Table", () => {
fireEvent.click(rowCheckbox);
expect(onEdit).toBeCalledTimes(1);
});

describe("Column Visibility", () => {
const headersWithIds = [
{ id: "company", label: "Company" },
{ id: "contact", label: "Contact" },
{ id: "country", label: "Country" },
];

it("should render column visibility button and toggle columns", async () => {
const { queryByTestId, container } = renderCUI(
<Table
headers={headersWithIds}
rows={rows}
enableColumnVisibility={true}
tableId="test-table"
data-testid="table"
/>
);

// Column visibility button should be present
expect(queryByTestId("column-visibility-button")).not.toBeNull();

// All columns should be visible by default
let headerCells = container.querySelectorAll("thead th");
expect(headerCells.length).toBe(4); // 3 columns + 1 settings column
expect(headerCells[0].textContent).toBe("Company");
expect(headerCells[1].textContent).toBe("Contact");
expect(headerCells[2].textContent).toBe("Country");

// Open popover and hide Contact column
const settingsButton = queryByTestId("column-visibility-button");
fireEvent.click(settingsButton!);

// Wait for popover to open and find checkboxes in the document (portal renders outside container)
await waitFor(() => {
const checkboxes = document.querySelectorAll('[role="checkbox"]');
expect(checkboxes.length).toBeGreaterThan(0);
});

const checkboxes = document.querySelectorAll('[role="checkbox"]');
const contactCheckbox = checkboxes[1];
fireEvent.click(contactCheckbox);

// Contact column should now be hidden
headerCells = container.querySelectorAll("thead th");
const headerTexts = Array.from(headerCells).map(cell => cell.textContent);
expect(headerTexts).toContain("Company");
expect(headerTexts).not.toContain("Contact");
expect(headerTexts).toContain("Country");
});

it("should not allow hiding mandatory columns", async () => {
const headersWithMandatory = [
{ id: "company", label: "Company", mandatory: true },
{ id: "contact", label: "Contact" },
{ id: "country", label: "Country" },
];

const { queryByTestId } = renderCUI(
<Table
headers={headersWithMandatory}
rows={rows}
enableColumnVisibility={true}
tableId="test-table-mandatory"
data-testid="table"
/>
);

const settingsButton = queryByTestId("column-visibility-button");
fireEvent.click(settingsButton!);

// Wait for popover to open and find checkboxes in the document (portal renders outside container)
await waitFor(() => {
const checkboxes = document.querySelectorAll('[role="checkbox"]');
expect(checkboxes.length).toBeGreaterThan(0);
});

const checkboxes = document.querySelectorAll('[role="checkbox"]');
const companyCheckbox = checkboxes[0];

// Mandatory column checkbox should be disabled (Radix UI sets data-disabled="" when disabled)
expect(companyCheckbox.hasAttribute("data-disabled")).toBe(true);
});

it("should call onLoadColumnVisibility and onSaveColumnVisibility", async () => {
const onLoad = vi.fn(() => ({
company: true,
contact: false,
country: true,
}));
const onSave = vi.fn();

const { queryByTestId, container } = renderCUI(
<Table
headers={headersWithIds}
rows={rows}
enableColumnVisibility={true}
tableId="test-table-storage"
onLoadColumnVisibility={onLoad}
onSaveColumnVisibility={onSave}
data-testid="table"
/>
);

// onLoad should be called on mount
expect(onLoad).toHaveBeenCalledWith("test-table-storage");

// Contact column should be hidden based on loaded state
const headerCells = container.querySelectorAll("thead th");
const headerTexts = Array.from(headerCells).map(cell => cell.textContent);
expect(headerTexts).not.toContain("Contact");

// Toggle Country column visibility
const settingsButton = queryByTestId("column-visibility-button");
fireEvent.click(settingsButton!);

// Wait for popover to open and find checkboxes in the document (portal renders outside container)
await waitFor(() => {
const checkboxes = document.querySelectorAll('[role="checkbox"]');
expect(checkboxes.length).toBeGreaterThan(0);
});

const checkboxes = document.querySelectorAll('[role="checkbox"]');
const countryCheckbox = checkboxes[2];
fireEvent.click(countryCheckbox);

// onSave should be called with updated visibility
expect(onSave).toHaveBeenCalled();
const lastCall = onSave.mock.calls[onSave.mock.calls.length - 1];
expect(lastCall[0]).toBe("test-table-storage");
expect(lastCall[1]).toMatchObject({
company: true,
contact: false,
country: false,
});
});
});
});
Loading