From e170fb22aaad34a916142b9871ca8dd44f3deea4 Mon Sep 17 00:00:00 2001 From: KhushamBansal Date: Tue, 11 Aug 2026 14:09:59 +0530 Subject: [PATCH 1/2] [DataTableToolbar] Fix mobile single-row layout and add compactTrailing Signed-off-by: KhushamBansal --- src/__testing__/DataTableToolbar.test.tsx | 50 +++++++++++++- .../DataTableToolbar/DataTableToolbar.tsx | 66 +++++++++++++++---- .../DataTableToolbar.types.ts | 7 ++ 3 files changed, 109 insertions(+), 14 deletions(-) diff --git a/src/__testing__/DataTableToolbar.test.tsx b/src/__testing__/DataTableToolbar.test.tsx index 3f504865c..9ebdf204b 100644 --- a/src/__testing__/DataTableToolbar.test.tsx +++ b/src/__testing__/DataTableToolbar.test.tsx @@ -136,7 +136,7 @@ describe('DataTableToolbar', () => { it('pushes right section to the right when only right content is present', () => { renderWithTheme(Search} />); const rightContent = screen.getByTestId('right-content'); - const rightSection = rightContent.parentElement as HTMLElement; + const rightSection = rightContent.parentElement?.parentElement?.parentElement as HTMLElement; // RightSection has marginLeft: auto — check via computed style expect(rightSection).toBeTruthy(); expect(window.getComputedStyle(rightSection).marginLeft).toBe('auto'); @@ -161,7 +161,7 @@ describe('DataTableToolbar', () => { const leftContent = screen.getByTestId('left-btn'); const rightContent = screen.getByTestId('right-content'); const leftSection = leftContent.parentElement as HTMLElement; - const rightSection = rightContent.parentElement as HTMLElement; + const rightSection = rightContent.parentElement?.parentElement?.parentElement as HTMLElement; expect(leftSection).toBeTruthy(); expect(rightSection).toBeTruthy(); @@ -170,5 +170,51 @@ describe('DataTableToolbar', () => { // Right section has auto margin to push it right expect(window.getComputedStyle(rightSection).marginLeft).toBe('auto'); }); + + it('hides trailing controls when compactTrailing is true', () => { + renderWithTheme( + Search} + filter={Filter} + viewSwitch={Grid/Table} + compactTrailing + /> + ); + expect(screen.getByTestId('search-slot')).toBeTruthy(); + expect(screen.queryByTestId('filter-slot')).toBeNull(); + expect(screen.queryByTestId('view-switch')).toBeNull(); + }); + + it('keeps search and trailing controls grouped in the right section when compactTrailing is false', () => { + renderWithTheme( + Search} + viewSwitch={Grid/Table} + /> + ); + const searchSlot = screen.getByTestId('search-slot'); + const viewSwitch = screen.getByTestId('view-switch'); + expect(searchSlot.parentElement?.parentElement).toBe(viewSwitch.parentElement?.parentElement); + }); + + it('groups search and trailing controls together on the right', () => { + renderWithTheme( + Add} + search={Search} + viewSwitch={Grid/Table} + /> + ); + const searchSlot = screen.getByTestId('search-slot'); + const viewSwitch = screen.getByTestId('view-switch'); + const controlsGroup = searchSlot.parentElement?.parentElement; + + expect(controlsGroup).toBeTruthy(); + expect(controlsGroup).toBe(viewSwitch.parentElement?.parentElement); + expect(controlsGroup?.parentElement).toBeTruthy(); + expect(window.getComputedStyle(controlsGroup?.parentElement as HTMLElement).marginLeft).toBe( + 'auto' + ); + }); }); }); diff --git a/src/custom/DataTableToolbar/DataTableToolbar.tsx b/src/custom/DataTableToolbar/DataTableToolbar.tsx index 7d0dd1827..512f3d6c7 100644 --- a/src/custom/DataTableToolbar/DataTableToolbar.tsx +++ b/src/custom/DataTableToolbar/DataTableToolbar.tsx @@ -12,6 +12,7 @@ import type { DataTableToolbarProps } from './DataTableToolbar.types'; const ToolbarRoot = styled(Box)(({ theme }) => ({ display: 'flex', alignItems: 'center', + justifyContent: 'space-between', marginBottom: theme.spacing(2), minHeight: theme.spacing(8), padding: theme.spacing(1.5), @@ -20,8 +21,7 @@ const ToolbarRoot = styled(Box)(({ theme }) => ({ boxShadow: theme.shadows[2], [theme.breakpoints.down('sm')]: { - height: 'auto', - flexWrap: 'wrap', + flexWrap: 'nowrap', padding: theme.spacing(1), gap: theme.spacing(1) } @@ -30,13 +30,43 @@ const ToolbarRoot = styled(Box)(({ theme }) => ({ const Section = styled(Box)(({ theme }) => ({ display: 'flex', alignItems: 'center', + gap: theme.spacing(1), + minWidth: 0 +})); + +const RightSection = styled(Section)(({ theme }) => ({ + marginLeft: 'auto', + flexWrap: 'nowrap', + flexShrink: 1, + minWidth: 0, + justifyContent: 'flex-end', + + [theme.breakpoints.down('sm')]: { + paddingLeft: theme.spacing(1) + } +})); + +const RightControlsGroup = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + flexShrink: 1, + minWidth: 0, gap: theme.spacing(1) })); -const RightSection = styled(Section)({ - marginLeft: 'auto' +const SearchSlot = styled(Box)({ + flex: '0 1 auto', + minWidth: 0, + maxWidth: '15rem' }); +const TrailingControls = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + flexShrink: 0, + gap: theme.spacing(1) +})); + export function DataTableToolbar({ primaryActions, secondaryActions, @@ -45,6 +75,7 @@ export function DataTableToolbar({ filter, columnVisibility, viewSwitch, + compactTrailing = false, searchHelperText, tabs, columns, @@ -155,27 +186,38 @@ export function DataTableToolbar({ columnVisibility ); + const trailingControls = compactTrailing ? null : ( + <> + {filter} + {columnControl} + {viewSwitch} + + ); + + const hasTrailingControls = + !compactTrailing && (Boolean(filter) || Boolean(columnControl) || Boolean(viewSwitch)); + const hasLeftContent = Boolean(primaryActions); const hasRightContent = Boolean(bulkOperations) || Boolean(secondaryActions) || - Boolean(filter) || Boolean(search) || - Boolean(columnControl) || - Boolean(viewSwitch); + hasTrailingControls; return ( <> - + {hasLeftContent &&
{primaryActions}
} {hasRightContent && ( {bulkOperations} {secondaryActions} - {search} - {filter} - {columnControl} - {viewSwitch} + {(search || hasTrailingControls) && ( + + {search && {search}} + {hasTrailingControls && {trailingControls}} + + )} )}
diff --git a/src/custom/DataTableToolbar/DataTableToolbar.types.ts b/src/custom/DataTableToolbar/DataTableToolbar.types.ts index 3af2a024a..fc2721ac3 100644 --- a/src/custom/DataTableToolbar/DataTableToolbar.types.ts +++ b/src/custom/DataTableToolbar/DataTableToolbar.types.ts @@ -45,6 +45,13 @@ export interface DataTableToolbarProps { /** Right side: Grid/table view toggle */ viewSwitch?: React.ReactNode; + /** + * When true, hides filter, column visibility, and view switch so an expanded + * search bar keeps the toolbar on one row on narrow viewports. Consumers + * typically tie this to `width < breakpoint && isSearchExpanded`. + */ + compactTrailing?: boolean; + /** Helper text displayed below the search bar (e.g., "Search by name, kind, category") */ searchHelperText?: string; From c78686331346f8bf9e1957f0e909aa907321ad00 Mon Sep 17 00:00:00 2001 From: KhushamBansal Date: Tue, 11 Aug 2026 15:57:07 +0530 Subject: [PATCH 2/2] fix(DataTableToolbar): remove dead guard, add testids, internalize mobile compact logic Signed-off-by: KhushamBansal --- src/__testing__/DataTableToolbar.test.tsx | 80 +++++++++++-------- .../DataTableToolbar/DataTableToolbar.tsx | 23 ++++-- .../DataTableToolbar.types.ts | 7 +- 3 files changed, 67 insertions(+), 43 deletions(-) diff --git a/src/__testing__/DataTableToolbar.test.tsx b/src/__testing__/DataTableToolbar.test.tsx index 9ebdf204b..1778a5391 100644 --- a/src/__testing__/DataTableToolbar.test.tsx +++ b/src/__testing__/DataTableToolbar.test.tsx @@ -27,10 +27,19 @@ jest.mock('@sistent/mui-datatables', () => ({ default: () => null })); +let mockViewportWidth = 1200; + +jest.mock('../custom/Helpers/Dimension', () => ({ + useWindowDimensions: () => ({ width: mockViewportWidth, height: 800 }) +})); + const renderWithTheme = (ui: React.ReactElement) => render({ui}); describe('DataTableToolbar', () => { + beforeEach(() => { + mockViewportWidth = 1200; + }); it('renders primaryActions content', () => { renderWithTheme(Add} />); expect(screen.getByRole('button', { name: 'Add' })).toBeTruthy(); @@ -135,19 +144,13 @@ describe('DataTableToolbar', () => { describe('layout positioning', () => { it('pushes right section to the right when only right content is present', () => { renderWithTheme(Search} />); - const rightContent = screen.getByTestId('right-content'); - const rightSection = rightContent.parentElement?.parentElement?.parentElement as HTMLElement; - // RightSection has marginLeft: auto — check via computed style - expect(rightSection).toBeTruthy(); + const rightSection = screen.getByTestId('data-table-toolbar-right-section'); expect(window.getComputedStyle(rightSection).marginLeft).toBe('auto'); }); it('keeps left content on the left when only left content is present', () => { renderWithTheme(Add} />); - const leftContent = screen.getByTestId('left-content'); - const leftSection = leftContent.parentElement as HTMLElement; - // Default Section has no marginLeft override - expect(leftSection).toBeTruthy(); + const leftSection = screen.getByTestId('data-table-toolbar-left-section'); expect(window.getComputedStyle(leftSection).marginLeft).not.toBe('auto'); }); @@ -158,16 +161,10 @@ describe('DataTableToolbar', () => { search={Search} /> ); - const leftContent = screen.getByTestId('left-btn'); - const rightContent = screen.getByTestId('right-content'); - const leftSection = leftContent.parentElement as HTMLElement; - const rightSection = rightContent.parentElement?.parentElement?.parentElement as HTMLElement; - - expect(leftSection).toBeTruthy(); - expect(rightSection).toBeTruthy(); - // Left section has no auto margin + const leftSection = screen.getByTestId('data-table-toolbar-left-section'); + const rightSection = screen.getByTestId('data-table-toolbar-right-section'); + expect(window.getComputedStyle(leftSection).marginLeft).not.toBe('auto'); - // Right section has auto margin to push it right expect(window.getComputedStyle(rightSection).marginLeft).toBe('auto'); }); @@ -183,38 +180,57 @@ describe('DataTableToolbar', () => { expect(screen.getByTestId('search-slot')).toBeTruthy(); expect(screen.queryByTestId('filter-slot')).toBeNull(); expect(screen.queryByTestId('view-switch')).toBeNull(); + expect(screen.queryByTestId('data-table-toolbar-trailing-controls')).toBeNull(); }); - it('keeps search and trailing controls grouped in the right section when compactTrailing is false', () => { + it('groups search and trailing controls in the right controls group', () => { renderWithTheme( Add} search={Search} viewSwitch={Grid/Table} + compactTrailing={false} /> ); - const searchSlot = screen.getByTestId('search-slot'); - const viewSwitch = screen.getByTestId('view-switch'); - expect(searchSlot.parentElement?.parentElement).toBe(viewSwitch.parentElement?.parentElement); + + const controlsGroup = screen.getByTestId('data-table-toolbar-right-controls'); + const rightSection = screen.getByTestId('data-table-toolbar-right-section'); + + expect(controlsGroup.contains(screen.getByTestId('search-slot'))).toBe(true); + expect(controlsGroup.contains(screen.getByTestId('view-switch'))).toBe(true); + expect(rightSection.contains(controlsGroup)).toBe(true); + expect(window.getComputedStyle(rightSection).marginLeft).toBe('auto'); }); - it('groups search and trailing controls together on the right', () => { + it('auto-hides trailing controls on narrow viewports when compactTrailing is omitted', () => { + mockViewportWidth = 400; + renderWithTheme( Add} search={Search} viewSwitch={Grid/Table} /> ); - const searchSlot = screen.getByTestId('search-slot'); - const viewSwitch = screen.getByTestId('view-switch'); - const controlsGroup = searchSlot.parentElement?.parentElement; - - expect(controlsGroup).toBeTruthy(); - expect(controlsGroup).toBe(viewSwitch.parentElement?.parentElement); - expect(controlsGroup?.parentElement).toBeTruthy(); - expect(window.getComputedStyle(controlsGroup?.parentElement as HTMLElement).marginLeft).toBe( - 'auto' + + expect(screen.getByTestId('search-slot')).toBeTruthy(); + expect(screen.queryByTestId('view-switch')).toBeNull(); + expect(screen.queryByTestId('data-table-toolbar-trailing-controls')).toBeNull(); + }); + + it('keeps trailing controls visible on narrow viewports when compactTrailing is false', () => { + mockViewportWidth = 400; + + renderWithTheme( + Search} + viewSwitch={Grid/Table} + compactTrailing={false} + /> ); + + expect(screen.getByTestId('search-slot')).toBeTruthy(); + expect(screen.getByTestId('view-switch')).toBeTruthy(); + expect(screen.getByTestId('data-table-toolbar-trailing-controls')).toBeTruthy(); }); }); }); diff --git a/src/custom/DataTableToolbar/DataTableToolbar.tsx b/src/custom/DataTableToolbar/DataTableToolbar.tsx index 512f3d6c7..a62e5f780 100644 --- a/src/custom/DataTableToolbar/DataTableToolbar.tsx +++ b/src/custom/DataTableToolbar/DataTableToolbar.tsx @@ -12,7 +12,6 @@ import type { DataTableToolbarProps } from './DataTableToolbar.types'; const ToolbarRoot = styled(Box)(({ theme }) => ({ display: 'flex', alignItems: 'center', - justifyContent: 'space-between', marginBottom: theme.spacing(2), minHeight: theme.spacing(8), padding: theme.spacing(1.5), @@ -75,7 +74,7 @@ export function DataTableToolbar({ filter, columnVisibility, viewSwitch, - compactTrailing = false, + compactTrailing, searchHelperText, tabs, columns, @@ -85,6 +84,9 @@ export function DataTableToolbar({ }: DataTableToolbarProps): JSX.Element { const theme = useTheme(); const { width: viewportWidth } = useWindowDimensions(); + const isNarrowViewport = + viewportWidth > 0 && viewportWidth < theme.breakpoints.values.sm; + const effectiveCompactTrailing = compactTrailing ?? isNarrowViewport; // Compute auto-hide visibility from columns config + viewport width const autoHideVisibility = React.useMemo(() => { @@ -186,7 +188,7 @@ export function DataTableToolbar({ columnVisibility ); - const trailingControls = compactTrailing ? null : ( + const trailingControls = ( <> {filter} {columnControl} @@ -195,7 +197,8 @@ export function DataTableToolbar({ ); const hasTrailingControls = - !compactTrailing && (Boolean(filter) || Boolean(columnControl) || Boolean(viewSwitch)); + !effectiveCompactTrailing && + (Boolean(filter) || Boolean(columnControl) || Boolean(viewSwitch)); const hasLeftContent = Boolean(primaryActions); const hasRightContent = @@ -207,15 +210,19 @@ export function DataTableToolbar({ return ( <> - {hasLeftContent &&
{primaryActions}
} + {hasLeftContent &&
{primaryActions}
} {hasRightContent && ( - + {bulkOperations} {secondaryActions} {(search || hasTrailingControls) && ( - + {search && {search}} - {hasTrailingControls && {trailingControls}} + {hasTrailingControls && ( + + {trailingControls} + + )} )} diff --git a/src/custom/DataTableToolbar/DataTableToolbar.types.ts b/src/custom/DataTableToolbar/DataTableToolbar.types.ts index fc2721ac3..0b2b935fd 100644 --- a/src/custom/DataTableToolbar/DataTableToolbar.types.ts +++ b/src/custom/DataTableToolbar/DataTableToolbar.types.ts @@ -46,9 +46,10 @@ export interface DataTableToolbarProps { viewSwitch?: React.ReactNode; /** - * When true, hides filter, column visibility, and view switch so an expanded - * search bar keeps the toolbar on one row on narrow viewports. Consumers - * typically tie this to `width < breakpoint && isSearchExpanded`. + * Controls visibility of filter, column visibility, and view switch on narrow + * viewports. When omitted, trailing controls auto-hide below the MUI `sm` + * breakpoint. Pass `true` to force-hide, or `false` to keep them visible even + * on narrow viewports (may overflow with single-row layout). */ compactTrailing?: boolean;