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 } = {}) {
2424 });
2525 const element = Object.assign(new EventTarget(), {
2626 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),
2833 });
2934 const onSelection = vi.fn();
3035 const onDismiss = vi.fn();
......@@ -58,7 +63,7 @@ function createSelectionSurface({ interactiveActions = false } = {}) {
5863 const press = event("pointerdown", { button, isPrimary });
5964 Object.defineProperty(press, "target", { value: target });
6065 document.dispatchEvent(press);
61 if (target === element && !consumed) element.dispatchEvent(press);
66 if (element.contains(target) && !consumed) element.dispatchEvent(press);
6267 };
6368 const focus = (target: EventTarget) => {
6469 document.activeElement = target;
......@@ -92,6 +97,7 @@ function createSelectionSurface({ interactiveActions = false } = {}) {
9297 view,
9398 document,
9499 element,
100 buttonIcon,
95101 actionElement,
96102 field,
97103 onSelection,
......@@ -177,6 +183,31 @@ describe("selection action gestures", () => {
177183 expect(surface.onSelection).not.toHaveBeenCalled();
178184 });
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
180211 it("ignores nonprimary pointers and non-left mouseup without ending a left drag", () => {
181212 const surface = createSelectionSurface();
182213 surface.down({ isPrimary: false });
apps/web/src/lib/selectionActions.ts+4
......@@ -101,6 +101,10 @@ export function observeSelectionActions({
101101 };
102102 const onSelectionStart = (event: PointerEvent) => {
103103 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;
104108 // A surface may consume a press for a link or terminal mouse reporting.
105109 gestureActive = event.button === 0 && !event.defaultPrevented;
106110 dismissed = !gestureActive;