From a07755ec101d5957c976023439e1f3b32cfcf4f5 Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Thu, 16 Jul 2026 08:55:02 +0200 Subject: [PATCH] =?UTF-8?q?fix(admin):=20remove=20a=20grid=20chip=20via=20?= =?UTF-8?q?its=20=C3=97=20instead=20of=20leaving=20edit=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ark's combobox InteractOutsideEvent is a CustomEvent whose actually-clicked element is in event.detail.target, not event.target (the dispatch node). The ChipSelect read event.target, so in a real browser a chip's × — which sits in the cell but outside the combobox control, the only in-cell click that reaches interact-outside — read as "outside" and committed the cell, leaving edit mode instead of removing the chip. Read event.detail.target, falling back to event.target where detail is absent. The failure only manifests in a real browser (jsdom doesn't fire interact-outside; the Playwright runner doesn't reproduce it either), so the added e2e test guards the × behaviour generally rather than as a strict regression guard for this event bug. Signed-off-by: Sebastian Mendel --- e2e/admin-inline-edit.spec.ts | 26 ++++++++++++++++++++++++++ frontend/src/lib/chipSelect.tsx | 9 ++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/e2e/admin-inline-edit.spec.ts b/e2e/admin-inline-edit.spec.ts index 1b1e57588..599c009db 100644 --- a/e2e/admin-inline-edit.spec.ts +++ b/e2e/admin-inline-edit.spec.ts @@ -157,6 +157,32 @@ test.describe('Admin inline cell editing', () => { .locator('td[data-col-key="teams"] .tag')).toHaveCount(expected); }); + test('removing a chip with its × stays in edit mode and drops the chip', async ({ page }) => { + // Regression: a chip's × read as an "interact outside" the cell because the handler + // read event.target (the dispatch node) instead of event.detail.target (the actual + // click). So clicking × committed and LEFT edit mode instead of removing the chip. + await page.goto('/ui/admin/users'); + await page.waitForSelector('table.admin-table [role="gridcell"]', { timeout: 15000 }); + await page.locator('input.admin-filter').fill('sandy'); + const row = page.locator('table.admin-table tbody tr').filter({ hasText: /sandy/i }).first(); + await expect(row).toBeVisible(); + const before = await row.locator('td[data-col-key="teams"] .tag').count(); + expect(before).toBeGreaterThanOrEqual(1); + + await row.locator('td[data-col-key="teams"]').focus(); + await page.keyboard.press('Enter'); + await expect(page.locator('.combobox-input')).toBeVisible(); + + await page.locator('td[data-inline-editing] .tag .tag-remove').first().click(); + + // The chip is gone AND the cell is still being edited (the × must not commit/close it). + await expect(page.locator('td[data-inline-editing] .tag')).toHaveCount(before - 1); + await expect(page.locator('td[data-inline-editing]')).toHaveCount(1); + await expect(page.locator('.combobox-input')).toBeVisible(); + + await page.keyboard.press('Escape'); // discard — leave the fixture unchanged + }); + test('the Status sub-page shows read-only diagnostics', async ({ page }) => { await page.goto('/ui/admin/status'); // Seven groups (app/build/php/symfony/db/packages/config). Assert on locale- diff --git a/frontend/src/lib/chipSelect.tsx b/frontend/src/lib/chipSelect.tsx index 0b781ad0e..b76a12c31 100644 --- a/frontend/src/lib/chipSelect.tsx +++ b/frontend/src/lib/chipSelect.tsx @@ -294,7 +294,14 @@ export function ChipSelect(props: { // its own content so the search field is never bound to a narrow column. positioning={{ sameWidth: props.multiple, gutter: 2, flip: true, fitViewport: true }} onInteractOutside={(event) => { - const target = event.target as Node | null + // Ark's InteractOutsideEvent is a CustomEvent: the actually-clicked element + // is in event.detail.target, NOT event.target (which is the dispatch node, + // e.g. the document). Reading event.target made a chip's × read as "outside" + // the cell in a real browser, so clicking it committed the cell (leaving edit + // mode) instead of removing the chip. Multi chips (× buttons) live in rootEl + // but outside the combobox control, so they are the only in-cell clicks that + // reach here. Fall back to event.target where detail is absent. + const target = (event.detail?.target ?? event.target) as Node | null if (target !== null && rootEl?.contains(target)) { event.preventDefault() // a click within the cell keeps editing } else {