From 09924769bdb35fa0fe4ab573b927da78670041e3 Mon Sep 17 00:00:00 2001 From: nellicus Date: Mon, 13 Oct 2025 15:28:42 +0200 Subject: [PATCH 1/6] implement table configurable columns --- src/components/Table/Table.stories.tsx | 74 +++++++ src/components/Table/Table.tsx | 271 +++++++++++++++++++++++-- 2 files changed, 330 insertions(+), 15 deletions(-) diff --git a/src/components/Table/Table.stories.tsx b/src/components/Table/Table.stories.tsx index 69b9f27b..7942fac5 100644 --- a/src/components/Table/Table.stories.tsx +++ b/src/components/Table/Table.stories.tsx @@ -135,3 +135,77 @@ export const Sortable: StoryObj = { ); }, }; + +export const ConfigurableColumns: StoryObj = { + 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 ( +
+

+ Click the settings icon in the top-right corner to configure visible columns. + The "Company" column is mandatory and cannot be hidden. + Uses default localStorage with key "click-ui-table-column-visibility-demo-table". +

+ + + ); + }, +}; + +export const ConfigurableColumnsCustomStorage: StoryObj = { + 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 => { + try { + const stored = localStorage.getItem(`my-app-columns-${tableId}`); + return stored ? JSON.parse(stored) : {}; + } catch { + return {}; + } + }; + + const handleSave = (tableId: string, visibility: Record) => { + try { + localStorage.setItem(`my-app-columns-${tableId}`, JSON.stringify(visibility)); + } catch { + // Handle error silently + } + }; + + return ( +
+

+ This example uses custom storage with key prefix "my-app-columns-". + You can provide your own onLoadColumnVisibility and onSaveColumnVisibility + for custom storage (API, IndexedDB, sessionStorage, etc.). +

+
+ + ); + }, +}; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index f1639957..50fb6c75 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,4 +1,13 @@ -import { FC, HTMLAttributes, MouseEvent, ReactNode, forwardRef, useMemo } from "react"; +import { + FC, + HTMLAttributes, + MouseEvent, + ReactNode, + forwardRef, + useMemo, + useState, + useEffect, +} from "react"; import { styled } from "styled-components"; import { CheckedState } from "@radix-ui/react-checkbox"; @@ -11,6 +20,7 @@ import { Icon, IconButton, Text, + Popover, } from "@/components"; type SortDir = "asc" | "desc"; @@ -23,6 +33,8 @@ export interface TableHeaderType extends HTMLAttributes { sortDir?: SortDir; sortPosition?: HorizontalDirection; width?: string; + mandatory?: boolean; + id?: string; } const StyledHeader = styled.th<{ $size: TableSize }>` @@ -107,6 +119,9 @@ interface TheadProps { size: TableSize; rows: TableRowType[]; selectedIds: (number | string)[]; + enableColumnVisibility?: boolean; + visibleColumns: Record; + onVisibilityChange: (columnId: string, visible: boolean) => void; } const Thead = ({ @@ -118,23 +133,33 @@ const Thead = ({ size, rows, selectedIds, + enableColumnVisibility, + visibleColumns, + onVisibilityChange, }: TheadProps) => { const onSort = (header: TableHeaderType, headerIndex: number) => () => { if (typeof onSortProp === "function" && header.isSortable) { onSortProp(header.sortDir === "asc" ? "desc" : "asc", header, headerIndex); } }; + + const visibleHeaders = headers.filter((header, index) => { + const columnId = header.id || `column-${index}`; + return visibleColumns[columnId] !== false; + }); + return ( <> {isSelectable && } - {headers.map((headerProps, index) => ( + {visibleHeaders.map((headerProps, index) => ( ))} {actionsList.length > 0 && } + {enableColumnVisibility && } @@ -150,7 +175,7 @@ const Thead = ({ /> )} - {headers.map(({ width, ...headerProps }, index) => ( + {visibleHeaders.map(({ width, ...headerProps }, index) => ( )} + {enableColumnVisibility && ( + + + + )} @@ -404,6 +441,10 @@ interface CommonTableProps size?: TableSize; showHeader?: boolean; rowHeight?: string; + tableId?: string; + enableColumnVisibility?: boolean; + onLoadColumnVisibility?: (tableId: string) => Record; + onSaveColumnVisibility?: (tableId: string, visibility: Record) => void; } type SelectReturnValue = { @@ -435,6 +476,8 @@ interface TableBodyRowProps extends Omit { actionsList: Array; size: TableSize; rowHeight?: string; + visibleColumns: Record; + enableColumnVisibility?: boolean; } const TableBodyRow = ({ @@ -452,10 +495,24 @@ const TableBodyRow = ({ size, actionsList, rowHeight, + visibleColumns, + enableColumnVisibility, ...rowProps }: TableBodyRowProps) => { const isDeletable = typeof onDelete === "function"; const isEditable = typeof onEdit === "function"; + + const visibleItems = items.filter((_, cellIndex) => { + const header = headers[cellIndex]; + const columnId = header?.id || `column-${cellIndex}`; + return visibleColumns[columnId] !== false; + }); + + const visibleHeaders = headers.filter((header, index) => { + const columnId = header.id || `column-${index}`; + return visibleColumns[columnId] !== false; + }); + return ( )} - {items.map(({ label, ...cellProps }, cellIndex) => ( - - {headers[cellIndex] && {headers[cellIndex].label}} - {label} - - ))} + {visibleItems.map(({ label, ...cellProps }, visibleIndex) => { + const originalIndex = items.findIndex(item => item === visibleItems[visibleIndex]); + return ( + + {visibleHeaders[visibleIndex] && ( + {visibleHeaders[visibleIndex].label} + )} + {label} + + ); + })} {actionsList.length > 0 && ( @@ -512,6 +574,7 @@ const TableBodyRow = ({ )} + {enableColumnVisibility && } ); }; @@ -576,6 +639,10 @@ const Table = forwardRef( size = "sm", showHeader = true, rowHeight, + tableId, + enableColumnVisibility = false, + onLoadColumnVisibility, + onSaveColumnVisibility, ...props }, ref @@ -583,6 +650,42 @@ const Table = forwardRef( const isDeletable = typeof onDelete === "function"; const isEditable = typeof onEdit === "function"; + // Initialize column visibility from storage + const [visibleColumns, setVisibleColumns] = useState>(() => { + if (!enableColumnVisibility || !tableId) return {}; + + const loadFn = onLoadColumnVisibility || defaultLoadColumnVisibility; + const stored = loadFn(tableId); + const initial: Record = {}; + + headers.forEach((header, index) => { + const columnId = header.id || `column-${index}`; + // If mandatory, always visible. Otherwise, check stored preference (default true) + if (header.mandatory) { + initial[columnId] = true; + } else { + initial[columnId] = stored[columnId] !== false; + } + }); + + return initial; + }); + + // Save to storage when visibility changes + useEffect(() => { + if (enableColumnVisibility && tableId) { + const saveFn = onSaveColumnVisibility || defaultSaveColumnVisibility; + saveFn(tableId, visibleColumns); + } + }, [visibleColumns, enableColumnVisibility, tableId, onSaveColumnVisibility]); + + const handleVisibilityChange = (columnId: string, visible: boolean) => { + setVisibleColumns(prev => ({ + ...prev, + [columnId]: visible, + })); + }; + const onRowSelect = (id: number | string) => (checked: boolean): void => { @@ -612,6 +715,13 @@ const Table = forwardRef( actionsList.push("editAction"); } + const visibleHeadersCount = enableColumnVisibility + ? headers.filter((header, index) => { + const columnId = header.id || `column-${index}`; + return visibleColumns[columnId] !== false; + }).length + : headers.length; + return ( {hasRows && showHeader && ( @@ -641,15 +751,19 @@ const Table = forwardRef( size={size} rows={rows} selectedIds={selectedIds} + enableColumnVisibility={enableColumnVisibility} + visibleColumns={visibleColumns} + onVisibilityChange={handleVisibilityChange} /> )} {(loading || !hasRows) && ( ( } size={size} rowHeight={rowHeight} + visibleColumns={visibleColumns} + enableColumnVisibility={enableColumnVisibility} {...rowProps} /> ))} @@ -777,4 +893,129 @@ const StyledTable = styled.table` } `; +// Default storage implementation (used as fallback) +const defaultLoadColumnVisibility = (tableId: string): Record => { + const key = `click-ui-table-column-visibility-${tableId}`; + + try { + const stored = localStorage.getItem(key); + return stored ? JSON.parse(stored) : {}; + } catch { + return {}; + } +}; + +const defaultSaveColumnVisibility = ( + tableId: string, + visibility: Record +) => { + const key = `click-ui-table-column-visibility-${tableId}`; + + try { + localStorage.setItem(key, JSON.stringify(visibility)); + } catch { + // Silently fail if localStorage is not available + } +}; + +// Styled components for column visibility popover +const ColumnVisibilityContainer = styled.div` + display: flex; + flex-direction: column; + gap: 0.5rem; + min-width: 200px; + max-width: 300px; +`; + +const ColumnVisibilityItem = styled.label<{ $disabled?: boolean }>` + display: flex; + align-items: center; + gap: 0.5rem; + cursor: ${({ $disabled }) => ($disabled ? "not-allowed" : "pointer")}; + opacity: ${({ $disabled }) => ($disabled ? 0.6 : 1)}; + user-select: none; +`; + +const ColumnVisibilityLabel = styled.span` + ${({ theme }) => ` + font: ${theme.click.table.cell.text.default}; + color: ${theme.click.table.row.color.text.default}; + `} +`; + +const ColumnVisibilityHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: 0.5rem; + ${({ theme }) => ` + border-bottom: ${theme.click.table.cell.stroke} solid ${theme.click.table.row.color.stroke.default}; + `} + margin-bottom: 0.5rem; +`; + +// Column Visibility Popover Component +interface ColumnVisibilityPopoverProps { + headers: Array; + visibleColumns: Record; + onVisibilityChange: (columnId: string, visible: boolean) => void; +} + +const ColumnVisibilityPopover: FC = ({ + headers, + visibleColumns, + onVisibilityChange, +}) => { + return ( + + + + + + + + + Columns + + + {headers.map((header, index) => { + const columnId = header.id || `column-${index}`; + const isMandatory = header.mandatory === true; + const isVisible = visibleColumns[columnId] !== false; + + return ( + + { + if (!isMandatory) { + onVisibilityChange(columnId, checked === true); + } + }} + /> + {header.label} + + ); + })} + + + + ); +}; + export { Table }; From 2739b03e9b1f5dc689c1d23089003edc21041f97 Mon Sep 17 00:00:00 2001 From: nellicus Date: Mon, 13 Oct 2025 15:39:18 +0200 Subject: [PATCH 2/6] update tests and prettify --- src/components/Table/Table.stories.tsx | 8 +- src/components/Table/Table.test.tsx | 118 +++++++++++++++++++++++++ src/components/Table/Table.tsx | 4 +- 3 files changed, 125 insertions(+), 5 deletions(-) diff --git a/src/components/Table/Table.stories.tsx b/src/components/Table/Table.stories.tsx index 7942fac5..7d5ec908 100644 --- a/src/components/Table/Table.stories.tsx +++ b/src/components/Table/Table.stories.tsx @@ -154,8 +154,8 @@ export const ConfigurableColumns: StoryObj = {

