Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c8f3abd
recent
aidenybai Feb 8, 2026
bf036ec
fix: dismiss recent dropdown on overlay deactivation
cursoragent Feb 8, 2026
8a2467a
Add recent tray keyboard controls
ben-million Feb 8, 2026
39cc6a7
Improve recents hover state
ben-million Feb 8, 2026
7532bf1
fix: add fallback to copy content for disconnected comment items
cursoragent Feb 8, 2026
b444015
Fix recent hover radius
ben-million Feb 8, 2026
0a29030
Prevent opening empty recent menu
ben-million Feb 8, 2026
0e15134
Fix recent menu radius and clear
ben-million Feb 8, 2026
9010795
Add hugeicons copy and trash icons
ben-million Feb 8, 2026
7a60ff1
Refine recent dropdown hover styles
ben-million Feb 8, 2026
a99a6d8
Adjust toolbar icon styling
ben-million Feb 8, 2026
8b26af2
Update toolbar icon active color
ben-million Feb 8, 2026
03576e1
Update comment icon states
ben-million Feb 8, 2026
48adabf
Fix toolbar unread icon state to use hasUnreadRecentItems instead of …
cursoragent Feb 8, 2026
3f1adc3
Fix toolbar freeze state not checking isHistoryOpen prop
cursoragent Feb 8, 2026
53333ef
Fix IconInboxUnread visibility on dark toolbar by using currentColor
cursoragent Feb 8, 2026
ed15ab5
Refine draggable toolbar visuals
ben-million Feb 8, 2026
5b86587
Fix toolbar drag animation issues
ben-million Feb 8, 2026
db2314b
Fix unread indicator selector, font family name, and remove unused co…
cursoragent Feb 8, 2026
26be2b6
refactor: extract shared edge distance calculation into helper function
cursoragent Feb 8, 2026
1f4eb6c
Increase animation prominence
ben-million Feb 8, 2026
0e8e3b3
Adjust toolbar icon states
ben-million Feb 8, 2026
193a04f
fix: remove unused TOOLBAR_VELOCITY_MULTIPLIER_MS and clear snap time…
cursoragent Feb 8, 2026
c20d51a
Adjust toolbar toggle animation
ben-million Feb 8, 2026
a0d82d5
Fix toolbar toggle drift
ben-million Feb 8, 2026
b951ff3
fix: remove transition-transform to prevent twMerge from dropping tra…
cursoragent Feb 8, 2026
901bc6e
Fix: Reset isSnapping state when clearing snapAnimationTimeout
cursoragent Feb 8, 2026
25d7ab8
Fix toolbar toggle shifting
ben-million Feb 8, 2026
730303a
color changes
ben-million Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions packages/react-grab/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface ToolbarInfo {
isCollapsed: boolean;
position: { x: number; y: number } | null;
snapEdge: string | null;
orientation: "horizontal" | "vertical" | null;
}

