added gain_focus / lose_focus events to TextInput and NumberInput

This commit is contained in:
Jakob Pinterits
2024-06-12 20:46:29 +02:00
parent 5a7966462c
commit e2edb768fe
5 changed files with 151 additions and 45 deletions

View File

@@ -1,5 +1,9 @@
# Changelog
- added gain_focus / lose_focus events to TextInput and NumberInput
## 0.9
- Buttons now have a smaller minimum size when using a `rio.Component` as
content
- `FrostedGlassFill` added (Contributed by MiniTT)

View File

@@ -348,7 +348,7 @@ export abstract class ComponentBase {
});
}
_setStateDontNotifyBackend(deltaState: ComponentState): void {
_setStateDontNotifyBackend(deltaState: object): void {
// Trigger an update
this.updateElement(deltaState, null as any as Set<ComponentBase>);

View File

@@ -54,10 +54,17 @@ export class TextInputComponent extends ComponentBase {
element.querySelectorAll('.rio-text-input-hint-text')
) as HTMLElement[];
// Create a rate-limited function for notifying the backend of change
// Create a rate-limited function for notifying the backend of changes.
// This allows reporting changes to the backend in real-time, rather
// just when losing focus.
this.onChangeLimiter = new Debouncer({
callback: (newText: string) => {
this.setStateAndNotifyBackend({
this._setStateDontNotifyBackend({
text: newText,
});
this.sendMessageToBackend({
type: 'change',
text: newText,
});
},
@@ -70,9 +77,23 @@ export class TextInputComponent extends ComponentBase {
this.onChangeLimiter.call(this.inputElement.value);
});
// Detect focus gain...
this.inputElement.addEventListener('focus', () => {
this.sendMessageToBackend({
type: 'gainFocus',
text: this.inputElement.value,
});
});
// ...and focus loss
this.inputElement.addEventListener('blur', () => {
this.onChangeLimiter.call(this.inputElement.value);
this.onChangeLimiter.flush();
this.sendMessageToBackend({
type: 'loseFocus',
text: this.inputElement.value,
});
});
// Detect the enter key and send it to the backend
@@ -92,6 +113,7 @@ export class TextInputComponent extends ComponentBase {
// Inform the backend
this.sendMessageToBackend({
type: 'confirm',
text: this.state.text,
});
}

View File

@@ -12,6 +12,7 @@ __all__ = [
"NumberInput",
"NumberInputChangeEvent",
"NumberInputConfirmEvent",
"NumberInputFocusEvent",
]
@@ -61,6 +62,25 @@ class NumberInputConfirmEvent:
value: float
@final
@rio.docs.mark_constructor_as_private
@dataclass
class NumberInputFocusEvent:
"""
Holds information regarding a number input focus event.
This is a simple dataclass that stores useful information for when a
`NumberInput` gains or loses focus. You'll typically receive this as
argument in `on_gain_focus` and `on_lose_focus` events.
## Attributes
value: The `value` of the `NumberInput`.
"""
value: float
@final
class NumberInput(Component):
"""
@@ -168,9 +188,13 @@ class NumberInput(Component):
decimals: int = 2
is_sensitive: bool = True
is_valid: bool = True
on_change: rio.EventHandler[NumberInputChangeEvent] = None
on_confirm: rio.EventHandler[NumberInputConfirmEvent] = None
on_gain_focus: rio.EventHandler[NumberInputFocusEvent] = None
on_lose_focus: rio.EventHandler[NumberInputFocusEvent] = None
def __post_init__(self):
self._text_input = None
@@ -238,7 +262,13 @@ class NumberInput(Component):
self.value = value
return True
async def _on_change(self, ev: rio.TextInputChangeEvent) -> None:
async def _on_gain_focus(self, ev: rio.TextInputFocusEvent) -> None:
await self.call_event_handler(
self.on_gain_focus,
NumberInputFocusEvent(self.value),
)
async def _on_lose_focus(self, ev: rio.TextInputFocusEvent) -> None:
was_updated = self._try_set_value(ev.text)
if was_updated:
@@ -247,10 +277,20 @@ class NumberInput(Component):
NumberInputChangeEvent(self.value),
)
await self.call_event_handler(
self.on_lose_focus,
NumberInputFocusEvent(self.value),
)
async def _on_confirm(self, ev: rio.TextInputConfirmEvent) -> None:
was_updated = self._try_set_value(ev.text)
if was_updated:
await self.call_event_handler(
self.on_change,
NumberInputChangeEvent(self.value),
)
await self.call_event_handler(
self.on_confirm,
NumberInputConfirmEvent(self.value),
@@ -289,8 +329,9 @@ class NumberInput(Component):
suffix_text=self.suffix_text,
is_sensitive=self.is_sensitive,
is_valid=self.is_valid,
on_change=self._on_change,
on_confirm=self._on_confirm,
on_gain_focus=self._on_gain_focus,
on_lose_focus=self._on_lose_focus,
)
return self._text_input

View File

@@ -3,8 +3,6 @@ from __future__ import annotations
from dataclasses import KW_ONLY, dataclass
from typing import Any, final
from uniserde import JsonDoc
import rio.docs
from .fundamental_component import KeyboardFocusableFundamentalComponent
@@ -13,6 +11,7 @@ __all__ = [
"TextInput",
"TextInputChangeEvent",
"TextInputConfirmEvent",
"TextInputFocusEvent",
]
@@ -54,6 +53,25 @@ class TextInputConfirmEvent:
text: str
@final
@rio.docs.mark_constructor_as_private
@dataclass
class TextInputFocusEvent:
"""
Holds information regarding a text input focus event.
This is a simple dataclass that stores useful information for when a
`TextInput` gains or loses focus. You'll typically receive this as argument
in `on_gain_focus` and `on_lose_focus` events.
## Attributes
`text`: The `text` of the `TextInput`.
"""
text: str
@final
class TextInput(KeyboardFocusableFundamentalComponent):
"""
@@ -146,36 +164,12 @@ class TextInput(KeyboardFocusableFundamentalComponent):
is_secret: bool = False
is_sensitive: bool = True
is_valid: bool = True
on_change: rio.EventHandler[TextInputChangeEvent] = None
on_confirm: rio.EventHandler[TextInputConfirmEvent] = None
def _validate_delta_state_from_frontend(self, delta_state: JsonDoc) -> None:
if not set(delta_state) <= {"text"}:
raise AssertionError(
f"Frontend tried to change `{type(self).__name__}` state: {delta_state}"
)
if "text" in delta_state and not self.is_sensitive:
raise AssertionError(
f"Frontend tried to set `TextInput.text` even though `is_sensitive` is `False`"
)
async def _call_event_handlers_for_delta_state(
self, delta_state: JsonDoc
) -> None:
# Trigger on_change event
try:
new_value = delta_state["text"]
except KeyError:
pass
else:
assert isinstance(new_value, str), new_value
await self.call_event_handler(
self.on_change,
TextInputChangeEvent(new_value),
)
self._apply_delta_state_from_frontend(delta_state)
on_gain_focus: rio.EventHandler[TextInputFocusEvent] = None
on_lose_focus: rio.EventHandler[TextInputFocusEvent] = None
async def _on_message(self, msg: Any) -> None:
# Listen for messages indicating the user has confirmed their input
@@ -185,19 +179,64 @@ class TextInput(KeyboardFocusableFundamentalComponent):
# date value.
assert isinstance(msg, dict), msg
self._apply_delta_state_from_frontend({"text": msg["text"]})
# Update the local state
old_value = self.text
# Trigger both the change event...
await self.call_event_handler(
self.on_change,
TextInputChangeEvent(self.text),
)
if self.is_sensitive:
self._apply_delta_state_from_frontend({"text": msg["text"]})
# And the confirm event
await self.call_event_handler(
self.on_confirm,
TextInputConfirmEvent(self.text),
)
value_has_changed = old_value != self.text
# What sort of event is this?
event_type = msg.get("type")
# Gain focus
if event_type == "gainFocus":
await self.call_event_handler(
self.on_gain_focus,
TextInputFocusEvent(self.text),
)
# Lose focus
elif event_type == "loseFocus":
await self.call_event_handler(
self.on_lose_focus,
TextInputFocusEvent(self.text),
)
if self.is_sensitive and value_has_changed:
await self.call_event_handler(
self.on_change,
TextInputChangeEvent(self.text),
)
# Change
elif event_type == "change":
if self.is_sensitive and value_has_changed:
await self.call_event_handler(
self.on_change,
TextInputChangeEvent(self.text),
)
# Confirm
elif event_type == "confirm":
if self.is_sensitive:
if value_has_changed:
await self.call_event_handler(
self.on_change,
TextInputChangeEvent(self.text),
)
await self.call_event_handler(
self.on_confirm,
TextInputConfirmEvent(self.text),
)
# Invalid
else:
raise AssertionError(
f"Received invalid event from the frontend: {msg}"
)
# Refresh the session
await self.session._refresh()