FN-7760: fix mobile model dropdown list not scrolling via touch
Adds an explicit touch-scroll contract to the portaled model combobox list so it remains scrollable on mobile despite the global mobile lockdown that hides body/root overflow and defaults gestures to vertical panning. - Add -webkit-overflow-scrolling: touch, overscroll-behavior: contain, and touch-action: pan-y to .model-combobox-list so the fixed-position dropdown owns its own touch scrolling on mobile. - Add regression tests covering the mobile scroll contract for CustomModelDropdown. - Add a patch changeset documenting the fix for release notes. Files changed: .changeset/fn-7760-mobile-model-dropdown-scroll.md | 7 +++ packages/dashboard/app/components/CustomModelDropdown.css | 7 +++ packages/dashboard/app/components/__tests__/CustomModelDropdown.test.tsx | 70 ++++++++++++++++++++++ 3 files changed, 84 insertions(+) Fusion-Task-Id: FN-7760 Fusion-Task-Lineage: e135ff47-275b-4606-aef2-c9859aeaa418 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7760-mobile-model-dropdown-scroll.md
Normal file
7
.changeset/fn-7760-mobile-model-dropdown-scroll.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Fix mobile model drop-down lists so they scroll by touch.
|
||||||
|
category: fix
|
||||||
|
dev: Adds the CustomModelDropdown portaled list mobile scroll contract.
|
||||||
@@ -130,10 +130,17 @@
|
|||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:ModelDropdown 2026-07-09-00:00:
|
||||||
|
FN-7760 requires the portaled model list to remain the touch-scroll owner on mobile. The global mobile lockdown keeps body/root overflow hidden and defaults touch gestures to vertical panning, so this fixed-position scroller needs its own iOS momentum scrolling, contained overscroll, and explicit vertical pan contract.
|
||||||
|
*/
|
||||||
.model-combobox-list {
|
.model-combobox-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
touch-action: pan-y;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,76 @@ describe("CustomModelDropdown", () => {
|
|||||||
expect(hostSurface.contains(portal)).toBe(false);
|
expect(hostSurface.contains(portal)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ width: 390, height: 844, query: "(max-width: 640px)", expectedMaxHeight: "360px" },
|
||||||
|
{ width: 700, height: 900, query: "(max-width: 768px)", expectedMaxHeight: "420px" },
|
||||||
|
])("keeps the portaled model list touch-scrollable at $query", async ({ width, height, query, expectedMaxHeight }) => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onChange = vi.fn();
|
||||||
|
const css = readFileSync(resolve(__dirname, "../CustomModelDropdown.css"), "utf-8");
|
||||||
|
const listRule = css.match(/\.model-combobox-list\s*\{[^}]*\}/)?.[0] ?? "";
|
||||||
|
const overflowingModels = Array.from({ length: 30 }, (_, index) => ({
|
||||||
|
provider: index % 2 === 0 ? "openai" : "anthropic",
|
||||||
|
id: `model-${index}`,
|
||||||
|
name: `Model ${index}`,
|
||||||
|
reasoning: index % 3 === 0,
|
||||||
|
contextWindow: 128000 + index,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.spyOn(window, "innerWidth", "get").mockReturnValue(width);
|
||||||
|
vi.spyOn(window, "innerHeight", "get").mockReturnValue(height);
|
||||||
|
vi.spyOn(window, "matchMedia").mockImplementation((mediaQuery: string) => ({
|
||||||
|
matches: mediaQuery === query || (width <= 640 && mediaQuery === "(max-width: 768px)"),
|
||||||
|
media: mediaQuery,
|
||||||
|
onchange: null,
|
||||||
|
addListener: vi.fn(),
|
||||||
|
removeListener: vi.fn(),
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
} as MediaQueryList));
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CustomModelDropdown
|
||||||
|
label="Executor Model"
|
||||||
|
value=""
|
||||||
|
onChange={onChange}
|
||||||
|
models={overflowingModels}
|
||||||
|
favoriteModels={["openai/model-0", "anthropic/model-1"]}
|
||||||
|
onToggleModelFavorite={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const trigger = screen.getByRole("button", { name: "Executor Model" });
|
||||||
|
vi.spyOn(trigger, "getBoundingClientRect").mockReturnValue({
|
||||||
|
x: 24,
|
||||||
|
y: 80,
|
||||||
|
width: 320,
|
||||||
|
height: 36,
|
||||||
|
top: 80,
|
||||||
|
right: 344,
|
||||||
|
bottom: 116,
|
||||||
|
left: 24,
|
||||||
|
toJSON: () => ({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.click(trigger);
|
||||||
|
|
||||||
|
const portal = await screen.findByTestId("model-combobox-portal");
|
||||||
|
const list = portal.querySelector(".model-combobox-list");
|
||||||
|
|
||||||
|
expect(portal.classList.contains("model-combobox-dropdown--portal")).toBe(true);
|
||||||
|
expect(portal.style.maxHeight).toBe(expectedMaxHeight);
|
||||||
|
expect(list).toBeInstanceOf(HTMLElement);
|
||||||
|
expect(within(portal).getAllByRole("option").length).toBeGreaterThan(20);
|
||||||
|
expect(listRule).toContain("overflow-y: auto;");
|
||||||
|
expect(listRule).toContain("overflow-x: hidden;");
|
||||||
|
expect(listRule).toContain("-webkit-overflow-scrolling: touch;");
|
||||||
|
expect(listRule).toContain("overscroll-behavior: contain;");
|
||||||
|
expect(listRule).toContain("touch-action: pan-y;");
|
||||||
|
expect(listRule).not.toContain("touch-action: none");
|
||||||
|
});
|
||||||
|
|
||||||
it("supports an explicit No change sentinel while keeping Use default available", async () => {
|
it("supports an explicit No change sentinel while keeping Use default available", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onChange = vi.fn();
|
const onChange = vi.fn();
|
||||||
|
|||||||
Reference in New Issue
Block a user