mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
Fix create task mode shortcut on iOS
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
d29a6c895d
commit
2e16d218b1
2 files changed
+82
-4
No files matched your search
@@ -14,6 +14,7 @@ const dialogState = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
const dialogContentState = vi.hoisted(() => ({
|
||||
onEscapeKeyDown: null as null | ((event: KeyboardEvent) => void),
|
||||
onPointerDownOutside: null as null | ((event: {
|
||||
detail: { originalEvent: { target: EventTarget | null } };
|
||||
preventDefault: () => void;
|
||||
@@ -193,14 +194,15 @@ vi.mock("@/components/ui/dialog", () => ({
|
||||
DialogContent: ({
|
||||
children,
|
||||
showCloseButton: _showCloseButton,
|
||||
onEscapeKeyDown: _onEscapeKeyDown,
|
||||
onEscapeKeyDown,
|
||||
onPointerDownOutside,
|
||||
...props
|
||||
}: ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean;
|
||||
onEscapeKeyDown?: (event: unknown) => void;
|
||||
onEscapeKeyDown?: (event: KeyboardEvent) => void;
|
||||
onPointerDownOutside?: (event: unknown) => void;
|
||||
}) => {
|
||||
dialogContentState.onEscapeKeyDown = onEscapeKeyDown ?? null;
|
||||
dialogContentState.onPointerDownOutside = onPointerDownOutside as typeof dialogContentState.onPointerDownOutside;
|
||||
return <div {...props}>{children}</div>;
|
||||
},
|
||||
@@ -315,6 +317,7 @@ describe("NewIssueDialog", () => {
|
||||
dialogState.newIssueOpen = true;
|
||||
dialogState.newIssueDefaults = {};
|
||||
dialogState.closeNewIssue.mockReset();
|
||||
dialogContentState.onEscapeKeyDown = null;
|
||||
dialogContentState.onPointerDownOutside = null;
|
||||
toastState.pushToast.mockReset();
|
||||
mockIssuesApi.create.mockReset();
|
||||
@@ -849,7 +852,7 @@ describe("NewIssueDialog", () => {
|
||||
await act(async () => {
|
||||
modeChip()?.dispatchEvent(new KeyboardEvent("keydown", {
|
||||
bubbles: true,
|
||||
code: "Period",
|
||||
code: "",
|
||||
key: ".",
|
||||
metaKey: true,
|
||||
}));
|
||||
@@ -879,6 +882,63 @@ describe("NewIssueDialog", () => {
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("cycles work modes when iOS reports cmd-period as Escape", async () => {
|
||||
const { root } = renderDialog(container);
|
||||
await flush();
|
||||
|
||||
const modeChip = () => container.querySelector("[data-issue-work-mode-chip]");
|
||||
expect(modeChip()?.getAttribute("data-issue-work-mode-chip")).toBe("standard");
|
||||
expect(dialogContentState.onEscapeKeyDown).not.toBeNull();
|
||||
|
||||
const commandPeriodAsEscape = new KeyboardEvent("keydown", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
key: "Escape",
|
||||
metaKey: true,
|
||||
});
|
||||
await act(async () => {
|
||||
dialogContentState.onEscapeKeyDown?.(commandPeriodAsEscape);
|
||||
});
|
||||
|
||||
expect(commandPeriodAsEscape.defaultPrevented).toBe(true);
|
||||
expect(modeChip()?.getAttribute("data-issue-work-mode-chip")).toBe("planning");
|
||||
expect(dialogState.closeNewIssue).not.toHaveBeenCalled();
|
||||
|
||||
const plainEscape = new KeyboardEvent("keydown", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
key: "Escape",
|
||||
});
|
||||
await act(async () => {
|
||||
dialogContentState.onEscapeKeyDown?.(plainEscape);
|
||||
});
|
||||
|
||||
expect(plainEscape.defaultPrevented).toBe(false);
|
||||
expect(modeChip()?.getAttribute("data-issue-work-mode-chip")).toBe("planning");
|
||||
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("cycles work modes with ctrl-period", async () => {
|
||||
const { root } = renderDialog(container);
|
||||
await flush();
|
||||
|
||||
const modeChip = () => container.querySelector("[data-issue-work-mode-chip]");
|
||||
expect(modeChip()?.getAttribute("data-issue-work-mode-chip")).toBe("standard");
|
||||
|
||||
await act(async () => {
|
||||
modeChip()?.dispatchEvent(new KeyboardEvent("keydown", {
|
||||
bubbles: true,
|
||||
code: "Period",
|
||||
key: ".",
|
||||
ctrlKey: true,
|
||||
}));
|
||||
});
|
||||
expect(modeChip()?.getAttribute("data-issue-work-mode-chip")).toBe("planning");
|
||||
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("submits the parent assignee when a sub-issue opens with inherited defaults", async () => {
|
||||
dialogState.newIssueDefaults = {
|
||||
parentId: "issue-1",
|
||||
|
||||
@@ -268,6 +268,15 @@ function defaultExecutionWorkspaceModeForIssueDefaults(
|
||||
: defaultExecutionWorkspaceModeForProject(project);
|
||||
}
|
||||
|
||||
function isWorkModePeriodShortcut(e: Pick<React.KeyboardEvent, "code" | "ctrlKey" | "key" | "metaKey">) {
|
||||
const isPeriod = e.code === "Period" || e.key === ".";
|
||||
return (e.metaKey || e.ctrlKey) && isPeriod;
|
||||
}
|
||||
|
||||
function isWorkModeEscapeShortcut(e: Pick<KeyboardEvent, "ctrlKey" | "key" | "metaKey">) {
|
||||
return (e.metaKey || e.ctrlKey) && e.key === "Escape";
|
||||
}
|
||||
|
||||
const IssueTitleTextarea = memo(function IssueTitleTextarea({
|
||||
value,
|
||||
pending,
|
||||
@@ -1006,7 +1015,7 @@ export function NewIssueDialog() {
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.code === "Period") {
|
||||
if (isWorkModePeriodShortcut(e)) {
|
||||
e.preventDefault();
|
||||
setWorkMode((current) => nextWorkMode(current));
|
||||
return;
|
||||
@@ -1236,6 +1245,15 @@ export function NewIssueDialog() {
|
||||
)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onEscapeKeyDown={(event) => {
|
||||
if (event.defaultPrevented) return;
|
||||
// iOS Safari maps command-period to Escape for hardware keyboards.
|
||||
// Treat modifier-Escape as the same mode-cycle shortcut so the
|
||||
// dialog does not dismiss before the shortcut can run.
|
||||
if (isWorkModeEscapeShortcut(event)) {
|
||||
event.preventDefault();
|
||||
setWorkMode((current) => nextWorkMode(current));
|
||||
return;
|
||||
}
|
||||
if (createIssue.isPending) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user