Click the settings icon in the top-right corner to configure visible columns. - The "Company" column is mandatory and cannot be hidden. - Uses default localStorage with key "click-ui-table-column-visibility-demo-table". + The "Company" column is mandatory and cannot be hidden. Uses default + localStorage with key "click-ui-table-column-visibility-demo-table".

@@ -197,8 +197,8 @@ export const ConfigurableColumnsCustomStorage: StoryObj = {

This example uses custom storage with key prefix "my-app-columns-". - You can provide your own onLoadColumnVisibility and onSaveColumnVisibility - for custom storage (API, IndexedDB, sessionStorage, etc.). + You can provide your own onLoadColumnVisibility and onSaveColumnVisibility for + custom storage (API, IndexedDB, sessionStorage, etc.).

{ 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", () => { + const { queryByTestId, container } = renderCUI( +
+ ); + + // 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!); + + const checkboxes = container.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", () => { + const headersWithMandatory = [ + { id: "company", label: "Company", mandatory: true }, + { id: "contact", label: "Contact" }, + { id: "country", label: "Country" }, + ]; + + const { queryByTestId, container } = renderCUI( +
+ ); + + const settingsButton = queryByTestId("column-visibility-button"); + fireEvent.click(settingsButton!); + + const checkboxes = container.querySelectorAll('[role="checkbox"]'); + const companyCheckbox = checkboxes[0]; + + // Mandatory column checkbox should be disabled + expect(companyCheckbox.getAttribute("data-disabled")).toBe("true"); + }); + + it("should call onLoadColumnVisibility and onSaveColumnVisibility", () => { + const onLoad = vi.fn(() => ({ + company: true, + contact: false, + country: true, + })); + const onSave = vi.fn(); + + const { queryByTestId, container } = renderCUI( +
+ ); + + // 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!); + const checkboxes = container.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, + }); + }); + }); }); diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 50fb6c75..1752d9bc 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -533,7 +533,9 @@ const TableBodyRow = ({ )} {visibleItems.map(({ label, ...cellProps }, visibleIndex) => { - const originalIndex = items.findIndex(item => item === visibleItems[visibleIndex]); + const originalIndex = items.findIndex( + item => item === visibleItems[visibleIndex] + ); return ( Date: Mon, 13 Oct 2025 15:50:13 +0200 Subject: [PATCH 3/6] fix tests --- src/components/Table/Table.test.tsx | 39 +++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/components/Table/Table.test.tsx b/src/components/Table/Table.test.tsx index 0ebc311a..dd3105fe 100644 --- a/src/components/Table/Table.test.tsx +++ b/src/components/Table/Table.test.tsx @@ -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"; @@ -106,7 +106,7 @@ describe("Table", () => { { id: "country", label: "Country" }, ]; - it("should render column visibility button and toggle columns", () => { + it("should render column visibility button and toggle columns", async () => { const { queryByTestId, container } = renderCUI(
{ const settingsButton = queryByTestId("column-visibility-button"); fireEvent.click(settingsButton!); - const checkboxes = container.querySelectorAll('[role="checkbox"]'); + // 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); @@ -143,14 +149,14 @@ describe("Table", () => { expect(headerTexts).toContain("Country"); }); - it("should not allow hiding mandatory columns", () => { + 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, container } = renderCUI( + const { queryByTestId } = renderCUI(
{ const settingsButton = queryByTestId("column-visibility-button"); fireEvent.click(settingsButton!); - const checkboxes = container.querySelectorAll('[role="checkbox"]'); + // 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 - expect(companyCheckbox.getAttribute("data-disabled")).toBe("true"); + // 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", () => { + it("should call onLoadColumnVisibility and onSaveColumnVisibility", async () => { const onLoad = vi.fn(() => ({ company: true, contact: false, @@ -201,7 +213,14 @@ describe("Table", () => { // Toggle Country column visibility const settingsButton = queryByTestId("column-visibility-button"); fireEvent.click(settingsButton!); - const checkboxes = container.querySelectorAll('[role="checkbox"]'); + + // 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); From 984c879f1f005596bd13b8881cf21f84259db873 Mon Sep 17 00:00:00 2001 From: nellicus Date: Mon, 13 Oct 2025 15:57:48 +0200 Subject: [PATCH 4/6] fix doublequotes in test --- src/components/Table/Table.test.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Table/Table.test.tsx b/src/components/Table/Table.test.tsx index dd3105fe..304cf285 100644 --- a/src/components/Table/Table.test.tsx +++ b/src/components/Table/Table.test.tsx @@ -133,11 +133,11 @@ describe("Table", () => { // Wait for popover to open and find checkboxes in the document (portal renders outside container) await waitFor(() => { - const checkboxes = document.querySelectorAll('[role="checkbox"]'); + const checkboxes = document.querySelectorAll("[role=\"checkbox\"]"); expect(checkboxes.length).toBeGreaterThan(0); }); - const checkboxes = document.querySelectorAll('[role="checkbox"]'); + const checkboxes = document.querySelectorAll("[role=\"checkbox\"]"); const contactCheckbox = checkboxes[1]; fireEvent.click(contactCheckbox); @@ -171,11 +171,11 @@ describe("Table", () => { // Wait for popover to open and find checkboxes in the document (portal renders outside container) await waitFor(() => { - const checkboxes = document.querySelectorAll('[role="checkbox"]'); + const checkboxes = document.querySelectorAll("[role=\"checkbox\"]"); expect(checkboxes.length).toBeGreaterThan(0); }); - const checkboxes = document.querySelectorAll('[role="checkbox"]'); + const checkboxes = document.querySelectorAll("[role=\"checkbox\"]"); const companyCheckbox = checkboxes[0]; // Mandatory column checkbox should be disabled (Radix UI sets data-disabled="" when disabled) @@ -216,11 +216,11 @@ describe("Table", () => { // Wait for popover to open and find checkboxes in the document (portal renders outside container) await waitFor(() => { - const checkboxes = document.querySelectorAll('[role="checkbox"]'); + const checkboxes = document.querySelectorAll("[role=\"checkbox\"]"); expect(checkboxes.length).toBeGreaterThan(0); }); - const checkboxes = document.querySelectorAll('[role="checkbox"]'); + const checkboxes = document.querySelectorAll("[role=\"checkbox\"]"); const countryCheckbox = checkboxes[2]; fireEvent.click(countryCheckbox); From b275249a6768ed8f03f0783c769b5b951089f13f Mon Sep 17 00:00:00 2001 From: nellicus Date: Mon, 13 Oct 2025 16:10:05 +0200 Subject: [PATCH 5/6] fix eslint to allow strings to use single-quotes or double-quotes so long as the string contains a quote that would have to be escaped otherwise --- .eslintrc.cjs | 2 +- src/components/Grid/useColumns.test.ts | 1 - src/components/Table/Table.test.tsx | 12 ++++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 12e303c7..e370cd8b 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -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", diff --git a/src/components/Grid/useColumns.test.ts b/src/components/Grid/useColumns.test.ts index bddc0cb0..ca33b0e8 100644 --- a/src/components/Grid/useColumns.test.ts +++ b/src/components/Grid/useColumns.test.ts @@ -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); }); diff --git a/src/components/Table/Table.test.tsx b/src/components/Table/Table.test.tsx index 304cf285..dd3105fe 100644 --- a/src/components/Table/Table.test.tsx +++ b/src/components/Table/Table.test.tsx @@ -133,11 +133,11 @@ describe("Table", () => { // Wait for popover to open and find checkboxes in the document (portal renders outside container) await waitFor(() => { - const checkboxes = document.querySelectorAll("[role=\"checkbox\"]"); + const checkboxes = document.querySelectorAll('[role="checkbox"]'); expect(checkboxes.length).toBeGreaterThan(0); }); - const checkboxes = document.querySelectorAll("[role=\"checkbox\"]"); + const checkboxes = document.querySelectorAll('[role="checkbox"]'); const contactCheckbox = checkboxes[1]; fireEvent.click(contactCheckbox); @@ -171,11 +171,11 @@ describe("Table", () => { // Wait for popover to open and find checkboxes in the document (portal renders outside container) await waitFor(() => { - const checkboxes = document.querySelectorAll("[role=\"checkbox\"]"); + const checkboxes = document.querySelectorAll('[role="checkbox"]'); expect(checkboxes.length).toBeGreaterThan(0); }); - const checkboxes = document.querySelectorAll("[role=\"checkbox\"]"); + const checkboxes = document.querySelectorAll('[role="checkbox"]'); const companyCheckbox = checkboxes[0]; // Mandatory column checkbox should be disabled (Radix UI sets data-disabled="" when disabled) @@ -216,11 +216,11 @@ describe("Table", () => { // Wait for popover to open and find checkboxes in the document (portal renders outside container) await waitFor(() => { - const checkboxes = document.querySelectorAll("[role=\"checkbox\"]"); + const checkboxes = document.querySelectorAll('[role="checkbox"]'); expect(checkboxes.length).toBeGreaterThan(0); }); - const checkboxes = document.querySelectorAll("[role=\"checkbox\"]"); + const checkboxes = document.querySelectorAll('[role="checkbox"]'); const countryCheckbox = checkboxes[2]; fireEvent.click(countryCheckbox); From 217e9a98b2ab3abe2136f8ce5ef06c4e2c93b6f4 Mon Sep 17 00:00:00 2001 From: nellicus Date: Wed, 15 Oct 2025 17:56:58 +0200 Subject: [PATCH 6/6] change settings icon, remove unneeded header, add selected (preselects the non required column), rename mandatory -> required, use lock icon for required col --- src/components/Table/Table.stories.tsx | 10 +++--- src/components/Table/Table.tsx | 44 ++++++++------------------ 2 files changed, 18 insertions(+), 36 deletions(-) diff --git a/src/components/Table/Table.stories.tsx b/src/components/Table/Table.stories.tsx index 7d5ec908..c48268f3 100644 --- a/src/components/Table/Table.stories.tsx +++ b/src/components/Table/Table.stories.tsx @@ -139,9 +139,9 @@ export const Sortable: StoryObj = { export const ConfigurableColumns: StoryObj = { args: { headers: [ - { id: "company", label: "Company", mandatory: true }, - { id: "contact", label: "Contact" }, - { id: "country", label: "Country" }, + { id: "company", label: "Company", required: true }, + { id: "contact", label: "Contact", selected: true }, + { id: "country", label: "Country", selected: false }, ], rows, enableColumnVisibility: true, @@ -154,7 +154,7 @@ export const ConfigurableColumns: StoryObj = {

Click the settings icon in the top-right corner to configure visible columns. - The "Company" column is mandatory and cannot be hidden. Uses default + The "Company" column is required and cannot be hidden. Uses default localStorage with key "click-ui-table-column-visibility-demo-table".

@@ -166,7 +166,7 @@ export const ConfigurableColumns: StoryObj = { export const ConfigurableColumnsCustomStorage: StoryObj = { args: { headers: [ - { id: "company", label: "Company", mandatory: true }, + { id: "company", label: "Company", required: true }, { id: "contact", label: "Contact" }, { id: "country", label: "Country" }, ], diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 1752d9bc..b2647c67 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -33,7 +33,8 @@ export interface TableHeaderType extends HTMLAttributes { sortDir?: SortDir; sortPosition?: HorizontalDirection; width?: string; - mandatory?: boolean; + required?: boolean; + selected?: boolean; id?: string; } @@ -662,11 +663,11 @@ const Table = forwardRef( headers.forEach((header, index) => { const columnId = header.id || `column-${index}`; - // If mandatory, always visible. Otherwise, check stored preference (default true) - if (header.mandatory) { + // If required, always visible. Otherwise, if selected show by default, else hidden + if (header.required) { initial[columnId] = true; } else { - initial[columnId] = stored[columnId] !== false; + initial[columnId] = stored[columnId] ?? header.selected; } }); @@ -945,17 +946,6 @@ const ColumnVisibilityLabel = styled.span` `} `; -const ColumnVisibilityHeader = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - padding-bottom: 0.5rem; - ${({ theme }) => ` - border-bottom: ${theme.click.table.cell.stroke} solid ${theme.click.table.row.color.stroke.default}; - `} - margin-bottom: 0.5rem; -`; - // Column Visibility Popover Component interface ColumnVisibilityPopoverProps { headers: Array; @@ -973,7 +963,7 @@ const ColumnVisibilityPopover: FC = ({ @@ -983,33 +973,25 @@ const ColumnVisibilityPopover: FC = ({ sideOffset={8} > - - - Columns - - {headers.map((header, index) => { const columnId = header.id || `column-${index}`; - const isMandatory = header.mandatory === true; + const isRequired = header.required === true; const isVisible = visibleColumns[columnId] !== false; - return ( + return ( - : { - if (!isMandatory) { + if (!isRequired) { onVisibilityChange(columnId, checked === true); } }} - /> + />} {header.label} );