mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(core): prevent slider track click from closing popover (#776)
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
Reference in New Issue
Block a user