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

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
); );
} }
let reportKeyDown = // Check if we need to update event listeners
this.keyDownCombinations === true || if (
this.keyDownCombinations.size > 0; deltaState.reportKeyDown !== undefined ||
let reportKeyUp = deltaState.reportKeyPress !== undefined ||
this.keyUpCombinations === true || this.keyUpCombinations.size > 0; deltaState.event_order !== undefined
let reportKeyPress = ) {
this.keyPressCombinations === true || let reportKeyDown =
this.keyPressCombinations.size > 0; 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.onKeyDownBound = this._updateEventListener(
this.element.onkeydown = (e: KeyboardEvent) => { "keydown",
this.handleKeyEvent(e, "KeyPress", this.keyPressCombinations); reportKeyDown || reportKeyPress,
this.handleKeyEvent(e, "KeyDown", this.keyDownCombinations); eventOrder,
}; this.onKeyDownBound,
} else { (e: KeyboardEvent) => {
this.element.onkeydown = null; this.handleKeyEvent(
e,
"KeyPress",
this.keyPressCombinations
);
this.handleKeyEvent(e, "KeyDown", this.keyDownCombinations);
}
);
} }
if (reportKeyUp) { if (
this.element.onkeyup = (e: KeyboardEvent) => { deltaState.reportKeyUp !== undefined ||
this.handleKeyEvent(e, "KeyUp", this.keyUpCombinations); deltaState.event_order !== undefined
}; ) {
} else { let reportKeyUp =
this.element.onkeyup = null; 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); 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