From e21de88f15e57bce462d32654e30b45a49472711 Mon Sep 17 00:00:00 2001 From: Fiona Date: Thu, 16 Jul 2026 02:30:36 -0700 Subject: [PATCH 1/4] docs(table): state the new menu semantics on RowActions Co-Authored-By: Claude Fable 5 --- src/components/EnhancedTable/types.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/EnhancedTable/types.ts b/src/components/EnhancedTable/types.ts index 5e4210523..46065ebe1 100644 --- a/src/components/EnhancedTable/types.ts +++ b/src/components/EnhancedTable/types.ts @@ -23,9 +23,14 @@ export interface RowAction { } export interface RowActions { - /** surfaced as icon buttons, left of the kebab */ + /** signature actions, always surfaced as icon buttons (left of secondary actions) */ inline?: RowAction[]; - /** expanded into icon buttons when the row fits `actionMaxIcons`; kept in the kebab menu otherwise */ + /** + * Secondary actions. Presentation belongs to the component, not the caller: + * expanded into icon buttons while the row fits `actionMaxIcons`, collapsed + * into the kebab menu otherwise. Listing an action here no longer means it + * renders collapsed — set `collapsed: true` on an item to force that. + */ menu?: RowAction[]; } From c23ee2ff852d6a3e0a41accdf0c77e7649f8f457 Mon Sep 17 00:00:00 2001 From: Fiona Date: Fri, 17 Jul 2026 01:45:24 -0700 Subject: [PATCH 2/4] feat(table): cap surfaced row actions at 2 icons once a kebab exists Rows with at most 3 actions (and no node/collapsed items) still expand fully. Bigger rows now surface inline items plus non-danger menu items up to 2 icons and collapse the rest - danger items included - into the kebab, which always keeps at least 2 items (icons are demoted back when needed). Default actionMaxIcons drops from 4 to 3. --- .../EnhancedTable/RowActionCell.test.ts | 63 ++++++++++++++----- .../EnhancedTable/RowActionCell.tsx | 35 ++++++++--- src/components/EnhancedTable/types.ts | 9 +-- 3 files changed, 78 insertions(+), 29 deletions(-) diff --git a/src/components/EnhancedTable/RowActionCell.test.ts b/src/components/EnhancedTable/RowActionCell.test.ts index 18c7bf027..d5717be08 100644 --- a/src/components/EnhancedTable/RowActionCell.test.ts +++ b/src/components/EnhancedTable/RowActionCell.test.ts @@ -25,47 +25,76 @@ describe('splitRowActions', () => { const act = (key: string, extra: Partial = {}): RowAction => ({ key, ...extra }); const keys = (list: RowAction[]) => list.map((a) => a.key); - it('expands kebab actions into icons when the row fits the limit, danger last', () => { + it('expands a row within the limit entirely into icons, danger last, no kebab', () => { const actions: RowActions = { inline: [act('run')], - menu: [act('delete', { danger: true }), act('edit'), act('copy')], + menu: [act('delete', { danger: true }), act('edit')], }; const { icons, kebab } = splitRowActions(actions); - expect(keys(icons)).toEqual(['run', 'edit', 'copy', 'delete']); + expect(keys(icons)).toEqual(['run', 'edit', 'delete']); expect(kebab).toEqual([]); }); - it('keeps the full kebab when the row exceeds the limit', () => { + it('surfaces at most 2 icons and collapses the rest when the row exceeds the limit', () => { const actions: RowActions = { inline: [act('run')], - menu: [act('edit'), act('copy'), act('export'), act('delete', { danger: true })], + menu: [act('history'), act('edit'), act('copy'), act('delete', { danger: true })], }; const { icons, kebab } = splitRowActions(actions); - expect(keys(icons)).toEqual(['run']); - expect(keys(kebab)).toEqual(['edit', 'copy', 'export', 'delete']); + expect(keys(icons)).toEqual(['run', 'history']); + expect(keys(kebab)).toEqual(['edit', 'copy', 'delete']); }); - it('honors a custom limit', () => { - const actions: RowActions = { menu: [act('edit'), act('copy'), act('export'), act('offline'), act('delete')] }; - expect(keys(splitRowActions(actions, 5).icons)).toEqual(['edit', 'copy', 'export', 'offline', 'delete']); - expect(keys(splitRowActions(actions, 2).kebab)).toEqual(['edit', 'copy', 'export', 'offline', 'delete']); + it('sinks danger items into the kebab instead of promoting them', () => { + const actions: RowActions = { + menu: [act('delete', { danger: true }), act('edit'), act('copy'), act('export')], + }; + const { icons, kebab } = splitRowActions(actions); + expect(keys(icons)).toEqual(['edit', 'copy']); + expect(keys(kebab)).toEqual(['delete', 'export']); + }); + + it('skips collapsed items during promotion so pinned low-frequency actions stay inside', () => { + const actions: RowActions = { + inline: [act('run')], + menu: [act('history', { collapsed: true }), act('edit'), act('copy'), act('export', { collapsed: true }), act('delete', { danger: true })], + }; + const { icons, kebab } = splitRowActions(actions); + expect(keys(icons)).toEqual(['run', 'edit']); + expect(keys(kebab)).toEqual(['history', 'copy', 'export', 'delete']); }); - it('pins node and collapsed items in the kebab while the rest expand', () => { + it('forces a kebab when a node item exists, even within the limit', () => { const actions: RowActions = { - menu: [act('edit'), act('bespoke', { node: 'x' }), act('reset', { collapsed: true }), act('delete', { danger: true })], + menu: [act('edit'), act('bespoke', { node: 'x' }), act('copy'), act('delete', { danger: true })], }; const { icons, kebab } = splitRowActions(actions); - expect(keys(icons)).toEqual(['edit', 'delete']); - expect(keys(kebab)).toEqual(['bespoke', 'reset']); + expect(keys(icons)).toEqual(['edit', 'copy']); + expect(keys(kebab)).toEqual(['bespoke', 'delete']); + }); + + it('demotes promoted icons so the kebab never holds a single item', () => { + const actions: RowActions = { + menu: [act('edit'), act('copy'), act('bespoke', { node: 'x' })], + }; + const { icons, kebab } = splitRowActions(actions); + expect(keys(icons)).toEqual(['edit']); + expect(keys(kebab)).toEqual(['copy', 'bespoke']); + }); + + it('honors a custom limit', () => { + const actions: RowActions = { menu: [act('edit'), act('copy'), act('export'), act('offline')] }; + expect(keys(splitRowActions(actions, 4).icons)).toEqual(['edit', 'copy', 'export', 'offline']); + expect(keys(splitRowActions(actions, 2).icons)).toEqual(['edit', 'copy']); + expect(keys(splitRowActions(actions, 2).kebab)).toEqual(['export', 'offline']); }); it('ignores hidden actions when counting against the limit', () => { const actions: RowActions = { - menu: [act('edit'), act('copy'), act('export', { visible: false }), act('offline'), act('delete', { danger: true })], + menu: [act('edit'), act('copy', { visible: false }), act('offline'), act('delete', { danger: true })], }; const { icons, kebab } = splitRowActions(actions); - expect(keys(icons)).toEqual(['edit', 'copy', 'offline', 'delete']); + expect(keys(icons)).toEqual(['edit', 'offline', 'delete']); expect(kebab).toEqual([]); }); }); diff --git a/src/components/EnhancedTable/RowActionCell.tsx b/src/components/EnhancedTable/RowActionCell.tsx index 5c17a89a2..bb5cbbfb2 100644 --- a/src/components/EnhancedTable/RowActionCell.tsx +++ b/src/components/EnhancedTable/RowActionCell.tsx @@ -8,25 +8,44 @@ import type { RowAction, RowActions } from './types'; const visibleOnly = (list?: RowAction[]) => (list || []).filter((a) => a.visible !== false); -export const DEFAULT_ACTION_MAX_ICONS = 4; +export const DEFAULT_ACTION_MAX_ICONS = 3; +// Once a kebab exists, cap surfaced icons at 2 and keep at least 2 items inside — +// a one-item overflow menu costs a click without saving any space. +const MAX_SURFACED_ICONS = 2; +const MIN_KEBAB_ITEMS = 2; /** * Split a row's actions into surfaced icon buttons and kebab leftovers. - * Kebab actions expand into icon buttons when the whole row fits within `maxIcons` - * (danger items last); `node` and `collapsed: true` items always stay in the kebab. - * Rows exceeding the limit keep today's layout: inline icons + full kebab. + * A row with no `node`/`collapsed: true` items and at most `maxIcons` actions + * expands entirely into icon buttons (danger items last), with no kebab. + * Any other row gets a kebab: `inline` items stay surfaced, non-danger menu + * items are promoted until 2 icons show, and everything else — including all + * danger items — goes into the kebab (menu order preserved). Icons are demoted + * back if needed so the kebab never holds fewer than 2 items. */ export function splitRowActions(actions: RowActions, maxIcons = DEFAULT_ACTION_MAX_ICONS) { const inline = visibleOnly(actions.inline); const menu = visibleOnly(actions.menu); const pinned = menu.filter((a) => a.node || a.collapsed); const expandable = menu.filter((a) => !a.node && !a.collapsed); - if (inline.length + expandable.length > maxIcons) { - return { icons: inline, kebab: menu }; + if (!pinned.length && inline.length + expandable.length <= maxIcons) { + return { + icons: [...inline, ...expandable.filter((a) => !a.danger), ...expandable.filter((a) => a.danger)], + kebab: [] as RowAction[], + }; + } + const promoted: RowAction[] = []; + for (const action of expandable) { + if (inline.length + promoted.length >= MAX_SURFACED_ICONS) break; + if (action.danger) continue; + promoted.push(action); + } + while (promoted.length && menu.length - promoted.length < MIN_KEBAB_ITEMS) { + promoted.pop(); } return { - icons: [...inline, ...expandable.filter((a) => !a.danger), ...expandable.filter((a) => a.danger)], - kebab: pinned, + icons: [...inline, ...promoted], + kebab: menu.filter((a) => !promoted.includes(a)), }; } diff --git a/src/components/EnhancedTable/types.ts b/src/components/EnhancedTable/types.ts index 46065ebe1..0dbb63b53 100644 --- a/src/components/EnhancedTable/types.ts +++ b/src/components/EnhancedTable/types.ts @@ -27,9 +27,10 @@ export interface RowActions { inline?: RowAction[]; /** * Secondary actions. Presentation belongs to the component, not the caller: - * expanded into icon buttons while the row fits `actionMaxIcons`, collapsed - * into the kebab menu otherwise. Listing an action here no longer means it - * renders collapsed — set `collapsed: true` on an item to force that. + * rows with at most `actionMaxIcons` actions expand entirely into icon buttons; + * bigger rows surface at most 2 icons and collapse the rest (danger items + * included) into a kebab holding at least 2 items. Listing an action here no + * longer means it renders collapsed — set `collapsed: true` to force that. */ menu?: RowAction[]; } @@ -41,7 +42,7 @@ export interface EnhancedTableProps extends TableProps { actionColumn?: Partial>; /** compact header: tighter thead padding + smaller sort hit-area, for tables embedded inside tabs/cards */ compactHeader?: boolean; - /** max icon buttons per row; rows exceeding it keep kebab actions collapsed (default 4) */ + /** rows with at most this many actions expand fully into icons, no kebab (default 3) */ actionMaxIcons?: number; /** auto-inject default sorter for columns without `sorter` (default false); column `sorter` always wins */ autoSortColumns?: boolean; From 4ec5d5eb50af9520b418351bc041d0bb09b5ced6 Mon Sep 17 00:00:00 2001 From: Fiona Date: Fri, 17 Jul 2026 02:28:08 -0700 Subject: [PATCH 3/4] feat(table): keep one row-action layout per table Decide expand-vs-kebab once per table instead of per row: if any row needs a kebab (too many actions, or node/collapsed items), every row renders in kebab layout, so icons align vertically and the kebab sits in a fixed position. Light rows may then hold a single kebab item - the accepted price of column-aligned consistency, replacing the per-row min-2-kebab-items demotion. --- .../EnhancedTable/EnhancedTable.tsx | 21 +++++++++++------- .../EnhancedTable/RowActionCell.test.ts | 14 +++++++----- .../EnhancedTable/RowActionCell.tsx | 22 +++++++++---------- src/components/EnhancedTable/types.ts | 11 +++++----- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/components/EnhancedTable/EnhancedTable.tsx b/src/components/EnhancedTable/EnhancedTable.tsx index d89626697..b1195b8a9 100644 --- a/src/components/EnhancedTable/EnhancedTable.tsx +++ b/src/components/EnhancedTable/EnhancedTable.tsx @@ -49,16 +49,21 @@ export default function EnhancedTable(props: En }); if (hasRowActions) { - // Auto-widen the action column so expanded icon rows never overflow legacy - // kebab-era widths: scan the rows for the widest icon layout (cheap builder - // calls; capped to bound client-side-paginated datasets). An explicit - // `actionColumn.width` still wins when it is larger. + // Scan the rows (cheap builder calls; capped to bound client-side-paginated + // datasets) to keep the whole table on one layout and one width: + // - kebabMode: if any row needs a kebab, every row renders in kebab layout, + // so icons align vertically and the kebab sits in a fixed position. + // - contentWidth: auto-widen the action column so expanded icon rows never + // overflow legacy kebab-era widths; explicit `actionColumn.width` still + // wins when it is larger. let contentWidth = 0; + let kebabMode = false; if (Array.isArray(dataSource)) { - dataSource.slice(0, 200).forEach((record, index) => { - const cfg = rowActionsRef.current?.(record, index); + const rowCfgs = dataSource.slice(0, 200).map((record, index) => rowActionsRef.current?.(record, index)); + kebabMode = rowCfgs.some((cfg) => cfg && splitRowActions(cfg, actionMaxIcons).kebab.length > 0); + rowCfgs.forEach((cfg) => { if (!cfg) return; - const { icons, kebab } = splitRowActions(cfg, actionMaxIcons); + const { icons, kebab } = splitRowActions(cfg, actionMaxIcons, kebabMode); const items = icons.length + (kebab.length ? 1 : 0); if (!items) return; // cell padding 16 + 24px per icon + 28px kebab trigger + 4px gaps @@ -74,7 +79,7 @@ export default function EnhancedTable(props: En ...actionColumn, render: (_value: unknown, record: RecordType, index: number) => { const cfg = rowActionsRef.current?.(record, index); - return cfg ? : null; + return cfg ? : null; }, }; if (typeof opColumn.width === 'number' && contentWidth > opColumn.width) { diff --git a/src/components/EnhancedTable/RowActionCell.test.ts b/src/components/EnhancedTable/RowActionCell.test.ts index d5717be08..b10ee8b5a 100644 --- a/src/components/EnhancedTable/RowActionCell.test.ts +++ b/src/components/EnhancedTable/RowActionCell.test.ts @@ -73,13 +73,17 @@ describe('splitRowActions', () => { expect(keys(kebab)).toEqual(['bespoke', 'delete']); }); - it('demotes promoted icons so the kebab never holds a single item', () => { + it('forceKebab collapses a row that would otherwise fit, keeping table layouts uniform', () => { const actions: RowActions = { - menu: [act('edit'), act('copy'), act('bespoke', { node: 'x' })], + menu: [act('edit'), act('copy'), act('delete', { danger: true })], }; - const { icons, kebab } = splitRowActions(actions); - expect(keys(icons)).toEqual(['edit']); - expect(keys(kebab)).toEqual(['copy', 'bespoke']); + const fits = splitRowActions(actions); + expect(keys(fits.icons)).toEqual(['edit', 'copy', 'delete']); + expect(fits.kebab).toEqual([]); + + const forced = splitRowActions(actions, undefined, true); + expect(keys(forced.icons)).toEqual(['edit', 'copy']); + expect(keys(forced.kebab)).toEqual(['delete']); }); it('honors a custom limit', () => { diff --git a/src/components/EnhancedTable/RowActionCell.tsx b/src/components/EnhancedTable/RowActionCell.tsx index bb5cbbfb2..2fa43c2e1 100644 --- a/src/components/EnhancedTable/RowActionCell.tsx +++ b/src/components/EnhancedTable/RowActionCell.tsx @@ -9,10 +9,8 @@ import type { RowAction, RowActions } from './types'; const visibleOnly = (list?: RowAction[]) => (list || []).filter((a) => a.visible !== false); export const DEFAULT_ACTION_MAX_ICONS = 3; -// Once a kebab exists, cap surfaced icons at 2 and keep at least 2 items inside — -// a one-item overflow menu costs a click without saving any space. +// Once a kebab exists, cap surfaced icons at 2 so heavy rows stay compact. const MAX_SURFACED_ICONS = 2; -const MIN_KEBAB_ITEMS = 2; /** * Split a row's actions into surfaced icon buttons and kebab leftovers. @@ -20,15 +18,18 @@ const MIN_KEBAB_ITEMS = 2; * expands entirely into icon buttons (danger items last), with no kebab. * Any other row gets a kebab: `inline` items stay surfaced, non-danger menu * items are promoted until 2 icons show, and everything else — including all - * danger items — goes into the kebab (menu order preserved). Icons are demoted - * back if needed so the kebab never holds fewer than 2 items. + * danger items — goes into the kebab (menu order preserved). + * `forceKebab` puts a row in kebab layout even when it would fit expanded: + * EnhancedTable sets it when any row of the table needs a kebab, so all rows + * of one table share the same layout (light rows may then hold a single + * kebab item — the accepted price of column-aligned consistency). */ -export function splitRowActions(actions: RowActions, maxIcons = DEFAULT_ACTION_MAX_ICONS) { +export function splitRowActions(actions: RowActions, maxIcons = DEFAULT_ACTION_MAX_ICONS, forceKebab = false) { const inline = visibleOnly(actions.inline); const menu = visibleOnly(actions.menu); const pinned = menu.filter((a) => a.node || a.collapsed); const expandable = menu.filter((a) => !a.node && !a.collapsed); - if (!pinned.length && inline.length + expandable.length <= maxIcons) { + if (!forceKebab && !pinned.length && inline.length + expandable.length <= maxIcons) { return { icons: [...inline, ...expandable.filter((a) => !a.danger), ...expandable.filter((a) => a.danger)], kebab: [] as RowAction[], @@ -40,9 +41,6 @@ export function splitRowActions(actions: RowActions, maxIcons = DEFAULT_ACTION_M if (action.danger) continue; promoted.push(action); } - while (promoted.length && menu.length - promoted.length < MIN_KEBAB_ITEMS) { - promoted.pop(); - } return { icons: [...inline, ...promoted], kebab: menu.filter((a) => !promoted.includes(a)), @@ -139,9 +137,9 @@ function renderMenuItem(action: RowAction, key: string, onAction: () => void) { ); } -export function RowActionCell({ actions, maxIcons }: { actions: RowActions; maxIcons?: number }) { +export function RowActionCell({ actions, maxIcons, forceKebab }: { actions: RowActions; maxIcons?: number; forceKebab?: boolean }) { const [menuOpen, setMenuOpen] = useState(false); - const { icons, kebab } = splitRowActions(actions, maxIcons); + const { icons, kebab } = splitRowActions(actions, maxIcons, forceKebab); if (!icons.length && !kebab.length) return null; const normal = kebab.filter((a) => !a.danger); diff --git a/src/components/EnhancedTable/types.ts b/src/components/EnhancedTable/types.ts index 0dbb63b53..ac2c64726 100644 --- a/src/components/EnhancedTable/types.ts +++ b/src/components/EnhancedTable/types.ts @@ -27,10 +27,11 @@ export interface RowActions { inline?: RowAction[]; /** * Secondary actions. Presentation belongs to the component, not the caller: - * rows with at most `actionMaxIcons` actions expand entirely into icon buttons; - * bigger rows surface at most 2 icons and collapse the rest (danger items - * included) into a kebab holding at least 2 items. Listing an action here no - * longer means it renders collapsed — set `collapsed: true` to force that. + * when every row of the table fits `actionMaxIcons` actions, all rows expand + * entirely into icon buttons; otherwise the whole table switches to kebab + * layout — each row surfaces at most 2 icons and collapses the rest (danger + * items included) into the kebab, so all rows stay aligned. Listing an action + * here no longer means it renders collapsed — set `collapsed: true` to force that. */ menu?: RowAction[]; } @@ -42,7 +43,7 @@ export interface EnhancedTableProps extends TableProps { actionColumn?: Partial>; /** compact header: tighter thead padding + smaller sort hit-area, for tables embedded inside tabs/cards */ compactHeader?: boolean; - /** rows with at most this many actions expand fully into icons, no kebab (default 3) */ + /** tables whose rows all have at most this many actions expand fully into icons, no kebab (default 3) */ actionMaxIcons?: number; /** auto-inject default sorter for columns without `sorter` (default false); column `sorter` always wins */ autoSortColumns?: boolean; From 0d45284bac7e6a0b8d207355c200d73c843af4d8 Mon Sep 17 00:00:00 2001 From: Fiona Date: Fri, 17 Jul 2026 04:28:45 -0700 Subject: [PATCH 4/4] refactor(table): drop the unused actionMaxIcons prop No caller across the three repos ever set it; the expand threshold (3) and surfaced-icon cap (2) are design constants of the row-action rule, not per-table knobs. splitRowActions keeps an internal maxIcons parameter for tests. --- src/components/EnhancedTable/EnhancedTable.tsx | 10 +++++----- src/components/EnhancedTable/RowActionCell.test.ts | 8 ++++---- src/components/EnhancedTable/RowActionCell.tsx | 6 +++--- src/components/EnhancedTable/types.ts | 4 +--- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/components/EnhancedTable/EnhancedTable.tsx b/src/components/EnhancedTable/EnhancedTable.tsx index b1195b8a9..b4cb138e1 100644 --- a/src/components/EnhancedTable/EnhancedTable.tsx +++ b/src/components/EnhancedTable/EnhancedTable.tsx @@ -18,7 +18,7 @@ import { defaultComparator } from './sorter'; * loses its boundary here and edits trigger a full page reload. */ export default function EnhancedTable(props: EnhancedTableProps) { - const { rowActions, actionColumn, columns, className, dataSource, compactHeader, autoSortColumns, actionMaxIcons, pagination, ...rest } = props; + const { rowActions, actionColumn, columns, className, dataSource, compactHeader, autoSortColumns, pagination, ...rest } = props; // Every paginated table gets the quick jumper, regardless of whether the caller spreads // usePagination. `pagination={false}` stays off, and an explicit caller value still wins. @@ -60,10 +60,10 @@ export default function EnhancedTable(props: En let kebabMode = false; if (Array.isArray(dataSource)) { const rowCfgs = dataSource.slice(0, 200).map((record, index) => rowActionsRef.current?.(record, index)); - kebabMode = rowCfgs.some((cfg) => cfg && splitRowActions(cfg, actionMaxIcons).kebab.length > 0); + kebabMode = rowCfgs.some((cfg) => cfg && splitRowActions(cfg).kebab.length > 0); rowCfgs.forEach((cfg) => { if (!cfg) return; - const { icons, kebab } = splitRowActions(cfg, actionMaxIcons, kebabMode); + const { icons, kebab } = splitRowActions(cfg, kebabMode); const items = icons.length + (kebab.length ? 1 : 0); if (!items) return; // cell padding 16 + 24px per icon + 28px kebab trigger + 4px gaps @@ -79,7 +79,7 @@ export default function EnhancedTable(props: En ...actionColumn, render: (_value: unknown, record: RecordType, index: number) => { const cfg = rowActionsRef.current?.(record, index); - return cfg ? : null; + return cfg ? : null; }, }; if (typeof opColumn.width === 'number' && contentWidth > opColumn.width) { @@ -89,7 +89,7 @@ export default function EnhancedTable(props: En } return allColumns; - }, [columns, actionColumn, hasRowActions, autoSortColumns, actionMaxIcons, dataSource]); + }, [columns, actionColumn, hasRowActions, autoSortColumns, dataSource]); return ( diff --git a/src/components/EnhancedTable/RowActionCell.test.ts b/src/components/EnhancedTable/RowActionCell.test.ts index b10ee8b5a..ca6a614f4 100644 --- a/src/components/EnhancedTable/RowActionCell.test.ts +++ b/src/components/EnhancedTable/RowActionCell.test.ts @@ -81,16 +81,16 @@ describe('splitRowActions', () => { expect(keys(fits.icons)).toEqual(['edit', 'copy', 'delete']); expect(fits.kebab).toEqual([]); - const forced = splitRowActions(actions, undefined, true); + const forced = splitRowActions(actions, true); expect(keys(forced.icons)).toEqual(['edit', 'copy']); expect(keys(forced.kebab)).toEqual(['delete']); }); it('honors a custom limit', () => { const actions: RowActions = { menu: [act('edit'), act('copy'), act('export'), act('offline')] }; - expect(keys(splitRowActions(actions, 4).icons)).toEqual(['edit', 'copy', 'export', 'offline']); - expect(keys(splitRowActions(actions, 2).icons)).toEqual(['edit', 'copy']); - expect(keys(splitRowActions(actions, 2).kebab)).toEqual(['export', 'offline']); + expect(keys(splitRowActions(actions, false, 4).icons)).toEqual(['edit', 'copy', 'export', 'offline']); + expect(keys(splitRowActions(actions, false, 2).icons)).toEqual(['edit', 'copy']); + expect(keys(splitRowActions(actions, false, 2).kebab)).toEqual(['export', 'offline']); }); it('ignores hidden actions when counting against the limit', () => { diff --git a/src/components/EnhancedTable/RowActionCell.tsx b/src/components/EnhancedTable/RowActionCell.tsx index 2fa43c2e1..11b903709 100644 --- a/src/components/EnhancedTable/RowActionCell.tsx +++ b/src/components/EnhancedTable/RowActionCell.tsx @@ -24,7 +24,7 @@ const MAX_SURFACED_ICONS = 2; * of one table share the same layout (light rows may then hold a single * kebab item — the accepted price of column-aligned consistency). */ -export function splitRowActions(actions: RowActions, maxIcons = DEFAULT_ACTION_MAX_ICONS, forceKebab = false) { +export function splitRowActions(actions: RowActions, forceKebab = false, maxIcons = DEFAULT_ACTION_MAX_ICONS) { const inline = visibleOnly(actions.inline); const menu = visibleOnly(actions.menu); const pinned = menu.filter((a) => a.node || a.collapsed); @@ -137,9 +137,9 @@ function renderMenuItem(action: RowAction, key: string, onAction: () => void) { ); } -export function RowActionCell({ actions, maxIcons, forceKebab }: { actions: RowActions; maxIcons?: number; forceKebab?: boolean }) { +export function RowActionCell({ actions, forceKebab }: { actions: RowActions; forceKebab?: boolean }) { const [menuOpen, setMenuOpen] = useState(false); - const { icons, kebab } = splitRowActions(actions, maxIcons, forceKebab); + const { icons, kebab } = splitRowActions(actions, forceKebab); if (!icons.length && !kebab.length) return null; const normal = kebab.filter((a) => !a.danger); diff --git a/src/components/EnhancedTable/types.ts b/src/components/EnhancedTable/types.ts index ac2c64726..b655b483c 100644 --- a/src/components/EnhancedTable/types.ts +++ b/src/components/EnhancedTable/types.ts @@ -27,7 +27,7 @@ export interface RowActions { inline?: RowAction[]; /** * Secondary actions. Presentation belongs to the component, not the caller: - * when every row of the table fits `actionMaxIcons` actions, all rows expand + * when every row of the table has at most 3 actions, all rows expand * entirely into icon buttons; otherwise the whole table switches to kebab * layout — each row surfaces at most 2 icons and collapses the rest (danger * items included) into the kebab, so all rows stay aligned. Listing an action @@ -43,8 +43,6 @@ export interface EnhancedTableProps extends TableProps { actionColumn?: Partial>; /** compact header: tighter thead padding + smaller sort hit-area, for tables embedded inside tabs/cards */ compactHeader?: boolean; - /** tables whose rows all have at most this many actions expand fully into icons, no kebab (default 3) */ - actionMaxIcons?: number; /** auto-inject default sorter for columns without `sorter` (default false); column `sorter` always wins */ autoSortColumns?: boolean; }