Skip to content
This repository was archived by the owner on Oct 14, 2025. It is now read-only.

Commit ddfd345

Browse files
committed
click & dismiss
1 parent 6fcbef6 commit ddfd345

File tree

5 files changed

+689
-351
lines changed

5 files changed

+689
-351
lines changed

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

Lines changed: 126 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { isHTMLElement } from "@floating-ui/utils/dom";
22
import { isMouseLikePointerType } from "../internal/dom.js";
33
import { isTypeableElement } from "../internal/is-typeable-element.js";
4-
import type { ElementProps } from "./use-interactions.svelte.js";
54
import type { FloatingContext } from "./use-floating.svelte.js";
5+
import type { ReferenceType } from "../types.js";
66

77
interface UseClickOptions {
88
/**
@@ -48,131 +48,139 @@ function isButtonTarget(event: KeyboardEvent) {
4848
return isHTMLElement(event.target) && event.target.tagName === "BUTTON";
4949
}
5050

51-
function isSpaceIgnored(element: Element | null) {
51+
function isSpaceIgnored(element: ReferenceType | null) {
5252
return isTypeableElement(element);
5353
}
5454

55-
function useClick(
56-
context: FloatingContext,
57-
options: UseClickOptions = {},
58-
): ElementProps {
59-
const {
60-
enabled = true,
61-
event: eventOption = "click",
62-
toggle = true,
63-
ignoreMouse = false,
64-
keyboardHandlers = true,
65-
} = $derived(options);
66-
67-
let pointerType: PointerEvent["pointerType"] | undefined = undefined;
68-
let didKeyDown = false;
69-
70-
return {
71-
get reference() {
72-
if (!enabled) {
73-
return {};
74-
}
75-
return {
76-
onpointerdown: (event: PointerEvent) => {
77-
pointerType = event.pointerType;
78-
},
79-
onmousedown: (event: MouseEvent) => {
80-
if (event.button !== 0) {
81-
return;
82-
}
83-
84-
if (isMouseLikePointerType(pointerType, true) && ignoreMouse) {
85-
return;
86-
}
87-
88-
if (eventOption === "click") {
89-
return;
90-
}
91-
92-
if (
93-
context.open &&
94-
toggle &&
95-
(context.data.openEvent
96-
? context.data.openEvent.type === "mousedown"
97-
: true)
98-
) {
99-
context.onOpenChange(false, event, "click");
55+
class ClickState {
56+
#enabled = $derived.by(() => this.options.enabled ?? "true");
57+
#eventOption = $derived.by(() => this.options.event ?? "click");
58+
#toggle = $derived.by(() => this.options.toggle ?? true);
59+
#ignoreMouse = $derived.by(() => this.options.ignoreMouse ?? false);
60+
#keyboardHandlers = $derived.by(() => this.options.keyboardHandlers ?? true);
61+
#pointerType: PointerEvent["pointerType"] | undefined = undefined;
62+
#didKeyDown = false;
63+
64+
constructor(
65+
private readonly context: FloatingContext,
66+
private readonly options: UseClickOptions = {},
67+
) {}
68+
69+
get reference() {
70+
if (!this.#enabled) {
71+
return {};
72+
}
73+
return {
74+
onpointerdown: (event: PointerEvent) => {
75+
this.#pointerType = event.pointerType;
76+
},
77+
onmousedown: (event: MouseEvent) => {
78+
if (event.button !== 0) {
79+
return;
80+
}
81+
82+
if (
83+
isMouseLikePointerType(this.#pointerType, true) &&
84+
this.#ignoreMouse
85+
) {
86+
return;
87+
}
88+
89+
if (this.#eventOption === "click") {
90+
return;
91+
}
92+
93+
if (
94+
this.context.open &&
95+
this.#toggle &&
96+
(this.context.data.openEvent
97+
? this.context.data.openEvent.type === "mousedown"
98+
: true)
99+
) {
100+
this.context.onOpenChange(false, event, "click");
101+
} else {
102+
// Prevent stealing focus from the floating element
103+
event.preventDefault();
104+
this.context.onOpenChange(true, event, "click");
105+
}
106+
},
107+
onclick: (event: MouseEvent) => {
108+
if (this.#eventOption === "mousedown" && this.#pointerType) {
109+
this.#pointerType = undefined;
110+
return;
111+
}
112+
113+
if (
114+
isMouseLikePointerType(this.#pointerType, true) &&
115+
this.#ignoreMouse
116+
) {
117+
return;
118+
}
119+
120+
if (
121+
this.context.open &&
122+
this.#toggle &&
123+
(this.context.data.openEvent
124+
? this.context.data.openEvent.type === "click"
125+
: true)
126+
) {
127+
this.context.onOpenChange(false, event, "click");
128+
} else {
129+
this.context.onOpenChange(true, event, "click");
130+
}
131+
},
132+
onkeydown: (event: KeyboardEvent) => {
133+
this.#pointerType = undefined;
134+
135+
if (
136+
event.defaultPrevented ||
137+
!this.#keyboardHandlers ||
138+
isButtonTarget(event)
139+
) {
140+
return;
141+
}
142+
if (
143+
event.key === " " &&
144+
!isSpaceIgnored(this.context.elements.reference)
145+
) {
146+
// Prevent scrolling
147+
event.preventDefault();
148+
this.#didKeyDown = true;
149+
}
150+
151+
if (event.key === "Enter") {
152+
if (this.context.open && this.#toggle) {
153+
this.context.onOpenChange(false, event, "click");
100154
} else {
101-
// Prevent stealing focus from the floating element
102-
event.preventDefault();
103-
context.onOpenChange(true, event, "click");
104-
}
105-
},
106-
onclick: (event: MouseEvent) => {
107-
if (eventOption === "mousedown" && pointerType) {
108-
pointerType = undefined;
109-
return;
110-
}
111-
112-
if (isMouseLikePointerType(pointerType, true) && ignoreMouse) {
113-
return;
155+
this.context.onOpenChange(true, event, "click");
114156
}
115-
116-
if (
117-
context.open &&
118-
toggle &&
119-
(context.data.openEvent
120-
? context.data.openEvent.type === "click"
121-
: true)
122-
) {
123-
context.onOpenChange(false, event, "click");
157+
}
158+
},
159+
onkeyup: (event: KeyboardEvent) => {
160+
if (
161+
event.defaultPrevented ||
162+
!this.#keyboardHandlers ||
163+
isButtonTarget(event) ||
164+
isSpaceIgnored(this.context.elements.reference)
165+
) {
166+
return;
167+
}
168+
169+
if (event.key === " " && this.#didKeyDown) {
170+
this.#didKeyDown = false;
171+
if (this.context.open && this.#toggle) {
172+
this.context.onOpenChange(false, event, "click");
124173
} else {
125-
context.onOpenChange(true, event, "click");
126-
}
127-
},
128-
onkeydown: (event: KeyboardEvent) => {
129-
pointerType = undefined;
130-
131-
if (
132-
event.defaultPrevented ||
133-
!keyboardHandlers ||
134-
isButtonTarget(event)
135-
) {
136-
return;
137-
}
138-
// @ts-expect-error FIXME
139-
if (event.key === " " && !isSpaceIgnored(reference)) {
140-
// Prevent scrolling
141-
event.preventDefault();
142-
didKeyDown = true;
143-
}
144-
145-
if (event.key === "Enter") {
146-
if (context.open && toggle) {
147-
context.onOpenChange(false, event, "click");
148-
} else {
149-
context.onOpenChange(true, event, "click");
150-
}
151-
}
152-
},
153-
onkeyup: (event: KeyboardEvent) => {
154-
if (
155-
event.defaultPrevented ||
156-
!keyboardHandlers ||
157-
isButtonTarget(event) ||
158-
// @ts-expect-error FIXME
159-
isSpaceIgnored(reference)
160-
) {
161-
return;
174+
this.context.onOpenChange(true, event, "click");
162175
}
176+
}
177+
},
178+
};
179+
}
180+
}
163181

164-
if (event.key === " " && didKeyDown) {
165-
didKeyDown = false;
166-
if (context.open && toggle) {
167-
context.onOpenChange(false, event, "click");
168-
} else {
169-
context.onOpenChange(true, event, "click");
170-
}
171-
}
172-
},
173-
};
174-
},
175-
};
182+
function useClick(context: FloatingContext, options: UseClickOptions = {}) {
183+
return new ClickState(context, options);
176184
}
177185

178186
export type { UseClickOptions };

0 commit comments

Comments
 (0)