diff --git a/src/__testing__/DataTableToolbar.test.tsx b/src/__testing__/DataTableToolbar.test.tsx index 3f504865c..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 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,17 +161,76 @@ 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 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'); }); + + 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(); + expect(screen.queryByTestId('data-table-toolbar-trailing-controls')).toBeNull(); + }); + + it('groups search and trailing controls in the right controls group', () => { + renderWithTheme( + Add} + search={Search} + viewSwitch={Grid/Table} + compactTrailing={false} + /> + ); + + 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('auto-hides trailing controls on narrow viewports when compactTrailing is omitted', () => { + mockViewportWidth = 400; + + renderWithTheme( + Search} + viewSwitch={Grid/Table} + /> + ); + + 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 7d0dd1827..a62e5f780 100644 --- a/src/custom/DataTableToolbar/DataTableToolbar.tsx +++ b/src/custom/DataTableToolbar/DataTableToolbar.tsx @@ -20,8 +20,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 +29,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 +74,7 @@ export function DataTableToolbar({ filter, columnVisibility, viewSwitch, + compactTrailing, searchHelperText, tabs, columns, @@ -54,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(() => { @@ -155,27 +188,43 @@ export function DataTableToolbar({ columnVisibility ); + const trailingControls = ( + <> + {filter} + {columnControl} + {viewSwitch} + + ); + + const hasTrailingControls = + !effectiveCompactTrailing && + (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}
} + + {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..0b2b935fd 100644 --- a/src/custom/DataTableToolbar/DataTableToolbar.types.ts +++ b/src/custom/DataTableToolbar/DataTableToolbar.types.ts @@ -45,6 +45,14 @@ export interface DataTableToolbarProps { /** Right side: Grid/table view toggle */ viewSwitch?: React.ReactNode; + /** + * 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; + /** Helper text displayed below the search bar (e.g., "Search by name, kind, category") */ searchHelperText?: string;