fix(ui): improve mobile entity picker sheets (#13343)

Co-Authored-By: Paperclip CodexRunner <codexrunner@paperclip.local>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-12 17:42:37 -05:00 committed by GitHub
parent df984cbc2c
commit 8b84ae5b35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 84 additions and 1 deletions

View File

@ -129,6 +129,8 @@ describe("InlineEntitySelector", () => {
const searchInput = document.querySelector('input[placeholder="Search responsible..."]') as HTMLInputElement | null;
expect(searchInput).not.toBeNull();
expect(searchInput?.className).toContain("text-base");
expect(document.querySelector("[data-mobile-entity-picker]")).not.toBeNull();
expect(document.activeElement).toBe(searchInput);
act(() => {

View File

@ -142,6 +142,7 @@ export const InlineEntitySelector = forwardRef<HTMLButtonElement, InlineEntitySe
</button>
</PopoverTrigger>
<PopoverContent
data-mobile-entity-picker=""
align="start"
side="bottom"
collisionPadding={16}
@ -159,7 +160,7 @@ export const InlineEntitySelector = forwardRef<HTMLButtonElement, InlineEntitySe
>
<input
ref={inputRef}
className="w-full border-b border-border bg-transparent px-2 py-1.5 text-sm outline-none placeholder:text-muted-foreground/60"
className="w-full border-b border-border bg-transparent px-2 py-1.5 text-base outline-none placeholder:text-muted-foreground/60 md:text-sm"
placeholder={searchPlaceholder}
value={query}
onChange={(event) => {

View File

@ -130,6 +130,7 @@ describe("SearchableSelect", () => {
expect(container.querySelector("[data-option-key='recent:alpha']")).not.toBeNull();
expect(container.querySelector("[data-option-key='all:alpha']")).not.toBeNull();
expect(container.querySelector("[data-mobile-entity-picker]")).not.toBeNull();
});
it("filters options and returns the selected option object", async () => {

View File

@ -225,6 +225,7 @@ export function SearchableSelect<
</Button>
</PopoverTrigger>
<PopoverContent
data-mobile-entity-picker=""
align={align}
collisionPadding={16}
disablePortal={disablePortal}

View File

@ -2999,3 +2999,23 @@ span.paperclip-mention-chip[data-mention-kind="external-object"] {
.runner-activity-roll-in { animation: none; }
.runner-activity-roll-out { display: none; }
}
/* Keep entity-picker search controls visible when a narrow viewport opens its
software keyboard. The fixed sheet also avoids scale motion during mobile
viewport resizing. */
@media (max-width: 40rem) {
[data-mobile-entity-picker] {
position: fixed !important;
inset: auto calc(var(--spacing) * 4) max(calc(var(--spacing) * 4), env(safe-area-inset-bottom)) !important;
width: auto !important;
max-width: none !important;
max-height: calc(100dvh - calc(var(--spacing) * 8)) !important;
transform: none !important;
transform-origin: bottom center !important;
animation: none !important;
}
[data-mobile-entity-picker] [data-slot="command-list"] {
max-height: min(var(--sz-300px), calc(100dvh - calc(var(--spacing) * 24))) !important;
}
}

View File

@ -0,0 +1,58 @@
import { useEffect, useRef, useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { InlineEntitySelector, type InlineEntityOption } from "@/components/InlineEntitySelector";
const assignees: InlineEntityOption[] = [
{ id: "agent-product", label: "Product Lead", searchText: "planning product" },
{ id: "agent-engineer", label: "Frontend Engineer", searchText: "ui implementation" },
{ id: "agent-qa", label: "QA Engineer", searchText: "testing review" },
];
const projects: InlineEntityOption[] = [
{ id: "project-control-plane", label: "Control Plane" },
{ id: "project-mobile", label: "Mobile Experience" },
{ id: "project-connectors", label: "Apps and Connectors" },
];
function OpenPicker({ kind, options }: { kind: "Assignee" | "Project"; options: InlineEntityOption[] }) {
const triggerRef = useRef<HTMLButtonElement>(null);
const [value, setValue] = useState("");
useEffect(() => {
triggerRef.current?.click();
}, []);
return (
<div className="flex min-h-screen items-end p-4">
<InlineEntitySelector
ref={triggerRef}
value={value}
options={options}
placeholder={`Choose ${kind.toLowerCase()}`}
noneLabel={`No ${kind.toLowerCase()}`}
searchPlaceholder={`Search ${kind.toLowerCase()}s...`}
emptyMessage={`No matching ${kind.toLowerCase()}.`}
onChange={setValue}
/>
</div>
);
}
const meta = {
title: "Components/Entity pickers/Mobile",
parameters: {
layout: "fullscreen",
viewport: { defaultViewport: "mobile1" },
},
} satisfies Meta;
export default meta;
type Story = StoryObj<typeof meta>;
export const AssigneePicker: Story = {
render: () => <OpenPicker kind="Assignee" options={assignees} />,
};
export const ProjectPicker: Story = {
render: () => <OpenPicker kind="Project" options={projects} />,
};