mirror of
https://github.com/rio-labs/rio.git
synced 2026-01-05 20:59:46 -06:00
added gain_focus / lose_focus events to TextInput and NumberInput
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>);
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user