FN-8212: remove model dropdown sticky-header gap
Make pinned model provider headers sit flush against the fixed dropdown header stack. - Remove top padding that exposed scrolling rows above sticky provider headers. - Cover the no-seam invariant across desktop and mobile layouts with and without thinking controls. - Add a patch changeset for the dropdown scrolling fix. Files changed: .changeset/fn-8212-model-dropdown-sticky-gap.md | 7 ++++ .../app/components/CustomModelDropdown.css | 5 ++- .../__tests__/CustomModelDropdown.test.tsx | 46 ++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-8212 Fusion-Task-Lineage: d7ce42d1-bcbe-4000-8107-794ec8e05307 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8212-model-dropdown-sticky-gap.md
Normal file
7
.changeset/fn-8212-model-dropdown-sticky-gap.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Remove the gap above the pinned provider header in model dropdowns so list rows no longer show through while scrolling.
|
||||
category: fix
|
||||
dev: CustomModelDropdown.css — zero the .model-combobox-list top padding so the sticky .model-combobox-optgroup provider header sits flush against the header stack (FN-8212 refinement of FN-8193).
|
||||
@@ -228,6 +228,9 @@ The Thinking Level <select> renders inside the createPortal dropdown (.model-com
|
||||
/*
|
||||
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.
|
||||
|
||||
FNXC:ModelDropdown 2026-07-15-00:00:
|
||||
FN-8212 refines FN-8193 by removing the scroller's top padding. A sticky provider header anchors at the list top, so top padding exposed scrolling model rows in a seam between the fixed header stack and that header. Bottom token spacing preserves the list's trailing rhythm.
|
||||
*/
|
||||
.model-combobox-list {
|
||||
flex: 1;
|
||||
@@ -236,7 +239,7 @@ FN-7760 requires the portaled model list to remain the touch-scroll owner on mob
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
touch-action: pan-y;
|
||||
padding: 4px 0;
|
||||
padding: 0 0 var(--space-xs);
|
||||
}
|
||||
|
||||
.model-combobox-option {
|
||||
|
||||
@@ -65,6 +65,52 @@ describe("CustomModelDropdown", () => {
|
||||
expect(within(list!).getByText("openai").closest(".model-combobox-optgroup")).not.toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ width: 1024, query: "desktop", showThinking: false },
|
||||
{ width: 1024, query: "desktop", showThinking: true },
|
||||
{ width: 768, query: "(max-width: 768px)", showThinking: false },
|
||||
{ width: 768, query: "(max-width: 768px)", showThinking: true },
|
||||
{ width: 640, query: "(max-width: 640px)", showThinking: false },
|
||||
{ width: 640, query: "(max-width: 640px)", showThinking: true },
|
||||
])("keeps the provider header flush with the fixed header stack at $query with thinking $showThinking", async ({ width, showThinking }) => {
|
||||
const user = userEvent.setup();
|
||||
const css = readFileSync(resolve(__dirname, "../CustomModelDropdown.css"), "utf-8");
|
||||
const listRules = css.match(/\.model-combobox-list\s*\{[^}]*\}/g) ?? [];
|
||||
|
||||
vi.spyOn(window, "innerWidth", "get").mockReturnValue(width);
|
||||
render(
|
||||
<CustomModelDropdown
|
||||
label="Executor Model"
|
||||
value=""
|
||||
onChange={vi.fn()}
|
||||
models={COLLAPSIBLE_MODELS}
|
||||
favoriteModels={["anthropic/claude-haiku"]}
|
||||
thinkingLevel={showThinking ? "high" : undefined}
|
||||
onThinkingLevelChange={showThinking ? vi.fn() : undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Executor Model" }));
|
||||
|
||||
const portal = await screen.findByTestId("model-combobox-portal");
|
||||
const list = portal.querySelector(".model-combobox-list");
|
||||
const firstProviderGroup = Array.from(list?.children ?? []).find((child) =>
|
||||
child.classList.contains("model-combobox-group"),
|
||||
);
|
||||
|
||||
// JSDOM cannot scroll sticky elements; the zero top inset is the structural no-seam invariant.
|
||||
expect(listRules).toHaveLength(1);
|
||||
expect(listRules[0]).toContain("padding: 0 0 var(--space-xs);");
|
||||
expect(listRules[0]).not.toMatch(/padding-top\s*:\s*(?!0[;}])/);
|
||||
expect(firstProviderGroup?.querySelector(".model-combobox-optgroup")).not.toBeNull();
|
||||
expect(firstProviderGroup?.parentElement).toBe(list);
|
||||
if (showThinking) {
|
||||
expect(portal.querySelector(".model-combobox-thinking")).not.toBeNull();
|
||||
} else {
|
||||
expect(portal.querySelector(".model-combobox-thinking")).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("collapses provider rows, preserves special rows, and persists the preference", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user