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

@@ -4,171 +4,172 @@
Changes:
- Components that depend on session attributes (like `active_page_url`) or
- Components that depend on session attributes (like `active_page_url`) or
session attachments are now automatically rebuilt when those values change
- Components are now rebuilt immediately after a state change, not just after
- Components are now rebuilt immediately after a state change, not just after
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
Deprecations:
- The `CursorStyle` enum is deprecated in favor of string literals
Deprecations:
- The `CursorStyle` enum is deprecated in favor of string literals
Bugfixes:
- Fix `rio.Revealer` not stretching its child
- Fix various components propagating click events
- Fix `rio.Revealer` not stretching its child
- Fix various components propagating click events
Additions:
- `NumberInput` can now evaluate math expressions
- `PointerEventListener` can now listen to only specific button events
- `KeyEventListener` can now listen to only specifc hotkeys
- `Calendar` now has a `is_sensitive` parameter
- New component: `rio.PdfViewer`
- `rio.Image` now has a loading animation
- Added `accessibility_role` parameter to all components
- Added 'accessibility_relationship' parameter to `Link` component
- Added `auto_focus` parameter to input components
- Added `Session.pick_folder`
- Added `Font.from_google_fonts` and `Font.from_css_file`
- Added `rio.HttpOnly`
- Added `text_style` parameter to `rio.TextInput`
- Experimental additions:
- `rio.List`
- `rio.Dict`
- `rio.Set`
- `rio.Dataclass`
- `NumberInput` can now evaluate math expressions
- `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
- `Calendar` now has a `is_sensitive` parameter
- New component: `rio.PdfViewer`
- `rio.Image` now has a loading animation
- Added `accessibility_role` parameter to all components
- Added 'accessibility_relationship' parameter to `Link` component
- Added `auto_focus` parameter to input components
- Added `Session.pick_folder`
- Added `Font.from_google_fonts` and `Font.from_css_file`
- Added `rio.HttpOnly`
- Added `text_style` parameter to `rio.TextInput`
- Experimental additions:
- `rio.List`
- `rio.Dict`
- `rio.Set`
- `rio.Dataclass`
## 0.11
- Added `tile` fill mode to `rio.ImageFill`
- `Component.force_refresh` is now synchronous
- Added `tile` fill mode to `rio.ImageFill`
- Colors now use Oklab instead of RGB
- Breaking: `rio.Color.hex` now returns a 6-digit hex code instead of an
- Added `tile` fill mode to `rio.ImageFill`
- `Component.force_refresh` is now synchronous
- Added `tile` fill mode to `rio.ImageFill`
- Colors now use Oklab instead of RGB
- Breaking: `rio.Color.hex` now returns a 6-digit hex code instead of an
8-digit one. Use `rio.Color.hexa` to get the old behavior.
- Dialogs now apply a style by default
- `rio.Drawer` now sizes itself to not only fit the anchor, but also the
- Dialogs now apply a style by default
- `rio.Drawer` now sizes itself to not only fit the anchor, but also the
drawer content
- `rio.Popup` now accepts `user_closable` and `modal`, just like dialogs
- more styling options for cells in `rio.Table`
- `rio.Popup` now accepts `user_closable` and `modal`, just like dialogs
- more styling options for cells in `rio.Table`
- Expose additional platform information:
- Expose additional platform information:
- `rio.Session.screen_width`
- `rio.Session.screen_height`
- `rio.Session.pixels_per_font_height`
- `rio.Session.scroll_bar_size`
- `rio.Session.primary_pointer_type`
- `rio.Session.screen_width`
- `rio.Session.screen_height`
- `rio.Session.pixels_per_font_height`
- `rio.Session.scroll_bar_size`
- `rio.Session.primary_pointer_type`
- Themes now take an additional `header_font` parameter
- Themes now take an additional `header_font` parameter
- Breaking: Gradient stops can now be specified just as colors and Rio will
- Breaking: Gradient stops can now be specified just as colors and Rio will
infer their position (breaking, because the stops must be ordered now)
- add icons for common brands
- add icons for common brands
## ???
- New styles for input boxes: "rounded" and "pill"
- Improved mobile support: Dragging is now much smoother
- Improved tables
- `rio run` now also works when using `as_fastapi`
- New styles for input boxes: "rounded" and "pill"
- Improved mobile support: Dragging is now much smoother
- Improved tables
- `rio run` now also works when using `as_fastapi`
## 0.10
- `rio.Dropdown` will now open a fullscreen popup on mobile devices
- `rio.MediaPlayer` now also triggers the `on_playback_end` event when the
- `rio.Dropdown` will now open a fullscreen popup on mobile devices
- `rio.MediaPlayer` now also triggers the `on_playback_end` event when the
video loops
- experimental support for base-URL
- dialogs!
- dialogs can now store a result value similar to futures
- `rio.Text.wrap` is now `rio.Text.overflow`. Same for markdown.
- removed `rio.Popup.on_open_or_close`. This event never actually fired.
- `rio.Link` can now optionally display an icon
- Rio will automatically create basic navigation for you, if your app has more
- experimental support for base-URL
- dialogs!
- dialogs can now store a result value similar to futures
- `rio.Text.wrap` is now `rio.Text.overflow`. Same for markdown.
- removed `rio.Popup.on_open_or_close`. This event never actually fired.
- `rio.Link` can now optionally display an icon
- Rio will automatically create basic navigation for you, if your app has more
than one page
- Updated button styles: Added `colored-text` and renamed `plain` ->
- Updated button styles: Added `colored-text` and renamed `plain` ->
`plain-text`
- Methods for creating dialogs are now in `rio.Session` rather than
- Methods for creating dialogs are now in `rio.Session` rather than
`rio.Component`.
- Page rework
- Add `rio.Redirect`
- Still missing automatic page scan
- New experimental `rio.FilePickerArea` component
- Page rework
- Add `rio.Redirect`
- Still missing automatic page scan
- New experimental `rio.FilePickerArea` component
## 0.9.2
- restyled `rio.Switch`
- New ~~experimental~~ broken component `AspectRatioContainer`
- restyled `rio.Switch`
- New ~~experimental~~ broken component `AspectRatioContainer`
## 0.9.1
- added gain_focus / lose_focus events to TextInput and NumberInput
- `.rioignore` has been superseeded by the new `project-files` setting in
- added gain_focus / lose_focus events to TextInput and NumberInput
- `.rioignore` has been superseeded by the new `project-files` setting in
`rio.toml`
- values in `rio.toml` are now written in kebab-case instead of
- values in `rio.toml` are now written in kebab-case instead of
all_lower_case. Rio will still recognize the old names and automatically fix
them for you.
- deprecated `light` parameter of `Theme.from_color`, has been superseded by
- deprecated `light` parameter of `Theme.from_color`, has been superseded by
`mode`
- Tooltips now default to `position="auto"`
- Icons now use `_` instead of `-` in their names. This brings them more in line
- Tooltips now default to `position="auto"`
- Icons now use `_` instead of `-` in their names. This brings them more in line
with Python naming conventions
- Checkbox restyling
- Checkbox restyling
## 0.9
- Buttons now have a smaller minimum size when using a `rio.Component` as
- Buttons now have a smaller minimum size when using a `rio.Component` as
content
- `FrostedGlassFill` added (Contributed by MiniTT)
- added `@rio.event.on_window_size_change`
- popups now default to the "hud" color
- popups and tooltips are no longer cut off by other components
- Add HTML meta tags
- Add functions for reading and writing clipboard contents to the `Session`
- `FrostedGlassFill` added (Contributed by MiniTT)
- added `@rio.event.on_window_size_change`
- popups now default to the "hud" color
- popups and tooltips are no longer cut off by other components
- Add HTML meta tags
- Add functions for reading and writing clipboard contents to the `Session`
(Contributed by MiniTT)
- The color of drawers is now configurable, and also sets the theme context
- added `Calendar` component
- added `DateInput` component
- massive dev-tools overhaul
- new (but experimental) `Switcher` component
- TextInputs now update their text in real-time
- `rio run` no longer opens a browser
- `rio.HTML` components now execute embedded `<script>` nodes
- added `Checkbox` Component
- `FlowContainer` now has a convenience `spacing` parameter which controls both
- The color of drawers is now configurable, and also sets the theme context
- added `Calendar` component
- added `DateInput` component
- massive dev-tools overhaul
- new (but experimental) `Switcher` component
- TextInputs now update their text in real-time
- `rio run` no longer opens a browser
- `rio.HTML` components now execute embedded `<script>` nodes
- added `Checkbox` Component
- `FlowContainer` now has a convenience `spacing` parameter which controls both
`row_spacing` and `column_spacing` at the same time
deprecations:
- `rio.Fill` and `rio.FillLike` deprecated. Most components only support
- `rio.Fill` and `rio.FillLike` deprecated. Most components only support
specific fills, so these have no purpose any more
- `display_controls` parameter of `CodeBlock` component renamed to
- `display_controls` parameter of `CodeBlock` component renamed to
`show_controls`
breaking:
- `Text.justify` now defaults to `"left"`
- `FlowContainer.justify` now defaults to `"left"`
- `rio.Theme` is no longer frozen, and can now be modified. This is breaking,
- `Text.justify` now defaults to `"left"`
- `FlowContainer.justify` now defaults to `"left"`
- `rio.Theme` is no longer frozen, and can now be modified. This is breaking,
because the `replace` method has been removed
## 0.8
- Rectangles now honor the theme's shadow color
- Renamed `Banner.markup` to `Banner.markdown`
- Removed the "multiline" style from Banners
- Removed `Button.initially_disabled_for`
- Added a `text_color` parameter to `Theme.from_colors` and
- Rectangles now honor the theme's shadow color
- Renamed `Banner.markup` to `Banner.markdown`
- Removed the "multiline" style from Banners
- Removed `Button.initially_disabled_for`
- Added a `text_color` parameter to `Theme.from_colors` and
`Theme.pair_from_colors`
- `rio run` now checks that the installed version of Rio is up-to-date
- `rio run` now checks that the installed version of Rio is up-to-date
## 0.7
- New example: multi-page website
- New component: CodeBlock
- UserSettings can now have mutable default values
- Removed "undefined space"
- New example: multi-page website
- New component: CodeBlock
- UserSettings can now have mutable default values
- Removed "undefined space"

View File

@@ -697,12 +697,15 @@ export type KeyEventListenerState = KeyboardFocusableComponentState & {
reportKeyDown: KeyCombination[] | true;
reportKeyUp: KeyCombination[] | true;
reportKeyPress: KeyCombination[] | true;
event_order: "before-child" | "after-child";
};
export class KeyEventListenerComponent extends KeyboardFocusableComponent<KeyEventListenerState> {
private keyDownCombinations: Set<string> | true;
private keyUpCombinations: 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 {
let element = document.createElement("div");
@@ -738,30 +741,54 @@ export class KeyEventListenerComponent extends KeyboardFocusableComponent<KeyEve
);
}
let reportKeyDown =
this.keyDownCombinations === true ||
this.keyDownCombinations.size > 0;
let reportKeyUp =
this.keyUpCombinations === true || this.keyUpCombinations.size > 0;
let reportKeyPress =
this.keyPressCombinations === true ||
this.keyPressCombinations.size > 0;
// Check if we need to update event listeners
if (
deltaState.reportKeyDown !== undefined ||
deltaState.reportKeyPress !== undefined ||
deltaState.event_order !== undefined
) {
let reportKeyDown =
this.keyDownCombinations === true ||
this.keyDownCombinations.size > 0;
let reportKeyPress =
this.keyPressCombinations === true ||
this.keyPressCombinations.size > 0;
let eventOrder = deltaState.event_order ?? this.state.event_order;
if (reportKeyDown || reportKeyPress) {
this.element.onkeydown = (e: KeyboardEvent) => {
this.handleKeyEvent(e, "KeyPress", this.keyPressCombinations);
this.handleKeyEvent(e, "KeyDown", this.keyDownCombinations);
};
} else {
this.element.onkeydown = null;
this.onKeyDownBound = this._updateEventListener(
"keydown",
reportKeyDown || reportKeyPress,
eventOrder,
this.onKeyDownBound,
(e: KeyboardEvent) => {
this.handleKeyEvent(
e,
"KeyPress",
this.keyPressCombinations
);
this.handleKeyEvent(e, "KeyDown", this.keyDownCombinations);
}
);
}
if (reportKeyUp) {
this.element.onkeyup = (e: KeyboardEvent) => {
this.handleKeyEvent(e, "KeyUp", this.keyUpCombinations);
};
} else {
this.element.onkeyup = null;
if (
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.replaceOnlyChild(context, deltaState.content);
@@ -804,6 +831,32 @@ export class KeyEventListenerComponent extends KeyboardFocusableComponent<KeyEve
...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(

View File

@@ -679,6 +679,11 @@ class KeyEventListener(KeyboardFocusableFundamentalComponent):
`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.
`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
@@ -695,6 +700,7 @@ class KeyEventListener(KeyboardFocusableFundamentalComponent):
rio.EventHandler[KeyPressEvent]
| t.Mapping[KeyCombination, rio.EventHandler[KeyPressEvent]]
) = None
event_order: t.Literal["before-child", "after-child"] = "after-child"
def __post_init__(self):
# TODO: These values are never updated, which is a problem if someone