interface AgentSessionInfo {
Expand Down Expand Up @@ -135,7 +136,6 @@ export interface ReactGrabPageObject {
getRecentDropdownInfo: () => Promise<RecentDropdownInfo>;
clickRecentItem: (index: number) => Promise<void>;
clickRecentCopyAll: () => Promise<void>;
clickRecentClear: () => Promise<void>;
hoverRecentItem: (index: number) => Promise<void>;

getSelectionLabelInfo: () => Promise<SelectionLabelInfo>;
Expand Down Expand Up @@ -699,6 +699,7 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => {
isCollapsed: false,
position: null,
snapEdge: null,
orientation: null,
};
const root = shadowRoot.querySelector(`[${attrName}]`);
if (!root)
Expand All @@ -707,6 +708,7 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => {
isCollapsed: false,
position: null,
snapEdge: null,
orientation: null,
};

const toolbar = root.querySelector<HTMLElement>(
Expand All @@ -718,6 +720,7 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => {
isCollapsed: false,
position: null,
snapEdge: null,
orientation: null,
};

const computedStyle = window.getComputedStyle(toolbar);
Expand All @@ -734,7 +737,31 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => {
const rect = toolbar.getBoundingClientRect();

let snapEdge: string | null = null;
if (position) {
const serializedToolbarState = localStorage.getItem(
"react-grab-toolbar-state",
);
if (serializedToolbarState) {
try {
const parsedToolbarState = JSON.parse(serializedToolbarState);
if (
parsedToolbarState &&
typeof parsedToolbarState === "object" &&
"edge" in parsedToolbarState
) {
const edgeValue = parsedToolbarState.edge;
if (
edgeValue === "top" ||
edgeValue === "bottom" ||
edgeValue === "left" ||
edgeValue === "right"
) {
snapEdge = edgeValue;
}
}
} catch {}
}

if (!snapEdge && position) {
const SNAP_THRESHOLD = 30;
if (position.y <= SNAP_THRESHOLD) snapEdge = "top";
else if (position.y + rect.height >= viewportHeight - SNAP_THRESHOLD)
Expand All @@ -745,12 +772,21 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => {
}

const isCollapsed = computedStyle.cursor === "pointer";
const orientationAttribute = toolbar.getAttribute(
"data-react-grab-toolbar-orientation",
);
const orientation =
orientationAttribute === "vertical" ||
orientationAttribute === "horizontal"
? orientationAttribute
: null;

return {
isVisible: computedStyle.opacity !== "0",
isCollapsed,
position,
snapEdge,
orientation,
};
}, ATTRIBUTE_NAME);
};
Expand Down Expand Up @@ -879,7 +915,7 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => {
"[data-react-grab-toolbar-recent]",
);
if (!recentButton) return false;
const unreadDot = recentButton.querySelector('path[fill="#404040"]');
const unreadDot = recentButton.querySelector('path[fill="currentColor"]');
return unreadDot !== null;
}, ATTRIBUTE_NAME);
};
Expand Down Expand Up @@ -2070,7 +2106,6 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => {
getRecentDropdownInfo,
clickRecentItem,
clickRecentCopyAll,
clickRecentClear,
hoverRecentItem,

getSelectionLabelInfo,
Expand Down
196 changes: 138 additions & 58 deletions packages/react-grab/e2e/recent-items.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,51 @@ const copyElement = async (
await reactGrab.page.waitForTimeout(300);
};

interface CopiedListItemContents {
firstCopiedContent: string;
secondCopiedContent: string;
}

const copyThreeListItems = async (
reactGrab: ReactGrabPageObject,
): Promise<CopiedListItemContents> => {
await copyElement(reactGrab, "li:first-child");
const firstCopiedContent = await reactGrab.getClipboardContent();

await copyElement(reactGrab, "li:nth-child(2)");
const secondCopiedContent = await reactGrab.getClipboardContent();

await copyElement(reactGrab, "li:last-child");

return {
firstCopiedContent,
secondCopiedContent,
};
};

const getHighlightedRecentItemIndex = async (
reactGrab: ReactGrabPageObject,
): Promise<number | null> => {
return reactGrab.page.evaluate(() => {
const host = document.querySelector("[data-react-grab]");
const shadowRoot = host?.shadowRoot;
const root = shadowRoot?.querySelector("[data-react-grab]");
const dropdown = root?.querySelector("[data-react-grab-recent-dropdown]");
if (!dropdown) return null;

const recentItemButtons = Array.from(
dropdown.querySelectorAll<HTMLButtonElement>(
"[data-react-grab-recent-item]",
),
);
const highlightedIndex = recentItemButtons.findIndex((recentItemButton) =>
recentItemButton.hasAttribute("data-react-grab-recent-item-highlighted"),
);

return highlightedIndex >= 0 ? highlightedIndex : null;
});
};

test.describe("Recent Items", () => {
test.describe("Toolbar Recent Button", () => {
test("should not be visible before any elements are copied", async ({
Expand Down Expand Up @@ -83,6 +128,16 @@ test.describe("Recent Items", () => {
});

test.describe("Dropdown Open/Close", () => {
test("should not open when pressing R with no recent items", async ({
reactGrab,
}) => {
await reactGrab.activate();
await reactGrab.pressKey("r");
await reactGrab.page.waitForTimeout(100);

expect(await reactGrab.isRecentDropdownVisible()).toBe(false);
});

test("should open when clicking the recent button", async ({
reactGrab,
}) => {
Expand All @@ -93,6 +148,16 @@ test.describe("Recent Items", () => {
expect(isDropdownVisible).toBe(true);
});

test("should open when pressing R while active", async ({ reactGrab }) => {
await copyElement(reactGrab, "li:first-child");
await reactGrab.activate();
await reactGrab.pressKey("r");

await expect
.poll(() => reactGrab.isRecentDropdownVisible(), { timeout: 2000 })
.toBe(true);
});

test("should close when clicking the recent button again", async ({
reactGrab,
}) => {
Expand Down Expand Up @@ -157,20 +222,6 @@ test.describe("Recent Items", () => {
const dropdownInfo = await reactGrab.getRecentDropdownInfo();
expect(dropdownInfo.itemCount).toBe(2);
});

test("should hide recent button after clearing all items", async ({
reactGrab,
}) => {
await copyElement(reactGrab, "li:first-child");
await reactGrab.clickRecentButton();
await reactGrab.clickRecentClear();

await expect
.poll(() => reactGrab.isRecentButtonVisible(), { timeout: 2000 })
.toBe(false);

expect(await reactGrab.isRecentDropdownVisible()).toBe(false);
});
});

test.describe("Item Selection", () => {
Expand Down Expand Up @@ -207,6 +258,76 @@ test.describe("Recent Items", () => {

expect(await reactGrab.isRecentDropdownVisible()).toBe(false);
});

test("should select the next item with ArrowDown then Enter", async ({
reactGrab,
}) => {
const copiedListItemContents = await copyThreeListItems(reactGrab);

await reactGrab.activate();
await reactGrab.pressKey("r");
await expect
.poll(() => reactGrab.isRecentDropdownVisible(), { timeout: 2000 })
.toBe(true);

await reactGrab.pressArrowDown();
await reactGrab.pressEnter();

await expect
.poll(() => reactGrab.isRecentDropdownVisible(), { timeout: 2000 })
.toBe(false);
await expect
.poll(() => reactGrab.getClipboardContent(), { timeout: 3000 })
.toBe(copiedListItemContents.secondCopiedContent);
});

test("should show highlighted state while cycling with arrow keys", async ({
reactGrab,
}) => {
await copyThreeListItems(reactGrab);

await reactGrab.activate();
await reactGrab.pressKey("r");
await expect
.poll(() => reactGrab.isRecentDropdownVisible(), { timeout: 2000 })
.toBe(true);

await expect
.poll(() => getHighlightedRecentItemIndex(reactGrab), { timeout: 2000 })
.toBe(0);

await reactGrab.pressArrowDown();
await expect
.poll(() => getHighlightedRecentItemIndex(reactGrab), { timeout: 2000 })
.toBe(1);

await reactGrab.pressArrowUp();
await expect
.poll(() => getHighlightedRecentItemIndex(reactGrab), { timeout: 2000 })
.toBe(0);
});

test("should select the previous item with ArrowUp then Enter", async ({
reactGrab,
}) => {
const copiedListItemContents = await copyThreeListItems(reactGrab);

await reactGrab.activate();
await reactGrab.pressKey("r");
await expect
.poll(() => reactGrab.isRecentDropdownVisible(), { timeout: 2000 })
.toBe(true);

await reactGrab.pressArrowUp();
await reactGrab.pressEnter();

await expect
.poll(() => reactGrab.isRecentDropdownVisible(), { timeout: 2000 })
.toBe(false);
await expect
.poll(() => reactGrab.getClipboardContent(), { timeout: 3000 })
.toBe(copiedListItemContents.firstCopiedContent);
});
});

test.describe("Copy All", () => {
Expand Down Expand Up @@ -237,7 +358,9 @@ test.describe("Recent Items", () => {
expect(await reactGrab.isRecentDropdownVisible()).toBe(false);
});

test("should trigger copy all via Enter key", async ({ reactGrab }) => {
test("should select highlighted item via Enter key", async ({
reactGrab,
}) => {
await copyElement(reactGrab, "li:first-child");

await reactGrab.page.evaluate(() => navigator.clipboard.writeText(""));
Expand All @@ -248,49 +371,6 @@ test.describe("Recent Items", () => {

const clipboardContent = await reactGrab.getClipboardContent();
expect(clipboardContent).toBeTruthy();
});
});

test.describe("Clear All", () => {
test("should remove all recent items", async ({ reactGrab }) => {
await copyElement(reactGrab, "li:first-child");
await copyElement(reactGrab, "li:last-child");

await reactGrab.clickRecentButton();
expect((await reactGrab.getRecentDropdownInfo()).itemCount).toBe(2);

await reactGrab.clickRecentClear();

await expect
.poll(() => reactGrab.isRecentButtonVisible(), { timeout: 2000 })
.toBe(false);
});

test("should hide the recent button in toolbar after clearing", async ({
reactGrab,
}) => {
await copyElement(reactGrab, "li:first-child");

await expect
.poll(() => reactGrab.isRecentButtonVisible(), { timeout: 2000 })
.toBe(true);

await reactGrab.clickRecentButton();
await reactGrab.clickRecentClear();

await expect
.poll(() => reactGrab.isRecentButtonVisible(), { timeout: 2000 })
.toBe(false);
});

test("should close the dropdown after clearing", async ({ reactGrab }) => {
await copyElement(reactGrab, "li:first-child");
await reactGrab.clickRecentButton();

expect(await reactGrab.isRecentDropdownVisible()).toBe(true);

await reactGrab.clickRecentClear();

expect(await reactGrab.isRecentDropdownVisible()).toBe(false);
});
});
Expand Down
Loading
Loading