Skip to content

Commit feb178b

Browse files
committed
fix(extension): retain focus ownership through brief capture preparation
1 parent 45e7469 commit feb178b

2 files changed

Lines changed: 31 additions & 83 deletions

File tree

‎apps/sim/hooks/use-composer-submission.ts‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ interface UseComposerSubmissionProps {
2626

2727
export function useComposerSubmission({ scope, mode }: UseComposerSubmissionProps) {
2828
const active = useRef<PendingComposerSubmission | null>(null)
29-
const focusAfterSend = useRef<HTMLTextAreaElement | null>(null)
29+
const focusAfterSend = useRef<{
30+
input: HTMLTextAreaElement
31+
initiator: Element | null
32+
} | null>(null)
3033
const [pending, setPending] = useState(false)
3134

3235
useLayoutEffect(() => {
@@ -39,10 +42,14 @@ export function useComposerSubmission({ scope, mode }: UseComposerSubmissionProp
3942

4043
useLayoutEffect(() => {
4144
if (pending) return
42-
const input = focusAfterSend.current
45+
const focus = focusAfterSend.current
4346
focusAfterSend.current = null
44-
if (input?.isConnected && document.hasFocus() && document.activeElement === document.body) {
45-
input.focus({ preventScroll: true })
47+
if (
48+
focus?.input.isConnected &&
49+
document.hasFocus() &&
50+
(document.activeElement === document.body || document.activeElement === focus.initiator)
51+
) {
52+
focus.input.focus({ preventScroll: true })
4653
}
4754
}, [pending])
4855

@@ -60,7 +67,7 @@ export function useComposerSubmission({ scope, mode }: UseComposerSubmissionProp
6067
return
6168
}
6269
active.current = result
63-
focusAfterSend.current = input
70+
focusAfterSend.current = input ? { input, initiator: document.activeElement } : null
6471
setPending(true)
6572
void result.ready
6673
.then((commit) => {

‎apps/sim/scripts/test-browser-extension-e2e.ts‎

Lines changed: 19 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,6 @@ const chatResponses: {
8787
}[] = []
8888
const uploadRequests: { event: string; stage: string; method: string; status?: number }[] = []
8989
const draftRemovalReceipts: { scope: string; phase: string; matches: number }[] = []
90-
const submissionFocusReceipts: {
91-
scope: string
92-
events: {
93-
event: string
94-
activeTag: string | null
95-
activeLabel: string | null
96-
inputFocused: boolean
97-
inputConnected: boolean
98-
documentFocused: boolean
99-
inert: boolean
100-
oldInert: string | null
101-
}[]
102-
}[] = []
10390
const responseDiagnostics: Promise<void>[] = []
10491
const chatTurns = new Map<string, number>()
10592
const chatAttempts: {
@@ -2043,70 +2030,25 @@ try {
20432030
await input().fill(draft)
20442031
count = received.length
20452032
phase = 'denied-send'
2046-
const focusTrace = await input().evaluateHandle((element) => {
2047-
const events: (typeof submissionFocusReceipts)[number]['events'] = []
2048-
const record = (event: string, oldInert: string | null = null) => {
2049-
if (events.length >= 64) return
2050-
events.push({
2051-
event,
2052-
activeTag: document.activeElement?.tagName ?? null,
2053-
activeLabel: document.activeElement?.getAttribute('aria-label') ?? null,
2054-
inputFocused: document.activeElement === element,
2055-
inputConnected: element.isConnected,
2056-
documentFocused: document.hasFocus(),
2057-
inert: Boolean(element.closest('[inert]')),
2058-
oldInert,
2059-
})
2060-
}
2061-
const observeEvent = (event: Event) => record(event.type)
2062-
const types = ['pointerdown', 'click', 'focus', 'blur'] as const
2063-
for (const type of types) window.addEventListener(type, observeEvent, true)
2064-
const observer = new MutationObserver((records) => {
2065-
for (const mutation of records) record('inert-mutation', mutation.oldValue)
2066-
})
2067-
observer.observe(document.body, {
2068-
attributes: true,
2069-
subtree: true,
2070-
attributeFilter: ['inert'],
2071-
attributeOldValue: true,
2072-
})
2073-
record('before-mouse-send')
2074-
return {
2075-
finish() {
2076-
record('after-mouse-send')
2077-
observer.disconnect()
2078-
for (const type of types) window.removeEventListener(type, observeEvent, true)
2079-
return events
2080-
},
2081-
}
2082-
})
2083-
try {
2084-
await frame()
2085-
.getByRole('button', { name: org ? 'Send' : 'Send message', exact: true })
2086-
.click()
2087-
await expect(
2088-
shell.getByRole('button', { name: 'Request site access', exact: true })
2089-
).toBeEnabled()
2090-
await expect(input()).toHaveValue(draft)
2091-
await expect
2092-
.poll(() => input().evaluate((element) => Boolean(element.closest('[inert]'))))
2093-
.toBe(false)
2094-
assert.equal(received.length, count)
2095-
assert(
2096-
await frame().evaluate(() => document.hasFocus()),
2097-
'The denied mouse send must keep the chat document focused'
2098-
)
2099-
await expect(input()).toBeFocused()
2100-
await shell.keyboard.type(' Continue typing after the denied capture.')
2101-
await expect(input()).toHaveValue(`${draft} Continue typing after the denied capture.`)
2102-
assert.equal(received.length, count)
2103-
} finally {
2104-
submissionFocusReceipts.push({
2105-
scope: org ? 'organization' : 'workspace',
2106-
events: await focusTrace.evaluate((trace) => trace.finish()),
2107-
})
2108-
await focusTrace.dispose()
2109-
}
2033+
await frame()
2034+
.getByRole('button', { name: org ? 'Send' : 'Send message', exact: true })
2035+
.click()
2036+
await expect(
2037+
shell.getByRole('button', { name: 'Request site access', exact: true })
2038+
).toBeEnabled()
2039+
await expect(input()).toHaveValue(draft)
2040+
await expect
2041+
.poll(() => input().evaluate((element) => Boolean(element.closest('[inert]'))))
2042+
.toBe(false)
2043+
assert.equal(received.length, count)
2044+
assert(
2045+
await frame().evaluate(() => document.hasFocus()),
2046+
'The denied mouse send must keep the chat document focused'
2047+
)
2048+
await expect(input()).toBeFocused()
2049+
await shell.keyboard.type(' Continue typing after the denied capture.')
2050+
await expect(input()).toHaveValue(`${draft} Continue typing after the denied capture.`)
2051+
assert.equal(received.length, count)
21102052
await screenshot(`reference-${org ? 'organization' : 'workspace'}-denied`)
21112053
const accessNotice = await shell
21122054
.getByRole('status')
@@ -3087,7 +3029,6 @@ try {
30873029
dropReceipts,
30883030
uploadRequests,
30893031
draftRemovalReceipts,
3090-
submissionFocusReceipts,
30913032
},
30923033
null,
30933034
2

0 commit comments

Comments
 (0)