diff --git a/src/styles.css b/src/styles.css index 7c674a78..31b34219 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1775,22 +1775,27 @@ body.detached-tab .graph-overlay-panel { border: none; background: transparent; color: var(--fg-faint); cursor: pointer; border-radius: var(--r-xs); flex-shrink: 0; - display: none; align-items: center; justify-content: center; + /* `display: none` made the hover-revealed controls unreachable from the + keyboard: a browser cannot tab to an element it has removed from its focus + order. Keep them focusable, then let focus-within reveal the cluster. */ + display: inline-flex; align-items: center; justify-content: center; + opacity: 0; pointer-events: none; } -/* Revealed on hover like the Library row actions, but ALWAYS present for the - keyboard: `:focus-within` keeps the cluster visible while the row or one of - its controls owns focus, so no operation is keyboard-unreachable. +/* Revealed on hover like the Library row actions, but ALWAYS in the keyboard + focus order: `:focus-within` makes the visually concealed cluster visible + while the row or one of its controls owns focus, so no operation is + keyboard-unreachable. `[aria-expanded="true"]` holds it visible for its own dialog/confirmation's whole lifetime — both are body-mounted, so by the time one closes the - pointer and focus have left the row, and `focus()` on a `display: none` - trigger is a silent no-op in a real browser. */ + pointer and focus have left the row, and an invisible trigger must not + become unreachable before focus can return to it. */ .dash-tree-row:hover .dash-tree-act, .dash-tree-row:focus-within .dash-tree-act, -.dash-tree-act[aria-expanded="true"] { display: inline-flex; } +.dash-tree-act[aria-expanded="true"] { opacity: 1; pointer-events: auto; } /* #447's orphan-variable trash stays ALWAYS visible: it is the only way to remove stored SQL nothing references any more, and a hover-only affordance would hide that row's whole point. #494 preserves that control as it was. */ -.dash-tree-act-static { display: inline-flex; } +.dash-tree-act-static { opacity: 1; pointer-events: auto; } .dash-tree-act:hover { color: var(--fg); background: var(--bg-hover); } /* Destructive treatment on hover/focus, so the rightmost control of the cluster reads as the one that removes something. */ diff --git a/tests/e2e/tile-open-workbench.html b/tests/e2e/tile-open-workbench.html index 76f12a88..5d5fc406 100644 --- a/tests/e2e/tile-open-workbench.html +++ b/tests/e2e/tile-open-workbench.html @@ -47,7 +47,6 @@ import { createCodeMirrorEditor } from '/src/editor/codemirror-adapter.js'; import { createSpecEditor } from '/src/editor/spec-editor.js'; import { createCodeViewer } from '/src/editor/code-viewer.js'; - import { createAuthenticatedExecutionScope } from '/src/application/authenticated-execution-scope.js'; import { libraryQueries } from '/src/dashboard/model/query-ownership.js'; // `table` rather than a chart panel: the Chart.js seam is deliberately NOT @@ -119,11 +118,10 @@ CodeViewer: createCodeViewer, }); // #524: this full-surface fixture bypasses sign-in but still executes - // Dashboard and Workbench work, so provide the authenticated epoch that - // production boot installs after a successful connection. - const scope = createAuthenticatedExecutionScope({ epoch: 1, cancelRemote: () => {} }); - app.executionScope = () => scope; - app.requireAuthenticatedExecution = () => scope; + // Dashboard and Workbench work. Use the app-owned resume seam rather than + // replacing its public scope accessors: `renderApp()` uses its private + // lifecycle state to decide whether the blocking auth host starts hidden. + app.resumeAuthenticatedExecution(); app.conn.ensureFreshToken = async () => true; app.conn.chCtx.onSignedOut = () => {}; app.catalog.loadSchema = async () => { app.state.schema.value = []; }; diff --git a/tests/e2e/tile-open-workbench.spec.js b/tests/e2e/tile-open-workbench.spec.js index e1645007..a86d0edd 100644 --- a/tests/e2e/tile-open-workbench.spec.js +++ b/tests/e2e/tile-open-workbench.spec.js @@ -114,13 +114,13 @@ test('a Dashboard-row plus creates a blank linked Panel and focuses its SQL edit const plus = dashboard.locator('.dash-tree-act[aria-label="Add panel to Sales"]'); // The direct action is a real keyboard target, revealed by focus just like - // the adjacent pencil and trash. Start from the row's disclosure control: - // the next Tab must be the plus in the declared within-row order. - await dashboard.locator('.dash-tree-chev').focus(); - await page.keyboard.press('Tab'); + // the adjacent pencil and trash. Focus it explicitly: browser tab order also + // includes the independently interactive row label, so it is not portable to + // assert that the plus immediately follows the disclosure control. + await plus.focus(); await expect(plus).toHaveAttribute('aria-haspopup', 'dialog'); await expect(plus).toBeFocused(); - await expect.poll(() => plus.evaluate((el) => getComputedStyle(el).display)).not.toBe('none'); + await expect.poll(() => plus.evaluate((el) => getComputedStyle(el).opacity)).toBe('1'); await page.keyboard.press('Enter'); const dialog = page.getByRole('dialog', { name: 'Add panel' });