From a2414a4167a219e438decea8fb49aff0ad156281 Mon Sep 17 00:00:00 2001 From: Dinh-Van Colomban Date: Wed, 15 Jan 2025 08:37:59 +0100 Subject: [PATCH] feat(use-focus): adds focusWithin flag to useFocus hook This adds an option to add onfocusin and onfocusout event handlers to the focus hook. --- .../src/hooks/use-focus.svelte.ts | 22 +++++++++- .../test/hooks/use-focus.ts | 42 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/floating-ui-svelte/src/hooks/use-focus.svelte.ts b/packages/floating-ui-svelte/src/hooks/use-focus.svelte.ts index a3a87d84..74d11af7 100644 --- a/packages/floating-ui-svelte/src/hooks/use-focus.svelte.ts +++ b/packages/floating-ui-svelte/src/hooks/use-focus.svelte.ts @@ -1,4 +1,5 @@ import { getWindow, isElement, isHTMLElement } from "@floating-ui/utils/dom"; +import type { HTMLAttributes } from "svelte/elements"; import { activeElement, contains, @@ -25,6 +26,12 @@ interface UseFocusOptions { * @default true */ visibleOnly?: boolean; + /** + * Whether the open state should change when focusing within the trigger element. + * + * @default false + */ + focusWithin?: boolean; } function useFocus(context: FloatingContext, options: UseFocusOptions = {}) { @@ -35,7 +42,11 @@ function useFocus(context: FloatingContext, options: UseFocusOptions = {}) { elements: { reference, floating }, } = $derived(context); - const { enabled = true, visibleOnly = true } = $derived(options); + const { + enabled = true, + visibleOnly = true, + focusWithin = false, + } = $derived(options); let blockFocus = false; let timeout = -1; @@ -101,7 +112,7 @@ function useFocus(context: FloatingContext, options: UseFocusOptions = {}) { if (!enabled) { return {}; } - return { + const handlers: HTMLAttributes = { onpointerdown: (event: PointerEvent) => { if (isVirtualPointerEvent(event)) return; keyboardModality = false; @@ -174,6 +185,13 @@ function useFocus(context: FloatingContext, options: UseFocusOptions = {}) { }); }, }; + + if (focusWithin) { + handlers.onfocusin = handlers.onfocus; + handlers.onfocusout = handlers.onblur; + } + + return handlers; }, }; } diff --git a/packages/floating-ui-svelte/test/hooks/use-focus.ts b/packages/floating-ui-svelte/test/hooks/use-focus.ts index e30c4161..960dddb8 100644 --- a/packages/floating-ui-svelte/test/hooks/use-focus.ts +++ b/packages/floating-ui-svelte/test/hooks/use-focus.ts @@ -1,6 +1,8 @@ import { fireEvent, render, screen } from "@testing-library/svelte"; import { userEvent } from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; +import { useFloating, useFocus, useId } from "../../src/index.js"; +import { withRunes } from "../internal/with-runes.svelte.js"; import App from "./wrapper-components/use-focus.svelte"; /** @@ -102,3 +104,43 @@ describe.skip("useFocus", () => { }); }); }); + +describe("focusWithin", () => { + function createElements(): { reference: HTMLElement; floating: HTMLElement } { + const reference = document.createElement("div"); + const floating = document.createElement("div"); + reference.id = useId(); + floating.id = useId(); + return { reference, floating }; + } + + it( + "Should not output onfocusin or onfocusout event handlers", + withRunes(() => { + expect.assertions(2); + + const elements = createElements(); + const floating = useFloating({ elements }); + const focus = useFocus(floating.context); + const handlers = focus.reference; + + expect(handlers).not.toHaveProperty("onfocusin"); + expect(handlers).not.toHaveProperty("onfocusout"); + }), + ); + + it( + "Should output onfocusin or onfocusout event handlers", + withRunes(() => { + expect.assertions(2); + + const elements = createElements(); + const floating = useFloating({ elements }); + const focus = useFocus(floating.context, { focusWithin: true }); + const handlers = focus.reference; + + expect(handlers).toHaveProperty("onfocusin"); + expect(handlers).toHaveProperty("onfocusout"); + }), + ); +});