mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
fix(web-shell): treat an unfocused shadow root as unclaimed in the discard latch
The discard focus latch read the parked node from the page wrapper's own
root, so a shadow-root mount whose discard dropped focus to the outer
document body produced ShadowRoot.activeElement === null. Every `ours`
operand was false, the latch was dropped, and the composer that just got
the draft back was never focused. Treat an unfocused shadow root as
unclaimed, matching the body-focus operand. The latch still dies when the
user moves on (ManagedSessionsPage.test.tsx pins that).
Pinned by a new shadow-root regression: the page mounts into
host.attachShadow({mode:'open'}) with a host-side navigationTarget, a
pending record is discarded, and shadow.activeElement must be the
composer. The test is red without the fix and green with it.
Addresses review thread R13-1 on #16200.
Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
1 parent
32cd433c02
commit
ec9763fa60
2 files changed
+62
-1
No files matched your search
@@ -5191,6 +5191,63 @@ describe('ManagedSessionsPage', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('moves focus to the composer after a discard inside a shadow root with a host nav slot', async () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
const shadowContainer = document.createElement('div');
|
||||
shadow.appendChild(shadowContainer);
|
||||
const navigationTarget = document.createElement('nav');
|
||||
document.body.appendChild(navigationTarget);
|
||||
const shadowRoot = createRoot(shadowContainer);
|
||||
try {
|
||||
seedPending({
|
||||
idempotencyKey: 'key-1',
|
||||
text: 'Session draft',
|
||||
sessionId: 's1',
|
||||
});
|
||||
await act(async () => {
|
||||
shadowRoot.render(
|
||||
<I18nProvider language="en">
|
||||
<ManagedSessionsPage
|
||||
sessionId="s1"
|
||||
onSelectSession={onSelect}
|
||||
managedAgentProvider={provider}
|
||||
navigationTarget={navigationTarget}
|
||||
interactive={true}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
);
|
||||
await flush();
|
||||
});
|
||||
const discard = [...shadowContainer.querySelectorAll('button')].find(
|
||||
(item) => item.textContent === 'Discard this request',
|
||||
);
|
||||
expect(discard).toBeDefined();
|
||||
discard!.focus();
|
||||
await act(async () => {
|
||||
discard!.click();
|
||||
await flush();
|
||||
});
|
||||
// In Chromium the discard button's unmount leaves document.activeElement
|
||||
// at body and shadow.activeElement null; the latch must treat that as
|
||||
// unclaimed and land focus on the composer that just got the draft back,
|
||||
// not drop the latch. jsdom keeps the detached button as the shadow
|
||||
// root's activeElement, so assert only the outcome, not the browser's
|
||||
// exact blur state.
|
||||
expect(shadow.activeElement).toBe(
|
||||
shadowContainer.querySelector('textarea'),
|
||||
);
|
||||
expect(shadowContainer.querySelector('textarea')?.value).toBe(
|
||||
'Session draft',
|
||||
);
|
||||
} finally {
|
||||
await act(async () => shadowRoot.unmount());
|
||||
navigationTarget.remove();
|
||||
host.remove();
|
||||
}
|
||||
});
|
||||
|
||||
it('leaves focus alone when the user moved on during a discard’s loading window', async () => {
|
||||
let releaseSummary!: (value: ManagedAgentSessionSummary) => void;
|
||||
mocks.client.getSession.mockImplementationOnce(
|
||||
|
||||
@@ -866,12 +866,16 @@ function ManagedSessionsContent({
|
||||
// document.activeElement retargets to the shadow host in portal mode; read
|
||||
// the focused node from the page wrapper's own root instead.
|
||||
const parked = getShadowAwareActiveElement(pageRef.current);
|
||||
// A shadow root with nothing focused reports null here, not body, so a
|
||||
// shadow-root mount whose discard dropped focus to the outer document
|
||||
// body must still be recognised as unclaimed.
|
||||
const unclaimed = parked === null || parked === document.body;
|
||||
// hostSessionOnly renders no list, so the fallback below parks focus on the
|
||||
// page wrapper; the latch accepts the node it parked on itself, so a user
|
||||
// who merely clicked inside that pane is still recognised as having moved
|
||||
// on.
|
||||
const ours =
|
||||
parked === document.body ||
|
||||
unclaimed ||
|
||||
parked === sessionsNavRef.current ||
|
||||
(discardParked.current !== null && parked === discardParked.current);
|
||||
if (composer) {
|
||||
|
||||
Reference in new issue
Block a user