authorgravatar for saiansh2525@gmail.comYash Singh <saiansh2525@gmail.com> 2026-09-21 17:11:14-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-21 17:11:14-05:00
log2d377bfb1c8c85a61679fa6cdb6e8181cfb13626
tree750385aeb0df596a29cbbc14baceeb643c5b040d
parent2c2fa8cd7ded97be76587ae1cfce54ee0a65f544
signature Signed by PGP key B5690EEEBB952194

fix(web): dismiss selection actions when pressing buttons (#12950)


2 files changed, 37 insertions(+), 2 deletions(-)

apps/web/src/lib/selectionActions.test.ts+33-2
...@@ -24,7 +24,12 @@ function createSelectionSurface({ interactiveActions = false } = {}) {...@@ -24,7 +24,12 @@ function createSelectionSurface({ interactiveActions = false } = {}) {
24 });24 });
25 const element = Object.assign(new EventTarget(), {25 const element = Object.assign(new EventTarget(), {
26 ownerDocument: document,26 ownerDocument: document,
27 contains: (target: unknown): boolean => target === element,27 contains: (target: unknown): boolean => target === element || target === buttonIcon,
28 closest: () => null,
29 });
30 const button = new EventTarget();
31 const buttonIcon = Object.assign(new EventTarget(), {
32 closest: (selector: string) => (selector === "button, [role=button]" ? button : null),
28 });33 });
29 const onSelection = vi.fn();34 const onSelection = vi.fn();
30 const onDismiss = vi.fn();35 const onDismiss = vi.fn();
...@@ -58,7 +63,7 @@ function createSelectionSurface({ interactiveActions = false } = {}) {...@@ -58,7 +63,7 @@ function createSelectionSurface({ interactiveActions = false } = {}) {
58 const press = event("pointerdown", { button, isPrimary });63 const press = event("pointerdown", { button, isPrimary });
59 Object.defineProperty(press, "target", { value: target });64 Object.defineProperty(press, "target", { value: target });
60 document.dispatchEvent(press);65 document.dispatchEvent(press);
61 if (target === element && !consumed) element.dispatchEvent(press);66 if (element.contains(target) && !consumed) element.dispatchEvent(press);
62 };67 };
63 const focus = (target: EventTarget) => {68 const focus = (target: EventTarget) => {
64 document.activeElement = target;69 document.activeElement = target;
...@@ -92,6 +97,7 @@ function createSelectionSurface({ interactiveActions = false } = {}) {...@@ -92,6 +97,7 @@ function createSelectionSurface({ interactiveActions = false } = {}) {
92 view,97 view,
93 document,98 document,
94 element,99 element,
100 buttonIcon,
95 actionElement,101 actionElement,
96 field,102 field,
97 onSelection,103 onSelection,
...@@ -177,6 +183,31 @@ describe("selection action gestures", () => {...@@ -177,6 +183,31 @@ describe("selection action gestures", () => {
177 expect(surface.onSelection).not.toHaveBeenCalled();183 expect(surface.onSelection).not.toHaveBeenCalled();
178 });184 });
179185
186 it.each([1, 2])(
187 "dismisses selection actions when a button is pressed after %i clicks",
188 (detail) => {
189 const surface = createSelectionSurface();
190 surface.down();
191 surface.up({ detail });
192 surface.flush();
193 surface.onSelection.mockClear();
194 surface.onDismiss.mockClear();
195
196 surface.down({ target: surface.buttonIcon });
197 surface.up();
198 surface.change();
199 surface.flush(1000);
200 expect(surface.onDismiss).toHaveBeenCalledWith("interaction");
201 expect(surface.onSelection).not.toHaveBeenCalled();
202
203 surface.down();
204 surface.change();
205 surface.up();
206 surface.flush();
207 expect(surface.onSelection).toHaveBeenCalledOnce();
208 },
209 );
210
180 it("ignores nonprimary pointers and non-left mouseup without ending a left drag", () => {211 it("ignores nonprimary pointers and non-left mouseup without ending a left drag", () => {
181 const surface = createSelectionSurface();212 const surface = createSelectionSurface();
182 surface.down({ isPrimary: false });213 surface.down({ isPrimary: false });
apps/web/src/lib/selectionActions.ts+4
...@@ -101,6 +101,10 @@ export function observeSelectionActions({...@@ -101,6 +101,10 @@ export function observeSelectionActions({
101 };101 };
102 const onSelectionStart = (event: PointerEvent) => {102 const onSelectionStart = (event: PointerEvent) => {
103 if (!event.isPrimary) return;103 if (!event.isPrimary) return;
104 const target = event.target as Element | null;
105 // Buttons can preserve an existing text selection while opening a menu.
106 // Their release must not reopen selection actions beside the button.
107 if (target?.closest("button, [role=button]")) return;
104 // A surface may consume a press for a link or terminal mouse reporting.108 // A surface may consume a press for a link or terminal mouse reporting.
105 gestureActive = event.button === 0 && !event.defaultPrevented;109 gestureActive = event.button === 0 && !event.defaultPrevented;
106 dismissed = !gestureActive;110 dismissed = !gestureActive;