Skip to content

Commit a2414a4

Browse files
committed
feat(use-focus): adds focusWithin flag to useFocus hook
This adds an option to add onfocusin and onfocusout event handlers to the focus hook.
1 parent e713eba commit a2414a4

File tree

2 files changed

+62
-2
lines changed

2 files changed

+62
-2
lines changed

packages/floating-ui-svelte/src/hooks/use-focus.svelte.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getWindow, isElement, isHTMLElement } from "@floating-ui/utils/dom";
2+
import type { HTMLAttributes } from "svelte/elements";
23
import {
34
activeElement,
45
contains,
@@ -25,6 +26,12 @@ interface UseFocusOptions {
2526
* @default true
2627
*/
2728
visibleOnly?: boolean;
29+
/**
30+
* Whether the open state should change when focusing within the trigger element.
31+
*
32+
* @default false
33+
*/
34+
focusWithin?: boolean;
2835
}
2936

3037
function useFocus(context: FloatingContext, options: UseFocusOptions = {}) {
@@ -35,7 +42,11 @@ function useFocus(context: FloatingContext, options: UseFocusOptions = {}) {
3542
elements: { reference, floating },
3643
} = $derived(context);
3744

38-
const { enabled = true, visibleOnly = true } = $derived(options);
45+
const {
46+
enabled = true,
47+
visibleOnly = true,
48+
focusWithin = false,
49+
} = $derived(options);
3950

4051
let blockFocus = false;
4152
let timeout = -1;
@@ -101,7 +112,7 @@ function useFocus(context: FloatingContext, options: UseFocusOptions = {}) {
101112
if (!enabled) {
102113
return {};
103114
}
104-
return {
115+
const handlers: HTMLAttributes<HTMLElement> = {
105116
onpointerdown: (event: PointerEvent) => {
106117
if (isVirtualPointerEvent(event)) return;
107118
keyboardModality = false;
@@ -174,6 +185,13 @@ function useFocus(context: FloatingContext, options: UseFocusOptions = {}) {
174185
});
175186
},
176187
};
188+
189+
if (focusWithin) {
190+
handlers.onfocusin = handlers.onfocus;
191+
handlers.onfocusout = handlers.onblur;
192+
}
193+
194+
return handlers;
177195
},
178196
};
179197
}

packages/floating-ui-svelte/test/hooks/use-focus.ts

+42
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { fireEvent, render, screen } from "@testing-library/svelte";
22
import { userEvent } from "@testing-library/user-event";
33
import { describe, expect, it, vi } from "vitest";
4+
import { useFloating, useFocus, useId } from "../../src/index.js";
5+
import { withRunes } from "../internal/with-runes.svelte.js";
46
import App from "./wrapper-components/use-focus.svelte";
57

68
/**
@@ -102,3 +104,43 @@ describe.skip("useFocus", () => {
102104
});
103105
});
104106
});
107+
108+
describe("focusWithin", () => {
109+
function createElements(): { reference: HTMLElement; floating: HTMLElement } {
110+
const reference = document.createElement("div");
111+
const floating = document.createElement("div");
112+
reference.id = useId();
113+
floating.id = useId();
114+
return { reference, floating };
115+
}
116+
117+
it(
118+
"Should not output onfocusin or onfocusout event handlers",
119+
withRunes(() => {
120+
expect.assertions(2);
121+
122+
const elements = createElements();
123+
const floating = useFloating({ elements });
124+
const focus = useFocus(floating.context);
125+
const handlers = focus.reference;
126+
127+
expect(handlers).not.toHaveProperty("onfocusin");
128+
expect(handlers).not.toHaveProperty("onfocusout");
129+
}),
130+
);
131+
132+
it(
133+
"Should output onfocusin or onfocusout event handlers",
134+
withRunes(() => {
135+
expect.assertions(2);
136+
137+
const elements = createElements();
138+
const floating = useFloating({ elements });
139+
const focus = useFocus(floating.context, { focusWithin: true });
140+
const handlers = focus.reference;
141+
142+
expect(handlers).toHaveProperty("onfocusin");
143+
expect(handlers).toHaveProperty("onfocusout");
144+
}),
145+
);
146+
});

0 commit comments

Comments
 (0)