Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions apps/openstory/stories/renderer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,9 @@ const Scene = (props: SceneProps) => {
mouseX={mouseX()}
isPromptMode={props.isPromptMode}
inputValue={props.inputValue}
isPendingDismiss={props.isPendingDismiss}
discardPrompt={
props.isPendingDismiss ? { onConfirm: noop, onCancel: noop } : undefined
}
isFrozen={false}
toolbarVisible={props.showToolbar}
isActive={props.isActive}
Expand All @@ -336,7 +338,6 @@ const Scene = (props: SceneProps) => {
onInputSubmit={noop}
onToggleExpand={noop}
onConfirmDismiss={noop}
onCancelDismiss={noop}
onToggleActive={noop}
onToolbarStateChange={noop}
onContextMenuDismiss={noop}
Expand Down
3 changes: 1 addition & 2 deletions apps/openstory/stories/selection-label.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const baseProps: SelectionLabelProps = {
onSubmit: noop,
onDismiss: noop,
onConfirmDismiss: noop,
onCancelDismiss: noop,
onAcknowledgeError: noop,
onRetry: noop,
onShowContextMenu: noop,
Expand Down Expand Up @@ -122,7 +121,7 @@ export const PendingDismiss: Story = {
tagName: "header",
componentName: "Header",
isPromptMode: true,
isPendingDismiss: true,
discardPrompt: { onConfirm: noop, onCancel: noop },
inputValue: "tweak the spacing",
},
};
Expand Down
18 changes: 18 additions & 0 deletions packages/react-grab/e2e/edit-panel-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,24 @@ export const clickHeaderCopyButton = async (page: Page): Promise<void> => {
);
};

export const clickDiscardButton = async (
page: Page,
action: "cancel" | "confirm" | "copy",
): Promise<void> => {
await page.evaluate(
({ attrName, actionName }) => {
const host = document.querySelector(`[${attrName}]`);
const shadowRoot = host?.shadowRoot;
const button = shadowRoot?.querySelector<HTMLButtonElement>(
`[data-react-grab-discard-button='${actionName}']`,
);
if (!button) throw new Error("Discard button not found");
button.click();
},
{ attrName: ATTRIBUTE_NAME, actionName: action },
);
};

export const dragActiveSlider = async (page: Page): Promise<void> => {
const sliderBounds = await page.evaluate(
({ attrName, propertyAttr }) => {
Expand Down
200 changes: 194 additions & 6 deletions packages/react-grab/e2e/edit-panel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
IDLE_BUFFER_MS,
SEARCH_INPUT_ATTR,
clearEditStorage,
clickDiscardButton,
clickHeaderCopyButton,
dispatchOutsideDismiss,
dragActiveSlider,
Expand All @@ -25,7 +26,6 @@ import {
getOverlayFocusVisualStates,
getPropertyRowBounds,
getSearchInputFocusVisualState,
getVisibleSliderVisualState,
getVisiblePropertyKeys,
hoverVisibleSlider,
isDiscardPromptVisible,
Expand Down Expand Up @@ -285,6 +285,110 @@ test.describe("Style Panel", () => {
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(true);
});

test("mouse movement after keyboard tweak opens discard prompt with Copy", async ({
reactGrab,
}) => {
await openEditPanel(reactGrab, BUTTON_SELECTOR);
await reactGrab.page.keyboard.press("ArrowRight");
await reactGrab.page.waitForTimeout(80);
const inlineStyleAfterTweak = await getInlineStyleAttribute(reactGrab.page, BUTTON_SELECTOR);
expect(inlineStyleAfterTweak.length).toBeGreaterThan(0);

await reactGrab.page.mouse.move(10, 10);
await reactGrab.page.waitForTimeout(80);
expect(await isEditPanelVisible(reactGrab.page)).toBe(true);
expect(await isEditPanelCompact(reactGrab.page)).toBe(false);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(true);

await clickDiscardButton(reactGrab.page, "copy");
await expect.poll(() => isEditPanelVisible(reactGrab.page)).toBe(false);
expect(await getInlineStyleAttribute(reactGrab.page, BUTTON_SELECTOR)).toBe(
inlineStyleAfterTweak,
);
});

test("canceling mouse-move discard prompt consumes the pointer handoff", async ({
reactGrab,
}) => {
await openEditPanel(reactGrab, BUTTON_SELECTOR);
await reactGrab.page.keyboard.press("ArrowRight");
await reactGrab.page.waitForTimeout(80);

await reactGrab.page.mouse.move(10, 10);
await reactGrab.page.waitForTimeout(80);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(true);

await clickDiscardButton(reactGrab.page, "cancel");
await reactGrab.page.waitForTimeout(80);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(false);

await reactGrab.page.mouse.move(20, 20);
await reactGrab.page.waitForTimeout(80);
expect(await isEditPanelVisible(reactGrab.page)).toBe(true);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(false);
});

test("canceling outside-click discard prompt consumes the pointer handoff", async ({
reactGrab,
}) => {
await openEditPanel(reactGrab, BUTTON_SELECTOR);
await reactGrab.page.keyboard.press("ArrowRight");
await reactGrab.page.waitForTimeout(80);

await dispatchOutsideDismiss(reactGrab.page);
await reactGrab.page.waitForTimeout(80);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(true);

await clickDiscardButton(reactGrab.page, "cancel");
await reactGrab.page.waitForTimeout(80);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(false);

await reactGrab.page.mouse.move(20, 20);
await reactGrab.page.waitForTimeout(80);
expect(await isEditPanelVisible(reactGrab.page)).toBe(true);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(false);
});

test("mouse movement while discard prompt is visible does not consume the handoff", async ({
reactGrab,
}) => {
await openEditPanel(reactGrab, BUTTON_SELECTOR);
await reactGrab.page.keyboard.press("ArrowRight");
await reactGrab.page.waitForTimeout(80);

await openDiscardPromptViaEscape(reactGrab.page);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(true);
await reactGrab.page.mouse.move(10, 10);
await reactGrab.page.waitForTimeout(80);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(true);

await clickDiscardButton(reactGrab.page, "cancel");
await reactGrab.page.waitForTimeout(80);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(false);

await reactGrab.page.mouse.move(20, 20);
await reactGrab.page.waitForTimeout(80);
expect(await isEditPanelVisible(reactGrab.page)).toBe(true);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(false);
});

test("keyboard navigation after pointer tweak does not arm mouse-move discard prompt", async ({
reactGrab,
}) => {
await openEditPanel(reactGrab, BUTTON_SELECTOR);
await dragActiveSlider(reactGrab.page);
await reactGrab.page.waitForTimeout(80);
expect(await isHeaderCopyButtonVisible(reactGrab.page)).toBe(true);

await reactGrab.page.keyboard.press("ArrowDown");
await reactGrab.page.waitForTimeout(80);
await reactGrab.page.mouse.move(10, 10);
await reactGrab.page.waitForTimeout(80);

expect(await isEditPanelVisible(reactGrab.page)).toBe(true);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(false);
});

test("net-zero tweak dismiss restores preview inline styles", async ({ reactGrab }) => {
await openEditPanel(reactGrab, BUTTON_SELECTOR);
const beforeTweak = await getInlineStyleAttribute(reactGrab.page, BUTTON_SELECTOR);
Expand Down Expand Up @@ -343,6 +447,91 @@ test.describe("Style Panel", () => {
await reactGrab.page.keyboard.press("Escape");
await expect.poll(() => isEditPanelVisible(reactGrab.page)).toBe(false);
});

test("mouse movement without pending tweaks does not open the discard prompt", async ({
reactGrab,
}) => {
await openEditPanel(reactGrab, BUTTON_SELECTOR);

await reactGrab.page.mouse.move(10, 10);
await reactGrab.page.waitForTimeout(80);

// Nothing was tweaked, so the handoff was never armed.
expect(await isEditPanelVisible(reactGrab.page)).toBe(true);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(false);
});

test("mouse-move discard prompt Yes reverts the keyboard tweak", async ({ reactGrab }) => {
await openEditPanel(reactGrab, BUTTON_SELECTOR);
const beforeTweak = await getInlineStyleAttribute(reactGrab.page, BUTTON_SELECTOR);
await reactGrab.page.keyboard.press("ArrowRight");
await reactGrab.page.waitForTimeout(80);
expect(await getInlineStyleAttribute(reactGrab.page, BUTTON_SELECTOR)).not.toBe(beforeTweak);

await reactGrab.page.mouse.move(10, 10);
await reactGrab.page.waitForTimeout(80);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(true);

await clickDiscardButton(reactGrab.page, "confirm");
await expect.poll(() => isEditPanelVisible(reactGrab.page)).toBe(false);
expect(await getInlineStyleAttribute(reactGrab.page, BUTTON_SELECTOR)).toBe(beforeTweak);
});

test("a fresh keyboard tweak re-arms the consumed pointer handoff", async ({ reactGrab }) => {
await openEditPanel(reactGrab, BUTTON_SELECTOR);
await reactGrab.page.keyboard.press("ArrowRight");
await reactGrab.page.waitForTimeout(80);

await reactGrab.page.mouse.move(10, 10);
await reactGrab.page.waitForTimeout(80);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(true);

await clickDiscardButton(reactGrab.page, "cancel");
await reactGrab.page.waitForTimeout(80);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(false);

// The handoff is one-shot, but a new keyboard commit re-arms it.
await reactGrab.page.keyboard.press("ArrowRight");
await reactGrab.page.waitForTimeout(80);
await reactGrab.page.mouse.move(40, 40);
await reactGrab.page.waitForTimeout(80);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(true);
});

test("typing a tailwind class arms the mouse-move discard handoff", async ({ reactGrab }) => {
await openEditPanel(reactGrab, BUTTON_SELECTOR);
const beforeTweak = await getInlineStyleAttribute(reactGrab.page, BUTTON_SELECTOR);
// px-4 (16px) differs from the button's px-2 (8px), so it's a real,
// submittable edit rather than a net-zero one.
await reactGrab.page.keyboard.type("px-4");
await reactGrab.page.waitForTimeout(IDLE_BUFFER_MS);
expect(await getInlineStyleAttribute(reactGrab.page, BUTTON_SELECTOR)).not.toBe(beforeTweak);

await reactGrab.page.mouse.move(10, 10);
await reactGrab.page.waitForTimeout(80);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(true);
});

test("touch pointer movement does not consume the keyboard handoff", async ({ reactGrab }) => {
await openEditPanel(reactGrab, BUTTON_SELECTOR);
await reactGrab.page.keyboard.press("ArrowRight");
await reactGrab.page.waitForTimeout(80);

// A touch pointermove is ignored by the mouse-only handoff, so it must
// neither open the prompt nor consume the arm.
await reactGrab.page.evaluate(() => {
window.dispatchEvent(
new PointerEvent("pointermove", { pointerType: "touch", bubbles: true }),
);
});
await reactGrab.page.waitForTimeout(80);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(false);

// The still-armed handoff fires on the next real mouse move.
await reactGrab.page.mouse.move(10, 10);
await reactGrab.page.waitForTimeout(80);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(true);
});
});

test.describe("Property listing", () => {
Expand Down Expand Up @@ -777,17 +966,16 @@ test.describe("Style Panel", () => {
expect(await isEditPanelCompact(reactGrab.page)).toBe(true);
});

test("compact slider shows unit marks on hover", async ({ reactGrab }) => {
test("compact keyboard tweak opens discard prompt on hover", async ({ reactGrab }) => {
await openEditPanel(reactGrab, BUTTON_SELECTOR);
await reactGrab.page.keyboard.press("ArrowRight");
await reactGrab.page.waitForTimeout(IDLE_BUFFER_MS);
expect(await isEditPanelCompact(reactGrab.page)).toBe(true);
const beforeHover = await getVisibleSliderVisualState(reactGrab.page);
expect(beforeHover.maxHashMarkOpacity).toBe(0);

await hoverVisibleSlider(reactGrab.page);
await reactGrab.page.waitForTimeout(IDLE_BUFFER_MS);
const afterHover = await getVisibleSliderVisualState(reactGrab.page);
expect(afterHover.maxHashMarkOpacity).toBeGreaterThan(0);
expect(await isEditPanelCompact(reactGrab.page)).toBe(false);
expect(await isDiscardPromptVisible(reactGrab.page)).toBe(true);
});

test("typing in search re-expands the compact panel", async ({ reactGrab }) => {
Expand Down
Loading
Loading