fix(core): prevent slider track click from closing popover (#776)

This commit is contained in:
rahim
2026-03-09 11:51:47 -07:00
committed by GitHub
parent f6a4fc7887
commit c20d884932
3 changed files with 44 additions and 0 deletions
@@ -35,6 +35,8 @@ export interface PopoverTriggerProps {
export interface PopoverPopupProps {
onPointerEnter: (event: UIPointerEvent) => void;
onPointerLeave: (event: UIPointerEvent) => void;
onGotPointerCapture: (event: UIPointerEvent) => void;
onLostPointerCapture: (event: UIPointerEvent) => void;
onFocusOut: (event: UIFocusEvent) => void;
}
@@ -56,6 +58,7 @@ export function createPopover(options: PopoverOptions): PopoverApi {
let triggerEl: HTMLElement | null = null;
let popupEl: HTMLElement | null = null;
let hoverTimeout: ReturnType<typeof setTimeout> | null = null;
const capturedPointers = new Set<number>();
const layer = createDismissLayer({
transition: options.transition,
@@ -154,6 +157,7 @@ export function createPopover(options: PopoverOptions): PopoverApi {
// Cleanup hover timeout on destroy.
layer.signal.addEventListener('abort', () => {
clearHoverTimeout();
capturedPointers.clear();
triggerEl = null;
popupEl = null;
});
@@ -227,6 +231,10 @@ export function createPopover(options: PopoverOptions): PopoverApi {
onPointerLeave(_event) {
if (!options.openOnHover?.()) return;
// A descendant has pointer capture (e.g. slider drag). The leave is
// synthetic — the pointer hasn't actually left — so don't close.
if (capturedPointers.size > 0) return;
clearHoverTimeout();
if (!state.current.active) return;
@@ -235,6 +243,14 @@ export function createPopover(options: PopoverOptions): PopoverApi {
hoverTimeout = setTimeout(() => applyClose('hover'), closeDelay);
},
onGotPointerCapture(event) {
capturedPointers.add(event.pointerId);
},
onLostPointerCapture(event) {
capturedPointers.delete(event.pointerId);
},
onFocusOut(event) {
const relatedTarget = event.relatedTarget as Node | null;
+6
View File
@@ -131,6 +131,12 @@ export function createSlider(options: SliderOptions): SliderApi {
onPointerDown(event) {
if (options.isDisabled()) return;
// Prevent the browser's default mousedown focus behavior. Without this,
// clicking a non-focusable child (e.g. the track) causes the browser to
// move focus away from the thumb after our programmatic `focus()` call,
// which can trigger unrelated `focusout` handlers (e.g. popover close).
event.preventDefault();
const el = options.getElement();
cachedRect = el.getBoundingClientRect();
@@ -178,6 +178,28 @@ describe('createSlider', () => {
slider.destroy();
});
it('calls preventDefault to suppress default focus behavior', () => {
const slider = createSlider(createOptions());
const event = pointerEvent();
slider.rootProps.onPointerDown(event);
expect(event.preventDefault).toHaveBeenCalled();
slider.destroy();
});
it('does not call preventDefault when disabled', () => {
const slider = createSlider(createOptions({ isDisabled: () => true }));
const event = pointerEvent();
slider.rootProps.onPointerDown(event);
expect(event.preventDefault).not.toHaveBeenCalled();
slider.destroy();
});
it('does nothing when disabled', () => {
const onValueChange = vi.fn();
const slider = createSlider(createOptions({ isDisabled: () => true, onValueChange }));