fix(tickets): dropdowns ne sortent plus du viewport en bas d'écran (#3)

Remplace les <select> natifs de la vue Tickets (statut, priorité, tri,
lien, agent assigné/assistant, sprint) par TicketViewportSelect, qui
repositionne intelligemment son overlay selon l'espace disponible dans
la fenêtre au lieu de toujours ouvrir vers le bas.

QA vert : 80/80 (frontend/src/features/tickets), tsc --noEmit propre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 18:29:17 +02:00
parent 454f8ad57d
commit 05402c7544
8 changed files with 416 additions and 157 deletions

View File

@ -353,9 +353,8 @@ describe("TicketsView", () => {
expect(listSpy.mock.calls.at(-1)?.[1]?.sort).toBeUndefined();
// Choose a field ⇒ ascending by default, relayed in the query.
fireEvent.change(screen.getByLabelText("sort tickets by"), {
target: { value: "title" },
});
fireEvent.click(screen.getByLabelText("sort tickets by"));
fireEvent.click(await screen.findByRole("option", { name: "Titre" }));
await waitFor(() =>
expect(listSpy.mock.calls.at(-1)?.[1]?.sort).toEqual({
field: "title",
@ -375,9 +374,8 @@ describe("TicketsView", () => {
);
// Back to « Par défaut » ⇒ `sort` dropped from the query again.
fireEvent.change(screen.getByLabelText("sort tickets by"), {
target: { value: "" },
});
fireEvent.click(screen.getByLabelText("sort tickets by"));
fireEvent.click(await screen.findByRole("option", { name: "Par défaut" }));
await waitFor(() =>
expect(listSpy.mock.calls.at(-1)?.[1]?.sort).toBeUndefined(),
);
@ -390,8 +388,8 @@ describe("TicketsView", () => {
fireEvent.click(await screen.findByText("Editable"));
const dialog = await screen.findByRole("dialog");
const statusSelect = within(dialog).getByLabelText("ticket status");
fireEvent.change(statusSelect, { target: { value: "inProgress" } });
fireEvent.click(within(dialog).getByLabelText("ticket status"));
fireEvent.click(await screen.findByRole("option", { name: "In progress" }));
await waitFor(async () => {
const fresh = await ticket.read(PROJECT_ID, t.ref);
@ -426,9 +424,8 @@ describe("TicketsView", () => {
fireEvent.click(await screen.findByText("Assignable"));
const dialog = await screen.findByRole("dialog");
fireEvent.change(within(dialog).getByLabelText("assign agent"), {
target: { value: known.id },
});
fireEvent.click(within(dialog).getByLabelText("assign agent"));
fireEvent.click(await screen.findByRole("option", { name: "Backend" }));
fireEvent.click(within(dialog).getByText("Assign"));
await waitFor(async () => {
@ -495,9 +492,8 @@ describe("TicketsView", () => {
// Change the priority — an immediate-apply mutation that re-fetches the ticket
// and bumps its version. The unsaved draft must survive.
fireEvent.change(within(dialog).getByLabelText("ticket priority"), {
target: { value: "high" },
});
fireEvent.click(within(dialog).getByLabelText("ticket priority"));
fireEvent.click(await screen.findByRole("option", { name: "High" }));
// The backend applied the priority bump…
await waitFor(async () => {
@ -643,9 +639,8 @@ describe("TicketsView", () => {
expect(within(bucket).getByText("Movable")).toBeTruthy();
// Pick the sprint in the row selector → assign it.
fireEvent.change(screen.getByLabelText(`sprint for ${t.ref}`), {
target: { value: "s1" },
});
fireEvent.click(screen.getByLabelText(`sprint for ${t.ref}`));
fireEvent.click(await screen.findByRole("option", { name: "Sprint One" }));
// The gateway recorded the membership…
await waitFor(async () => {
@ -777,7 +772,8 @@ describe("TicketsView", () => {
);
expect(within(assistant).getByText("Ouvrir la conversation")).toBeTruthy();
fireEvent.change(profileSelect, { target: { value: "qa-assistant" } });
fireEvent.click(profileSelect);
fireEvent.click(await screen.findByRole("option", { name: "QA Assistant" }));
fireEvent.click(within(assistant).getByText("Ouvrir la conversation"));
expect(
@ -837,9 +833,8 @@ describe("TicketsView", () => {
expect(within(detail).queryByLabelText("link target ref")).toBeNull();
// Choose the link kind, then open the picker and select the target ticket.
fireEvent.change(within(detail).getByLabelText("link kind"), {
target: { value: "blocks" },
});
fireEvent.click(within(detail).getByLabelText("link kind"));
fireEvent.click(await screen.findByRole("option", { name: "blocks" }));
fireEvent.click(within(detail).getByLabelText("add link"));
// The picker excludes the ticket itself; the target is offered.