Skip to content
Open
Show file tree
Hide file tree
Changes from all 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", required: true },
{ id: "contact", label: "Contact", selected: true },
{ id: "country", label: "Country", selected: false },
],
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 required 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", required: 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
Loading