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 && }
+
+ {hasLeftContent && }
{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;