Skip to content

Commit 4acb29e

Browse files
Refactor blur timer handling (ellipsis feedback); improve fidelity of UT sequences
1 parent 4f6d10c commit 4acb29e

File tree

2 files changed

+39
-10
lines changed

2 files changed

+39
-10
lines changed

webview-ui/src/components/chat/__tests__/ChatView.test.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,10 @@ describe("ChatView - Focus Grabbing Tests", () => {
12361236
/>
12371237
</ExtensionStateContextProvider>,
12381238
)
1239+
// when chat view is hidden, this triggers a "blur" event
1240+
if (textAreaElement) {
1241+
fireEvent.blur(textAreaElement)
1242+
}
12391243

12401244
expect(mockFocus).toHaveBeenCalledTimes(1)
12411245

@@ -1256,7 +1260,7 @@ describe("ChatView - Focus Grabbing Tests", () => {
12561260
})
12571261

12581262
it("does not grab focus when unhidden and not previously focused", async () => {
1259-
const { rerender } = render(
1263+
const { getByTestId, rerender } = render(
12601264
<ExtensionStateContextProvider>
12611265
<ChatView
12621266
isHidden={false}
@@ -1267,6 +1271,7 @@ describe("ChatView - Focus Grabbing Tests", () => {
12671271
</ExtensionStateContextProvider>,
12681272
)
12691273

1274+
const textAreaElement = getByTestId("chat-textarea").querySelector("input")
12701275
rerender(
12711276
<ExtensionStateContextProvider>
12721277
<ChatView
@@ -1277,6 +1282,10 @@ describe("ChatView - Focus Grabbing Tests", () => {
12771282
/>
12781283
</ExtensionStateContextProvider>,
12791284
)
1285+
// when chat view is hidden, this triggers a "blur" event
1286+
if (textAreaElement) {
1287+
fireEvent.blur(textAreaElement)
1288+
}
12801289

12811290
rerender(
12821291
<ExtensionStateContextProvider>
@@ -1326,6 +1335,10 @@ describe("ChatView - Focus Grabbing Tests", () => {
13261335
expect(mockFocus).toHaveBeenCalledTimes(1)
13271336
})
13281337

1338+
// when extension becomes invisible, this will trigger a blur event before the message is sent.
1339+
if (textAreaElement) {
1340+
fireEvent.blur(textAreaElement)
1341+
}
13291342
await sendActionMessage("didBecomeInvisible")
13301343
expect(mockFocus).toHaveBeenCalledTimes(1)
13311344

@@ -1339,7 +1352,7 @@ describe("ChatView - Focus Grabbing Tests", () => {
13391352
})
13401353

13411354
it("does not grab focus when unhidden and not previously focused", async () => {
1342-
render(
1355+
const { findByTestId } = render(
13431356
<ExtensionStateContextProvider>
13441357
<ChatView
13451358
isHidden={false}
@@ -1350,6 +1363,12 @@ describe("ChatView - Focus Grabbing Tests", () => {
13501363
</ExtensionStateContextProvider>,
13511364
)
13521365

1366+
const textAreaElement = (await findByTestId("chat-textarea")).querySelector("input")
1367+
1368+
// when extension becomes invisible, this will trigger a blur event before the message is sent.
1369+
if (textAreaElement) {
1370+
fireEvent.blur(textAreaElement)
1371+
}
13531372
await sendActionMessage("didBecomeInvisible")
13541373
expect(mockFocus).toHaveBeenCalledTimes(0)
13551374

@@ -1389,9 +1408,12 @@ describe("ChatView - Focus Grabbing Tests", () => {
13891408
if (textAreaElement) {
13901409
fireEvent.blur(textAreaElement)
13911410
}
1392-
13931411
expect(mockFocus).toHaveBeenCalledTimes(1)
13941412

1413+
// must wait for > 500msecs after blur before making panel invisible
1414+
// so that the blur is interpreted as a deliberate user action.
1415+
await sleep(550)
1416+
13951417
await sendActionMessage("didBecomeInvisible")
13961418

13971419
expect(mockFocus).toHaveBeenCalledTimes(1)

webview-ui/src/components/chat/hooks/useFocusPreservation.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ import { useState, useEffect, useRef } from "react"
1818
export function useFocusPreservation(element: HTMLElement | null, isHidden: boolean) {
1919
const [isFocused, setIsFocused] = useState(false)
2020
const isHiddenRef = useRef(isHidden)
21-
const isHiddenLastChangedRef = useRef(0)
21+
const timerRef = useRef<NodeJS.Timeout | null>(null)
2222

23+
// keep isHiddenRef up-to-date
2324
useEffect(() => {
2425
isHiddenRef.current = isHidden
25-
isHiddenLastChangedRef.current = Date.now()
26+
27+
if (isHidden && timerRef.current) {
28+
// Now hidden, so any blur event should not be counted.
29+
clearTimeout(timerRef.current)
30+
}
2631
}, [isHidden])
2732

33+
// Apply focus when required
2834
useEffect(() => {
2935
if (!isHidden && isFocused) {
3036
element?.focus()
@@ -41,14 +47,15 @@ export function useFocusPreservation(element: HTMLElement | null, isHidden: bool
4147
const onTextAreaBlur = () => {
4248
// Blur event should only be interpreted as an intentional defocus
4349
// if they have not occured as a result of the element being hidden.
50+
// We consider a blur event to be an intentional defocus
51+
// if BLUR_DELAY msecs after the blur, the element is not hidden
52+
// (and has not been hidden at any point)
4453
const BLUR_DELAY = 500
45-
setTimeout(() => {
46-
if (!isHiddenRef.current && Date.now() - isHiddenLastChangedRef.current > BLUR_DELAY) {
47-
// We consider a blur event to be an intentional defocus
48-
// if BLUR_DELAY msecs after the blur, the element is still hidden
49-
// and the element's hidden state hasn't changed witin that time period.
54+
timerRef.current = setTimeout(() => {
55+
if (!isHiddenRef.current) {
5056
setIsFocused(false)
5157
}
58+
timerRef.current = null
5259
}, BLUR_DELAY)
5360
}
5461

0 commit comments

Comments
 (0)