Skip to content

Commit b8da646

Browse files
authored
Merge pull request #4620 from Ivy-Interactive/tendril/00546-FixDataTableFilterComponentMobileOverflow
[00546] Fix DataTable filter component mobile overflow
2 parents 9374e82 + f042c7a commit b8da646

3 files changed

Lines changed: 166 additions & 7 deletions

File tree

src/frontend/src/widgets/dataTables/DataTableOption.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
44
import { cn } from "@/lib/utils";
55
import { Densities } from "@/types/density";
66
import { controlHeight, controlSize } from "@/components/ui/density-scale";
7+
import { useCurrentBreakpoint } from "@/hooks/use-breakpoint-context";
78

89
/**
910
* Display modes for DataTableOption
@@ -70,6 +71,12 @@ export const DataTableOption: React.FC<DataTableOptionProps> = ({
7071
}
7172
const containerRef = useRef<HTMLDivElement>(null);
7273

74+
// Constrain the expanded inline width on smaller screens so the filter never
75+
// overflows the viewport horizontally. The interaction stays identical to
76+
// desktop (inline expansion) — only the expanded width adapts.
77+
const breakpoint = useCurrentBreakpoint();
78+
const isCompact = breakpoint === "mobile" || breakpoint === "tablet";
79+
7380
const heightClass = controlHeight[density] || controlHeight.Medium;
7481
const sizeClass = controlSize[density] || controlSize.Medium;
7582
const pxClass =
@@ -139,7 +146,14 @@ export const DataTableOption: React.FC<DataTableOptionProps> = ({
139146
<div
140147
ref={containerRef}
141148
className={cn(
142-
"inline-flex items-center",
149+
// When expanded on compact screens the container spans the full available
150+
// width so the panel can fill the viewport instead of overflowing it.
151+
// min-w-0/max-w-full let the editor inside clamp and scroll horizontally
152+
// rather than forcing the row wider than the screen.
153+
// Collapsed (or on desktop) it shrinks to the button.
154+
isCompact && expanded
155+
? "flex w-full min-w-0 max-w-full items-center"
156+
: "inline-flex items-center",
143157
"rounded-field border border-input bg-transparent shadow-sm",
144158
"dark:border-white/10 dark:bg-white/5",
145159
"focus-within:outline-none focus-within:ring-1 focus-within:ring-ring",
@@ -163,17 +177,20 @@ export const DataTableOption: React.FC<DataTableOptionProps> = ({
163177
{buttonContent}
164178
</button>
165179

166-
{/* Content container - fixed dimensions when expanded */}
180+
{/* Content container - fills remaining width on compact screens, fixed on desktop */}
167181
<div
168182
className={cn(
169183
`border-l ${heightClass}`,
170184
"transition-all duration-300 ease-in-out",
171-
expanded ? "w-[450px] opacity-100 border-input" : "w-0 opacity-0 border-transparent",
185+
expanded
186+
? cn("opacity-100 border-input", isCompact ? "flex-1 min-w-0" : "w-[450px]")
187+
: "w-0 opacity-0 border-transparent",
172188
)}
173189
>
174190
<div
175191
className={cn(
176-
"flex h-full min-h-0 min-w-0 w-[450px] max-w-full items-stretch",
192+
"flex h-full min-h-0 min-w-0 max-w-full items-stretch",
193+
isCompact ? "w-full" : "w-[450px]",
177194
"overflow-hidden rounded-l-none rounded-tr-fields rounded-br-fields",
178195
contentClassName,
179196
)}

