add event order control to KeyEventListener

This commit is contained in:
Jakob Pinterits
2025-11-24 12:41:10 +01:00
parent 72460013a7
commit df6f75b6f6
3 changed files with 186 additions and 126 deletions

View File

@@ -10,9 +10,7 @@ Changes:
event handlers event handlers
- `TextStyle` no longer has default values, omitted values are left unchanged - `TextStyle` no longer has default values, omitted values are left unchanged
instead instead
Deprecations: Deprecations:
- The `CursorStyle` enum is deprecated in favor of string literals - The `CursorStyle` enum is deprecated in favor of string literals
Bugfixes: Bugfixes:
@@ -24,6 +22,9 @@ Additions:
- `NumberInput` can now evaluate math expressions - `NumberInput` can now evaluate math expressions
- `PointerEventListener` can now listen to only specific button events - `PointerEventListener` can now listen to only specific button events
- `PointerEventListener` and `KeyEventListener` now support the `event_order`
parameter to control whether events are handled before or after child
components
- `KeyEventListener` can now listen to only specifc hotkeys - `KeyEventListener` can now listen to only specifc hotkeys
- `Calendar` now has a `is_sensitive` parameter - `Calendar` now has a `is_sensitive` parameter
- New component: `rio.PdfViewer` - New component: `rio.PdfViewer`

View File

@@ -697,12 +697,15 @@ export type KeyEventListenerState = KeyboardFocusableComponentState & {
reportKeyDown: KeyCombination[] | true; reportKeyDown: KeyCombination[] | true;
reportKeyUp: KeyCombination[] | true; reportKeyUp: KeyCombination[] | true;
reportKeyPress: KeyCombination[] | true; reportKeyPress: KeyCombination[] | true;
event_order: "before-child" | "after-child";
}; };
export class KeyEventListenerComponent extends KeyboardFocusableComponent<KeyEventListenerState> { export class KeyEventListenerComponent extends KeyboardFocusableComponent<KeyEventListenerState> {
private keyDownCombinations: Set<string> | true; private keyDownCombinations: Set<string> | true;
private keyUpCombinations: Set<string> | true; private keyUpCombinations: Set<string> | true;
private keyPressCombinations: Set<string> | true; private keyPressCombinations: Set<string> | true;
private onKeyDownBound: ((e: KeyboardEvent) => void) | null = null;
private onKeyUpBound: ((e: KeyboardEvent) => void) | null = null;
createElement(context: ComponentStatesUpdateContext): HTMLElement { createElement(context: ComponentStatesUpdateContext): HTMLElement {
let element = document.createElement("div"); let element = document.createElement("div");
@@ -738,30 +741,54 @@ export class KeyEventListenerComponent extends KeyboardFocusableComponent<KeyEve
); );
} }
// Check if we need to update event listeners
if (
deltaState.reportKeyDown !== undefined ||
deltaState.reportKeyPress !== undefined ||
deltaState.event_order !== undefined
) {
let reportKeyDown = let reportKeyDown =
this.keyDownCombinations === true || this.keyDownCombinations === true ||
this.keyDownCombinations.size > 0; this.keyDownCombinations.size > 0;
let reportKeyUp =
this.keyUpCombinations === true || this.keyUpCombinations.size > 0;
let reportKeyPress = let reportKeyPress =
this.keyPressCombinations === true || this.keyPressCombinations === true ||
this.keyPressCombinations.size > 0; this.keyPressCombinations.size > 0;
let eventOrder = deltaState.event_order ?? this.state.event_order;
if (reportKeyDown || reportKeyPress) { this.onKeyDownBound = this._updateEventListener(
this.element.onkeydown = (e: KeyboardEvent) => { "keydown",
this.handleKeyEvent(e, "KeyPress", this.keyPressCombinations); reportKeyDown || reportKeyPress,
eventOrder,
this.onKeyDownBound,
(e: KeyboardEvent) => {
this.handleKeyEvent(
e,
"KeyPress",
this.keyPressCombinations
);
this.handleKeyEvent(e, "KeyDown", this.keyDownCombinations); this.handleKeyEvent(e, "KeyDown", this.keyDownCombinations);
}; }
} else { );
this.element.onkeydown = null;
} }
if (reportKeyUp) { if (
this.element.onkeyup = (e: KeyboardEvent) => { deltaState.reportKeyUp !== undefined ||
deltaState.event_order !== undefined
) {
let reportKeyUp =
this.keyUpCombinations === true ||
this.keyUpCombinations.size > 0;
let eventOrder = deltaState.event_order ?? this.state.event_order;
this.onKeyUpBound = this._updateEventListener(
"keyup",
reportKeyUp,
eventOrder,
this.onKeyUpBound,
(e: KeyboardEvent) => {
this.handleKeyEvent(e, "KeyUp", this.keyUpCombinations); this.handleKeyEvent(e, "KeyUp", this.keyUpCombinations);
}; }
} else { );
this.element.onkeyup = null;
} }
this.replaceOnlyChild(context, deltaState.content); this.replaceOnlyChild(context, deltaState.content);
@@ -804,6 +831,32 @@ export class KeyEventListenerComponent extends KeyboardFocusableComponent<KeyEve
...encodedEvent, ...encodedEvent,
}); });
} }
/// Helper method to manage event listeners with capture phase support
private _updateEventListener(
eventName: string,
shouldInstall: boolean,
eventOrder: "before-child" | "after-child",
currentHandler: ((e: KeyboardEvent) => void) | null,
callbackMethod: (e: KeyboardEvent) => void
): ((e: KeyboardEvent) => void) | null {
// Remove existing listener if it exists
if (currentHandler !== null) {
this.element.removeEventListener(eventName, currentHandler, {
capture: this.state.event_order === "before-child",
});
}
if (!shouldInstall) {
return null;
}
// Install new listener with current capture setting
this.element.addEventListener(eventName, callbackMethod, {
capture: eventOrder === "before-child",
});
return callbackMethod;
}
} }
function keyCombinationsSetFromDeltaState( function keyCombinationsSetFromDeltaState(

View File

@@ -679,6 +679,11 @@ class KeyEventListener(KeyboardFocusableFundamentalComponent):
`on_key_up`: A function to call when a key is released. `on_key_up`: A function to call when a key is released.
`on_key_press`: A function to call repeatedly while a key is held down. `on_key_press`: A function to call repeatedly while a key is held down.
`event_order`: Controls when this listener receives events relative to
its child components. When `"before-child"`, this listener's handlers
are called before any child component handlers. When `"after-child"`
(default), child components receive events first, then this listener.
""" """
content: rio.Component content: rio.Component
@@ -695,6 +700,7 @@ class KeyEventListener(KeyboardFocusableFundamentalComponent):
rio.EventHandler[KeyPressEvent] rio.EventHandler[KeyPressEvent]
| t.Mapping[KeyCombination, rio.EventHandler[KeyPressEvent]] | t.Mapping[KeyCombination, rio.EventHandler[KeyPressEvent]]
) = None ) = None
event_order: t.Literal["before-child", "after-child"] = "after-child"
def __post_init__(self): def __post_init__(self):
# TODO: These values are never updated, which is a problem if someone # TODO: These values are never updated, which is a problem if someone