remove the default behavior change in the previous commit.

- Add capture_events parameter to PointerEventListener and MouseEventListener
- Install event listeners in capture phase when capture_events=True
This commit is contained in:
iap
2025-10-16 14:42:17 -04:00
parent 2c2b761b18
commit 1e30b14ae7
6 changed files with 446 additions and 269 deletions

View File

@@ -32,6 +32,7 @@ export type MouseEventListenerState = ComponentState & {
reportDragMove: boolean;
reportDragEnd: boolean;
consume_events: boolean;
capture_events: boolean;
};
export class MouseEventListenerComponent extends ComponentBase<MouseEventListenerState> {
@@ -58,166 +59,246 @@ export class MouseEventListenerComponent extends ComponentBase<MouseEventListene
this.replaceOnlyChild(context, deltaState.content);
if (deltaState.reportPress !== undefined) {
if (this.state.reportPress) {
if (this._onClickBound === null) {
this._onClickBound = (e: MouseEvent) => {
this._sendMessageToBackend(e, {
type: "press",
...eventMouseButtonToString(e),
...eventMousePositionToString(e),
});
};
this.element.addEventListener("click", this._onClickBound, {
capture: true,
if (
deltaState.reportPress !== undefined ||
deltaState.capture_events !== undefined
) {
const reportPress =
deltaState.reportPress ?? this.state.reportPress;
const captureEvents =
deltaState.capture_events ?? this.state.capture_events;
// Remove existing listener if it exists
if (this._onClickBound !== null) {
const oldOptions = this.state.capture_events
? { capture: true }
: {};
this.element.removeEventListener(
"click",
this._onClickBound,
oldOptions as AddEventListenerOptions
);
}
if (reportPress) {
// Install new listener with current capture setting
this._onClickBound = (e: MouseEvent) => {
this._sendMessageToBackend(e, {
type: "press",
...eventMouseButtonToString(e),
...eventMousePositionToString(e),
});
}
};
const options = captureEvents ? { capture: true } : {};
this.element.addEventListener(
"click",
this._onClickBound,
options
);
} else {
if (this._onClickBound !== null) {
this.element.removeEventListener(
"click",
this._onClickBound,
{ capture: true } as AddEventListenerOptions
);
this._onClickBound = null;
}
this._onClickBound = null;
}
}
if (deltaState.reportMouseDown !== undefined) {
if (this.state.reportMouseDown) {
if (this._onMouseDownBound === null) {
this._onMouseDownBound = (e: MouseEvent) => {
this._sendMessageToBackend(e, {
type: "mouseDown",
...eventMouseButtonToString(e),
...eventMousePositionToString(e),
});
};
this.element.addEventListener(
"mousedown",
this._onMouseDownBound,
{ capture: true }
);
}
if (
deltaState.reportMouseDown !== undefined ||
deltaState.capture_events !== undefined
) {
const reportMouseDown =
deltaState.reportMouseDown ?? this.state.reportMouseDown;
const captureEvents =
deltaState.capture_events ?? this.state.capture_events;
// Remove existing listener if it exists
if (this._onMouseDownBound !== null) {
const oldOptions = this.state.capture_events
? { capture: true }
: {};
this.element.removeEventListener(
"mousedown",
this._onMouseDownBound,
oldOptions as AddEventListenerOptions
);
}
if (reportMouseDown) {
// Install new listener with current capture setting
this._onMouseDownBound = (e: MouseEvent) => {
this._sendMessageToBackend(e, {
type: "mouseDown",
...eventMouseButtonToString(e),
...eventMousePositionToString(e),
});
};
const options = captureEvents ? { capture: true } : {};
this.element.addEventListener(
"mousedown",
this._onMouseDownBound,
options
);
} else {
if (this._onMouseDownBound !== null) {
this.element.removeEventListener(
"mousedown",
this._onMouseDownBound,
{ capture: true } as AddEventListenerOptions
);
this._onMouseDownBound = null;
}
this._onMouseDownBound = null;
}
}
if (deltaState.reportMouseUp !== undefined) {
if (this.state.reportMouseUp) {
if (this._onMouseUpBound === null) {
this._onMouseUpBound = (e: MouseEvent) => {
this._sendMessageToBackend(e, {
type: "mouseUp",
...eventMouseButtonToString(e),
...eventMousePositionToString(e),
});
};
this.element.addEventListener(
"mouseup",
this._onMouseUpBound,
{ capture: true }
);
}
if (
deltaState.reportMouseUp !== undefined ||
deltaState.capture_events !== undefined
) {
const reportMouseUp =
deltaState.reportMouseUp ?? this.state.reportMouseUp;
const captureEvents =
deltaState.capture_events ?? this.state.capture_events;
// Remove existing listener if it exists
if (this._onMouseUpBound !== null) {
const oldOptions = this.state.capture_events
? { capture: true }
: {};
this.element.removeEventListener(
"mouseup",
this._onMouseUpBound,
oldOptions as AddEventListenerOptions
);
}
if (reportMouseUp) {
// Install new listener with current capture setting
this._onMouseUpBound = (e: MouseEvent) => {
this._sendMessageToBackend(e, {
type: "mouseUp",
...eventMouseButtonToString(e),
...eventMousePositionToString(e),
});
};
const options = captureEvents ? { capture: true } : {};
this.element.addEventListener(
"mouseup",
this._onMouseUpBound,
options
);
} else {
if (this._onMouseUpBound !== null) {
this.element.removeEventListener(
"mouseup",
this._onMouseUpBound,
{ capture: true } as AddEventListenerOptions
);
this._onMouseUpBound = null;
}
this._onMouseUpBound = null;
}
}
if (deltaState.reportMouseMove !== undefined) {
if (this.state.reportMouseMove) {
if (this._onMouseMoveBound === null) {
this._onMouseMoveBound = (e: MouseEvent) => {
this._sendMessageToBackend(e, {
type: "mouseMove",
...eventMousePositionToString(e),
});
};
this.element.addEventListener(
"mousemove",
this._onMouseMoveBound,
{ capture: true }
);
}
if (
deltaState.reportMouseMove !== undefined ||
deltaState.capture_events !== undefined
) {
const reportMouseMove =
deltaState.reportMouseMove ?? this.state.reportMouseMove;
const captureEvents =
deltaState.capture_events ?? this.state.capture_events;
// Remove existing listener if it exists
if (this._onMouseMoveBound !== null) {
const oldOptions = this.state.capture_events
? { capture: true }
: {};
this.element.removeEventListener(
"mousemove",
this._onMouseMoveBound,
oldOptions as AddEventListenerOptions
);
}
if (reportMouseMove) {
// Install new listener with current capture setting
this._onMouseMoveBound = (e: MouseEvent) => {
this._sendMessageToBackend(e, {
type: "mouseMove",
...eventMousePositionToString(e),
});
};
const options = captureEvents ? { capture: true } : {};
this.element.addEventListener(
"mousemove",
this._onMouseMoveBound,
options
);
} else {
if (this._onMouseMoveBound !== null) {
this.element.removeEventListener(
"mousemove",
this._onMouseMoveBound,
{ capture: true } as AddEventListenerOptions
);
this._onMouseMoveBound = null;
}
this._onMouseMoveBound = null;
}
}
if (deltaState.reportMouseEnter !== undefined) {
if (this.state.reportMouseEnter) {
if (this._onMouseEnterBound === null) {
this._onMouseEnterBound = (e: MouseEvent) => {
this._sendMessageToBackend(e, {
type: "mouseEnter",
...eventMousePositionToString(e),
});
};
this.element.addEventListener(
"mouseenter",
this._onMouseEnterBound,
{ capture: true }
);
}
if (
deltaState.reportMouseEnter !== undefined ||
deltaState.capture_events !== undefined
) {
const reportMouseEnter =
deltaState.reportMouseEnter ?? this.state.reportMouseEnter;
const captureEvents =
deltaState.capture_events ?? this.state.capture_events;
// Remove existing listener if it exists
if (this._onMouseEnterBound !== null) {
const oldOptions = this.state.capture_events
? { capture: true }
: {};
this.element.removeEventListener(
"mouseenter",
this._onMouseEnterBound,
oldOptions as AddEventListenerOptions
);
}
if (reportMouseEnter) {
// Install new listener with current capture setting
this._onMouseEnterBound = (e: MouseEvent) => {
this._sendMessageToBackend(e, {
type: "mouseEnter",
...eventMousePositionToString(e),
});
};
const options = captureEvents ? { capture: true } : {};
this.element.addEventListener(
"mouseenter",
this._onMouseEnterBound,
options
);
} else {
if (this._onMouseEnterBound !== null) {
this.element.removeEventListener(
"mouseenter",
this._onMouseEnterBound,
{ capture: true } as AddEventListenerOptions
);
this._onMouseEnterBound = null;
}
this._onMouseEnterBound = null;
}
}
if (deltaState.reportMouseLeave !== undefined) {
if (this.state.reportMouseLeave) {
if (this._onMouseLeaveBound === null) {
this._onMouseLeaveBound = (e: MouseEvent) => {
this._sendMessageToBackend(e, {
type: "mouseLeave",
...eventMousePositionToString(e),
});
};
this.element.addEventListener(
"mouseleave",
this._onMouseLeaveBound,
{ capture: true }
);
}
if (
deltaState.reportMouseLeave !== undefined ||
deltaState.capture_events !== undefined
) {
const reportMouseLeave =
deltaState.reportMouseLeave ?? this.state.reportMouseLeave;
const captureEvents =
deltaState.capture_events ?? this.state.capture_events;
// Remove existing listener if it exists
if (this._onMouseLeaveBound !== null) {
const oldOptions = this.state.capture_events
? { capture: true }
: {};
this.element.removeEventListener(
"mouseleave",
this._onMouseLeaveBound,
oldOptions as AddEventListenerOptions
);
}
if (reportMouseLeave) {
// Install new listener with current capture setting
this._onMouseLeaveBound = (e: MouseEvent) => {
this._sendMessageToBackend(e, {
type: "mouseLeave",
...eventMousePositionToString(e),
});
};
const options = captureEvents ? { capture: true } : {};
this.element.addEventListener(
"mouseleave",
this._onMouseLeaveBound,
options
);
} else {
if (this._onMouseLeaveBound !== null) {
this.element.removeEventListener(
"mouseleave",
this._onMouseLeaveBound,
{ capture: true } as AddEventListenerOptions
);
this._onMouseLeaveBound = null;
}
this._onMouseLeaveBound = null;
}
}

View File

@@ -20,6 +20,7 @@ export type PointerEventListenerState = ComponentState & {
reportDragMove: boolean;
reportDragEnd: boolean;
consume_events: boolean;
capture_events: boolean;
};
const DOUBLE_CLICK_TIMEOUT = 300;
@@ -53,154 +54,228 @@ export class PointerEventListenerComponent extends ComponentBase<PointerEventLis
if (
deltaState.reportPress !== undefined ||
deltaState.reportDoublePress !== undefined
deltaState.reportDoublePress !== undefined ||
deltaState.capture_events !== undefined
) {
let reportPress = deltaState.reportPress ?? this.state.reportPress;
let reportDoublePress =
const reportPress =
deltaState.reportPress ?? this.state.reportPress;
const reportDoublePress =
deltaState.reportDoublePress ?? this.state.reportDoublePress;
const captureEvents =
deltaState.capture_events ?? this.state.capture_events;
// Remove existing listener if it exists
if (this._onClickBound !== null) {
const oldOptions = this.state.capture_events
? { capture: true }
: {};
this.element.removeEventListener(
"click",
this._onClickBound,
oldOptions as AddEventListenerOptions
);
}
if (reportPress || reportDoublePress) {
if (this._onClickBound === null) {
this._onClickBound = this._onClick.bind(this);
this.element.addEventListener("click", this._onClickBound, {
capture: true,
});
}
// Install new listener with current capture setting
this._onClickBound = this._onClick.bind(this);
const options = captureEvents ? { capture: true } : {};
this.element.addEventListener(
"click",
this._onClickBound,
options
);
} else {
if (this._onClickBound !== null) {
this.element.removeEventListener(
"click",
this._onClickBound,
{ capture: true } as AddEventListenerOptions
);
this._onClickBound = null;
}
this._onClickBound = null;
}
}
if (deltaState.reportPointerDown !== undefined) {
if ((this.state.reportPointerDown?.length ?? 0) > 0) {
if (this._onPointerDownBound === null) {
this._onPointerDownBound = (e: PointerEvent) => {
if (
eventMatchesButton(e, this.state.reportPointerDown)
) {
this._sendEventToBackend("pointerDown", e, false);
}
};
this.element.addEventListener(
"pointerdown",
this._onPointerDownBound,
{ capture: true }
);
}
if (
deltaState.reportPointerDown !== undefined ||
deltaState.capture_events !== undefined
) {
const reportPointerDown =
deltaState.reportPointerDown ?? this.state.reportPointerDown;
const captureEvents =
deltaState.capture_events ?? this.state.capture_events;
// Remove existing listener if it exists
if (this._onPointerDownBound !== null) {
const oldOptions = this.state.capture_events
? { capture: true }
: {};
this.element.removeEventListener(
"pointerdown",
this._onPointerDownBound,
oldOptions as AddEventListenerOptions
);
}
if ((reportPointerDown?.length ?? 0) > 0) {
// Install new listener with current capture setting
this._onPointerDownBound = (e: PointerEvent) => {
if (eventMatchesButton(e, reportPointerDown)) {
this._sendEventToBackend("pointerDown", e, false);
}
};
const options = captureEvents ? { capture: true } : {};
this.element.addEventListener(
"pointerdown",
this._onPointerDownBound,
options
);
} else {
if (this._onPointerDownBound !== null) {
this.element.removeEventListener(
"pointerdown",
this._onPointerDownBound,
{ capture: true } as AddEventListenerOptions
);
this._onPointerDownBound = null;
}
this._onPointerDownBound = null;
}
}
if (deltaState.reportPointerUp !== undefined) {
if ((this.state.reportPointerUp?.length ?? 0) > 0) {
if (this._onPointerUpBound === null) {
this._onPointerUpBound = (e: PointerEvent) => {
if (eventMatchesButton(e, this.state.reportPointerUp)) {
this._sendEventToBackend("pointerUp", e, false);
}
};
this.element.addEventListener(
"pointerup",
this._onPointerUpBound,
{ capture: true }
);
}
if (
deltaState.reportPointerUp !== undefined ||
deltaState.capture_events !== undefined
) {
const reportPointerUp =
deltaState.reportPointerUp ?? this.state.reportPointerUp;
const captureEvents =
deltaState.capture_events ?? this.state.capture_events;
// Remove existing listener if it exists
if (this._onPointerUpBound !== null) {
const oldOptions = this.state.capture_events
? { capture: true }
: {};
this.element.removeEventListener(
"pointerup",
this._onPointerUpBound,
oldOptions as AddEventListenerOptions
);
}
if ((reportPointerUp?.length ?? 0) > 0) {
// Install new listener with current capture setting
this._onPointerUpBound = (e: PointerEvent) => {
if (eventMatchesButton(e, reportPointerUp)) {
this._sendEventToBackend("pointerUp", e, false);
}
};
const options = captureEvents ? { capture: true } : {};
this.element.addEventListener(
"pointerup",
this._onPointerUpBound,
options
);
} else {
if (this._onPointerUpBound !== null) {
this.element.removeEventListener(
"pointerup",
this._onPointerUpBound,
{ capture: true } as AddEventListenerOptions
);
this._onPointerUpBound = null;
}
this._onPointerUpBound = null;
}
}
if (deltaState.reportPointerMove !== undefined) {
if (this.state.reportPointerMove) {
if (this._onPointerMoveBound === null) {
this._onPointerMoveBound = (e: PointerEvent) => {
this._sendEventToBackend("pointerMove", e, true);
};
this.element.addEventListener(
"pointermove",
this._onPointerMoveBound,
{ capture: true }
);
}
if (
deltaState.reportPointerMove !== undefined ||
deltaState.capture_events !== undefined
) {
const reportPointerMove =
deltaState.reportPointerMove ?? this.state.reportPointerMove;
const captureEvents =
deltaState.capture_events ?? this.state.capture_events;
// Remove existing listener if it exists
if (this._onPointerMoveBound !== null) {
const oldOptions = this.state.capture_events
? { capture: true }
: {};
this.element.removeEventListener(
"pointermove",
this._onPointerMoveBound,
oldOptions as AddEventListenerOptions
);
}
if (reportPointerMove) {
// Install new listener with current capture setting
this._onPointerMoveBound = (e: PointerEvent) => {
this._sendEventToBackend("pointerMove", e, true);
};
const options = captureEvents ? { capture: true } : {};
this.element.addEventListener(
"pointermove",
this._onPointerMoveBound,
options
);
} else {
if (this._onPointerMoveBound !== null) {
this.element.removeEventListener(
"pointermove",
this._onPointerMoveBound,
{ capture: true } as AddEventListenerOptions
);
this._onPointerMoveBound = null;
}
this._onPointerMoveBound = null;
}
}
if (deltaState.reportPointerEnter !== undefined) {
if (this.state.reportPointerEnter) {
if (this._onPointerEnterBound === null) {
this._onPointerEnterBound = (e: PointerEvent) => {
this._sendEventToBackend("pointerEnter", e, false);
};
this.element.addEventListener(
"pointerenter",
this._onPointerEnterBound,
{ capture: true }
);
}
if (
deltaState.reportPointerEnter !== undefined ||
deltaState.capture_events !== undefined
) {
const reportPointerEnter =
deltaState.reportPointerEnter ?? this.state.reportPointerEnter;
const captureEvents =
deltaState.capture_events ?? this.state.capture_events;
// Remove existing listener if it exists
if (this._onPointerEnterBound !== null) {
const oldOptions = this.state.capture_events
? { capture: true }
: {};
this.element.removeEventListener(
"pointerenter",
this._onPointerEnterBound,
oldOptions as AddEventListenerOptions
);
}
if (reportPointerEnter) {
// Install new listener with current capture setting
this._onPointerEnterBound = (e: PointerEvent) => {
this._sendEventToBackend("pointerEnter", e, false);
};
const options = captureEvents ? { capture: true } : {};
this.element.addEventListener(
"pointerenter",
this._onPointerEnterBound,
options
);
} else {
if (this._onPointerEnterBound !== null) {
this.element.removeEventListener(
"pointerenter",
this._onPointerEnterBound,
{ capture: true } as AddEventListenerOptions
);
this._onPointerEnterBound = null;
}
this._onPointerEnterBound = null;
}
}
if (deltaState.reportPointerLeave !== undefined) {
if (this.state.reportPointerLeave) {
if (this._onPointerLeaveBound === null) {
this._onPointerLeaveBound = (e: PointerEvent) => {
this._sendEventToBackend("pointerLeave", e, false);
};
this.element.addEventListener(
"pointerleave",
this._onPointerLeaveBound,
{ capture: true }
);
}
if (
deltaState.reportPointerLeave !== undefined ||
deltaState.capture_events !== undefined
) {
const reportPointerLeave =
deltaState.reportPointerLeave ?? this.state.reportPointerLeave;
const captureEvents =
deltaState.capture_events ?? this.state.capture_events;
// Remove existing listener if it exists
if (this._onPointerLeaveBound !== null) {
const oldOptions = this.state.capture_events
? { capture: true }
: {};
this.element.removeEventListener(
"pointerleave",
this._onPointerLeaveBound,
oldOptions as AddEventListenerOptions
);
}
if (reportPointerLeave) {
// Install new listener with current capture setting
this._onPointerLeaveBound = (e: PointerEvent) => {
this._sendEventToBackend("pointerLeave", e, false);
};
const options = captureEvents ? { capture: true } : {};
this.element.addEventListener(
"pointerleave",
this._onPointerLeaveBound,
options
);
} else {
if (this._onPointerLeaveBound !== null) {
this.element.removeEventListener(
"pointerleave",
this._onPointerLeaveBound,
{ capture: true } as AddEventListenerOptions
);
this._onPointerLeaveBound = null;
}
this._onPointerLeaveBound = null;
}
}

View File

@@ -249,7 +249,13 @@ class MouseEventListener(FundamentalComponent):
`on_drag_end`: Triggered when the user stops dragging the mouse.
`consume_events`: Consume events after they are processed.
`consume_events`: If True, prevents the event from reaching other components
after this listener processes it. The default is False.
`capture_events`: Controls when this listener receives events relative to
its child components. When True, this listener's handlers are called
before any child component handlers. When False (default), child
components receive events first, then this listener.
"""
content: rio.Component
@@ -264,6 +270,7 @@ class MouseEventListener(FundamentalComponent):
on_drag_move: rio.EventHandler[DragMoveEvent] = None
on_drag_end: rio.EventHandler[DragEndEvent] = None
consume_events: bool = False
capture_events: bool = False
def __post_init__(self) -> None:
deprecations.warn(

View File

@@ -172,7 +172,13 @@ class PointerEventListener(FundamentalComponent):
`on_drag_end`: Triggered when the user stops dragging the pointer.
`consume_events`: Consume events after they are processed.
`consume_events`: If True, prevents the event from reaching other components
after this listener processes it.
`capture_events`: Controls when this listener receives events relative to
its child components. When True, this listener's handlers are called
before any child component handlers. When False (default), child
components receive events first, then this listener.
"""
content: rio.Component
@@ -194,6 +200,7 @@ class PointerEventListener(FundamentalComponent):
on_drag_move: rio.EventHandler[PointerMoveEvent] = None
on_drag_end: rio.EventHandler[PointerEvent] = None
consume_events: bool = True
capture_events: bool = False
def _custom_serialize_(self) -> JsonDoc:
return {

View File

@@ -5,7 +5,10 @@ from rio.testing import BrowserClient
@pytest.mark.parametrize("consume_events", [True, False])
async def test_specific_button_events(consume_events: bool) -> None:
@pytest.mark.parametrize("capture_events", [True, False])
async def test_specific_button_events(
consume_events: bool, capture_events: bool
) -> None:
down_events: list[rio.MouseDownEvent] = []
up_events: list[rio.MouseUpEvent] = []
@@ -25,10 +28,11 @@ async def test_specific_button_events(consume_events: bool) -> None:
on_mouse_down=on_mouse_down,
on_mouse_up=on_mouse_up,
consume_events=consume_events,
capture_events=capture_events,
)
async with BrowserClient(build) as client:
await client.click(0.5, 0.5, sleep=0.5)
assert len(down_events) == 1 + (not consume_events)
assert len(up_events) == 1 + (not consume_events)
assert len(down_events) == 1 + (not capture_events or not consume_events)
assert len(up_events) == 1 + (not capture_events or not consume_events)

View File

@@ -43,7 +43,7 @@ async def test_on_double_press_event(
def build():
return rio.PointerEventListener(
rio.TextInput(), on_double_press=on_double_press
rio.Spacer(), on_double_press=on_double_press
)
async with BrowserClient(build) as client:
@@ -71,10 +71,12 @@ async def test_on_double_press_event(
)
@pytest.mark.parametrize("pressed_button", ["left", "middle", "right"])
@pytest.mark.parametrize("consume_events", [True, False])
@pytest.mark.parametrize("capture_events", [True, False])
async def test_specific_button_events(
event_buttons: t.Sequence[t.Literal["left", "middle", "right"]],
pressed_button: t.Literal["left", "middle", "right"],
consume_events: bool,
capture_events: bool,
) -> None:
down_events: list[rio.PointerEvent] = []
up_events: list[rio.PointerEvent] = []
@@ -101,14 +103,15 @@ async def test_specific_button_events(
},
on_pointer_up={button: on_pointer_up for button in event_buttons},
consume_events=consume_events,
capture_events=capture_events,
)
async with BrowserClient(build) as client:
await client.click(0.5, 0.5, button=pressed_button, sleep=0.5)
if pressed_button in event_buttons:
assert len(down_events) == 1 + (not consume_events)
assert len(up_events) == 1 + (not consume_events)
assert len(down_events) == 1 + (capture_events and not consume_events)
assert len(up_events) == 1 + (capture_events and not consume_events)
else:
assert len(down_events) == 0
assert len(up_events) == 0