fix(runtime): track useFocus autoFocus updates

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-06-05 20:45:57 +08:00
committed by GitHub
parent 7bf033b009
commit 941fff1845
2 changed files with 38 additions and 6 deletions
@@ -41,6 +41,38 @@ test("useFocusManager().activeId tracks the currently focused component", async
expect(activeId.value).toBe("a"); expect(activeId.value).toBe("a");
}); });
test("useFocus autoFocus prop update focuses when no item is focused", async () => {
let activeId!: ReturnType<typeof useFocusManager>["activeId"];
const autoFocus = shallowRef(false);
const Item = defineComponent({
props: {
id: { type: String, required: true },
autoFocus: Boolean,
},
setup(props) {
const { isFocused } = useFocus(props);
return () => <Text>{isFocused.value ? "focused" : "unfocused"}</Text>;
},
});
const App = defineComponent(() => {
activeId = useFocusManager().activeId;
return () => <Item id="item" autoFocus={autoFocus.value} />;
});
const { lastFrame, waitUntilRenderFlush } = await render(App);
expect(activeId.value).toBeNull();
expect(lastFrame()).toContain("unfocused");
autoFocus.value = true;
await waitUntilRenderFlush();
expect(activeId.value).toBe("item");
expect(lastFrame()).toContain("focused");
});
// Locks the vue API-surface sentinel: `activeId` is a ShallowRef whose EMPTY // Locks the vue API-surface sentinel: `activeId` is a ShallowRef whose EMPTY
// value is `null` (Ink's equivalent is `undefined`). See ink-divergences.md // value is `null` (Ink's equivalent is `undefined`). See ink-divergences.md
// ("`useFocusManager().activeId` empty value is `null`, not `undefined`"). // ("`useFocusManager().activeId` empty value is `null`, not `undefined`").
+6 -6
View File
@@ -12,7 +12,7 @@ import { FocusContextKey, StdinContextKey } from "../context.ts";
let nextAutoId = 0; let nextAutoId = 0;
export interface UseFocusOptions { export interface UseFocusOptions {
autoFocus?: boolean; autoFocus?: MaybeRefOrGetter<boolean>;
isActive?: MaybeRefOrGetter<boolean>; isActive?: MaybeRefOrGetter<boolean>;
id?: MaybeRefOrGetter<string>; id?: MaybeRefOrGetter<string>;
} }
@@ -67,11 +67,11 @@ export function useFocus(options: UseFocusOptions = {}): {
currentId = undefined; currentId = undefined;
}; };
const register = (id: string) => { const register = (id: string, autoFocus: boolean) => {
unsubscribe = ctx.subscribe(id, (v) => { unsubscribe = ctx.subscribe(id, (v) => {
isFocused.value = v; isFocused.value = v;
}); });
ctx.add(id, { autoFocus: options.autoFocus }); ctx.add(id, { autoFocus });
currentId = id; currentId = id;
// Apply the current active state to the freshly-registered id. // Apply the current active state to the freshly-registered id.
if (toValue(isActive)) { if (toValue(isActive)) {
@@ -83,11 +83,11 @@ export function useFocus(options: UseFocusOptions = {}): {
}; };
watch( watch(
() => toValue(options.id) ?? fallbackId, () => [toValue(options.id) ?? fallbackId, toValue(options.autoFocus ?? false)] as const,
(id) => { ([id, autoFocus]) => {
unregister(); unregister();
isFocused.value = false; isFocused.value = false;
register(id); register(id, autoFocus);
}, },
{ immediate: true, flush: "sync" }, { immediate: true, flush: "sync" },
); );