src/frontend/src/widgets/dataTables/DataTableWidget.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export const DataTable: React.FC<DataTableWidgetProps> = ({
142142
<TableLayout>
143143
<DataTableHeader className={spacing.mb}>
144144
<div className={`flex flex-wrap items-center w-full ${spacing.gapOuter}`}>
145-
<div className={`flex items-center ${spacing.gapInner}`}>
145+
<div className={`flex min-w-0 flex-1 items-center ${spacing.gapInner}`}>
146146
{finalConfig.allowFiltering && (
147147
<DataTableOption
148148
icon={FilterIcon}
@@ -158,8 +158,9 @@ export const DataTable: React.FC<DataTableWidgetProps> = ({
158158
)}
159159
{slots?.HeaderLeft}
160160
</div>
161-
<div className="flex-1" />
162-
<div className={`flex items-center ${spacing.gapInner}`}>{slots?.HeaderRight}</div>
161+
<div className={`flex shrink-0 items-center ${spacing.gapInner}`}>
162+
{slots?.HeaderRight}
163+
</div>
163164
</div>
164165
</DataTableHeader>
165166

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2+
import React, { act } from "react";
3+
import { createRoot, Root } from "react-dom/client";
4+
import { DataTableOption } from "../DataTableOption";
5+
import { Filter } from "lucide-react";
6+
import * as BreakpointContext from "@/hooks/use-breakpoint-context";
7+
8+
let container: HTMLDivElement;
9+
let root: Root;
10+
11+
function mount(element: React.ReactElement) {
12+
act(() => {
13+
root.render(element);
14+
});
15+
}
16+
17+
beforeEach(() => {
18+
container = document.createElement("div");
19+
document.body.appendChild(container);
20+
root = createRoot(container);
21+
});
22+
23+
afterEach(() => {
24+
act(() => {
25+
root.unmount();
26+
});
27+
container.remove();
28+
vi.restoreAllMocks();
29+
});
30+
31+
describe("DataTableOption responsive behavior", () => {
32+
it("renders inline (not popover) and fills available width on mobile breakpoint", () => {
33+
vi.spyOn(BreakpointContext, "useCurrentBreakpoint").mockReturnValue("mobile");
34+
35+
mount(
36+
<DataTableOption
37+
icon={Filter}
38+
label="Filter"
39+
displayMode="inline"
40+
inlineDirection="right"
41+
defaultExpanded={true}
42+
>
43+
<div data-testid="filter-content">Filter content</div>
44+
</DataTableOption>,
45+
);
46+
47+
const button = container.querySelector("button");
48+
expect(button).toBeTruthy();
49+
expect(button?.textContent).toContain("Filter");
50+
51+
// Behavior stays inline on mobile — the content is rendered inline (not
52+
// hidden behind a popover trigger).
53+
const filterContent = container.querySelector('[data-testid="filter-content"]');
54+
expect(filterContent).toBeTruthy();
55+
56+
// On compact screens the container goes full-width and the expanded panel
57+
// flexes to fill the remaining space rather than using a fixed 450px width.
58+
const containerEl = container.querySelector(".w-full");
59+
expect(containerEl).toBeTruthy();
60+
const flexPanel = container.querySelector(".flex-1");
61+
expect(flexPanel).toBeTruthy();
62+
// No fixed desktop width on mobile.
63+
expect(container.querySelector(".w-\\[450px\\]")).toBeFalsy();
64+
});
65+
66+
it("renders inline with fixed width on desktop breakpoint", () => {
67+
vi.spyOn(BreakpointContext, "useCurrentBreakpoint").mockReturnValue("desktop");
68+
69+
mount(
70+
<DataTableOption
71+
icon={Filter}
72+
label="Filter"
73+
displayMode="inline"
74+
inlineDirection="right"
75+
defaultExpanded={true}
76+
>
77+
<div data-testid="filter-content">Filter content</div>
78+
</DataTableOption>,
79+
);
80+
81+
// On desktop with expanded state, should render inline expansion
82+
const button = container.querySelector("button");
83+
expect(button).toBeTruthy();
84+
85+
// Content should be visible
86+
const filterContent = container.querySelector('[data-testid="filter-content"]');
87+
expect(filterContent).toBeTruthy();
88+
89+
// Check that the expansion container uses desktop width (w-[450px])
90+
const expansionContainer = container.querySelector(".w-\\[450px\\]");
91+
expect(expansionContainer).toBeTruthy();
92+
});
93+
94+
it("renders inline and fills available width on tablet breakpoint", () => {
95+
vi.spyOn(BreakpointContext, "useCurrentBreakpoint").mockReturnValue("tablet");
96+
97+
mount(
98+
<DataTableOption
99+
icon={Filter}
100+
label="Filter"
101+
displayMode="inline"
102+
inlineDirection="right"
103+
defaultExpanded={true}
104+
>
105+
<div data-testid="filter-content">Filter content</div>
106+
</DataTableOption>,
107+
);
108+
109+
// On tablet with expanded state, should render inline expansion
110+
const button = container.querySelector("button");
111+
expect(button).toBeTruthy();
112+
113+
// Content should be visible
114+
const filterContent = container.querySelector('[data-testid="filter-content"]');
115+
expect(filterContent).toBeTruthy();
116+
117+
// Tablet is also treated as compact: full-width container + flexing panel,
118+
// no fixed desktop width.
119+
expect(container.querySelector(".w-full")).toBeTruthy();
120+
expect(container.querySelector(".flex-1")).toBeTruthy();
121+
expect(container.querySelector(".w-\\[450px\\]")).toBeFalsy();
122+
});
123+
124+
it("respects explicit popover displayMode on desktop", () => {
125+
vi.spyOn(BreakpointContext, "useCurrentBreakpoint").mockReturnValue("desktop");
126+
127+
mount(
128+
<DataTableOption icon={Filter} label="Filter" displayMode="popover">
129+
<div data-testid="filter-content">Filter content</div>
130+
</DataTableOption>,
131+
);
132+
133+
// Even on desktop, if displayMode is explicitly "popover", it should render as popover
134+
const button = container.querySelector("button");
135+
expect(button).toBeTruthy();
136+
137+
// The popover content should not be visible initially
138+
const filterContent = container.querySelector('[data-testid="filter-content"]');
139+
expect(filterContent).toBeFalsy();
140+
});
141+
});

0 commit comments

Comments
 (0)