diff --git a/ui/src/components/NewIssueDialog.test.tsx b/ui/src/components/NewIssueDialog.test.tsx index 67ccb108ffa..9eb155cf017 100644 --- a/ui/src/components/NewIssueDialog.test.tsx +++ b/ui/src/components/NewIssueDialog.test.tsx @@ -1313,9 +1313,8 @@ describe("NewIssueDialog", () => { }); expect(dialogContent?.style.getPropertyValue("--new-issue-visual-viewport-height")).toBe("420px"); - expect(dialogContent?.style.getPropertyValue("--new-issue-visual-viewport-offset-top")).toBe("24px"); expect(dialogContent?.style.getPropertyValue("--new-issue-dialog-top")).toBe( - "calc(var(--new-issue-visual-viewport-offset-top) + var(--new-issue-dialog-top-gap))", + "var(--new-issue-dialog-top-gap)", ); expect(dialogContent?.style.getPropertyValue("--new-issue-dialog-height")).toBe( "calc(var(--new-issue-visual-viewport-height) - var(--new-issue-dialog-top-gap) - var(--new-issue-dialog-bottom-gap))", @@ -1372,7 +1371,6 @@ describe("NewIssueDialog", () => { }); expect(dialogContent?.style.getPropertyValue("--new-issue-visual-viewport-height")).toBe("420px"); - expect(dialogContent?.style.getPropertyValue("--new-issue-visual-viewport-offset-top")).toBe("24px"); expect(dialogContent?.style.top).toBe("var(--new-issue-dialog-top)"); expect(dialogContent?.style.height).toBe("var(--new-issue-dialog-height)"); diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index 51dacb2a3b5..dfd61750bf7 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -96,14 +96,12 @@ type VisualViewportLayout = { type NewIssueDialogViewportStyle = CSSProperties & { "--new-issue-visual-viewport-height"?: string; - "--new-issue-visual-viewport-offset-top"?: string; "--new-issue-dialog-top"?: string; "--new-issue-dialog-height"?: string; }; type MobileEntityPickerViewportStyle = CSSProperties & { "--mobile-entity-picker-visual-viewport-height"?: string; - "--mobile-entity-picker-visual-viewport-bottom"?: string; }; function readVisualViewportLayout(): VisualViewportLayout | null { @@ -1327,8 +1325,10 @@ export function NewIssueDialog() { const CurrentWorkModeIcon = currentWorkMode.icon; const dialogViewportStyle = useMemo(() => { const dialogGeometry = { - "--new-issue-dialog-top": - "calc(var(--new-issue-visual-viewport-offset-top) + var(--new-issue-dialog-top-gap))", + // Fixed-position coordinates are relative to Safari's visual viewport. + // Adding visualViewport.offsetTop here places the dialog below that + // viewport after the software keyboard pans the page. + "--new-issue-dialog-top": "var(--new-issue-dialog-top-gap)", "--new-issue-dialog-height": "calc(var(--new-issue-visual-viewport-height) - var(--new-issue-dialog-top-gap) - var(--new-issue-dialog-bottom-gap))", }; @@ -1336,7 +1336,6 @@ export function NewIssueDialog() { return { ...dialogGeometry, "--new-issue-visual-viewport-height": `${visualViewportLayout.height}px`, - "--new-issue-visual-viewport-offset-top": `${visualViewportLayout.offsetTop}px`, ...(visualViewportLayout.constrained ? { top: "var(--new-issue-dialog-top)", @@ -1350,7 +1349,6 @@ export function NewIssueDialog() { if (!visualViewportLayout) return {}; return { "--mobile-entity-picker-visual-viewport-height": `${visualViewportLayout.height}px`, - "--mobile-entity-picker-visual-viewport-bottom": `${visualViewportLayout.offsetTop + visualViewportLayout.height}px`, }; }, [visualViewportLayout]); diff --git a/ui/src/index.css b/ui/src/index.css index d7d33d54471..f1b89a779be 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -231,7 +231,6 @@ --new-issue-dialog-top-gap: max(1rem, env(safe-area-inset-top)); --new-issue-dialog-bottom-gap: max(1rem, env(safe-area-inset-bottom)); --mobile-entity-picker-visual-viewport-height: 100dvh; - --mobile-entity-picker-visual-viewport-bottom: 100dvh; /* ──────────────────────────────────────────────────────────────────────── Motion tokens — chat-style task thread (default; classic legacy view sits @@ -3008,20 +3007,26 @@ span.paperclip-mention-chip[data-mention-kind="external-object"] { viewport resizing. */ @media (max-width: 40rem) { [data-radix-popper-content-wrapper]:has(> [data-mobile-entity-picker]) { + inset: 0 !important; + width: 100% !important; + height: 100% !important; + min-width: 0 !important; transform: none !important; + will-change: auto !important; + pointer-events: none; } [data-mobile-entity-picker] { position: fixed !important; - inset: calc(var(--mobile-entity-picker-visual-viewport-bottom) - var(--spacing) * 4) - calc(var(--spacing) * 4) auto !important; + inset: auto calc(var(--spacing) * 4) calc(var(--spacing) * 4) !important; width: auto !important; max-width: none !important; max-height: calc(var(--mobile-entity-picker-visual-viewport-height) - var(--spacing) * 8) !important; transform: none !important; - translate: 0 -100% !important; + translate: none !important; transform-origin: bottom center !important; animation: none !important; + pointer-events: auto; } [data-mobile-entity-picker] [data-slot="command-list"], diff --git a/ui/storybook/stories/dialogs-modals.stories.tsx b/ui/storybook/stories/dialogs-modals.stories.tsx index ba3e5bb040b..74e1221aa7c 100644 --- a/ui/storybook/stories/dialogs-modals.stories.tsx +++ b/ui/storybook/stories/dialogs-modals.stories.tsx @@ -1,5 +1,6 @@ import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from "react"; import type { Meta, StoryObj } from "@storybook/react-vite"; +import { userEvent, within } from "storybook/test"; import type { DocumentRevision, ExecutionWorkspaceCloseReadiness, @@ -716,6 +717,48 @@ export const NewIssuePrefilled: Story = { ), }; +export const NewIssueMobileAssigneePicker: Story = { + name: "New Issue - Mobile Assignee Picker", + parameters: { + viewport: { defaultViewport: "iphone14" }, + }, + render: () => ( + + + + ), + play: async () => { + const page = within(document.body); + await userEvent.click(await page.findByRole("button", { name: "Assignee" })); + }, +}; + +export const NewIssueMobileProjectPicker: Story = { + name: "New Issue - Mobile Project Picker", + parameters: { + viewport: { defaultViewport: "iphone14" }, + }, + render: () => ( + + + + ), + play: async () => { + const page = within(document.body); + await userEvent.click(await page.findByRole("button", { name: "Project" })); + }, +}; + export const NewIssueValidationError: Story = { name: "New Issue - Validation Error", render: () => (