From 72034cd679c51845ef8dd2d4a5a613b4a384a55a Mon Sep 17 00:00:00 2001 From: Sn3llius Date: Thu, 10 Oct 2024 19:13:52 +0200 Subject: [PATCH] reworked DateInput --- rio/components/calendar.py | 20 +++++++++ rio/components/date_input.py | 84 +++++++++++++++++++++++++++++++----- 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/rio/components/calendar.py b/rio/components/calendar.py index fe86d71d..77a01b83 100644 --- a/rio/components/calendar.py +++ b/rio/components/calendar.py @@ -12,6 +12,7 @@ from .fundamental_component import FundamentalComponent __all__ = [ "DateChangeEvent", + "DateConfirmEvent", "Calendar", ] @@ -35,6 +36,25 @@ class DateChangeEvent: value: date +@t.final +@rio.docs.mark_constructor_as_private +@dataclass +class DateConfirmEvent: + """ + Holds information regarding a date confirm event. + + This is a simple dataclass that stores useful information for when the user + confirms the date in `DateInput`. You'll typically receive this as argument + in `on_confirm` events. + + ## Attributes + + `value`: The newly selected date. + """ + + value: date + + class Calendar(FundamentalComponent): """ Allows the user to pick a date from a calendar. diff --git a/rio/components/date_input.py b/rio/components/date_input.py index 6fafbc5c..0d9f83e1 100644 --- a/rio/components/date_input.py +++ b/rio/components/date_input.py @@ -3,7 +3,7 @@ from __future__ import annotations import random import typing as t from dataclasses import KW_ONLY -from datetime import date +from datetime import date, datetime import rio @@ -52,6 +52,12 @@ def make_fake_input_box( align_y=1, grow_x=True, ), + rio.TextInput( + value, + margin_bottom=0.4, + align_y=1, + grow_x=True, + ), # spacing=0.8, grow_y=True, ), @@ -103,6 +109,7 @@ def make_fake_input_box( return define_content(corner_radius) +# TODO: update the docstring and do more testing @t.final class DateInput(Component): """ @@ -129,6 +136,12 @@ class DateInput(Component): `on_change`: Triggered whenever the user selects a new date. + `on_confirm`: Triggered when the user explicitly confirms their input, + such as by pressing the "Enter" key. You can use this to trigger + followup actions, such as logging in or submitting a form. + + `position`: The location at which the popup opens, relative to the anchor. + ## Examples @@ -191,10 +204,36 @@ class DateInput(Component): label: str = "" style: t.Literal["underlined", "rounded", "pill"] = "underlined" + position: t.Literal[ + "auto", "left", "top", "right", "bottom", "center", "fullscreen" + ] = "auto" + on_change: rio.EventHandler[rio.DateChangeEvent] = None + on_confirm: rio.EventHandler[rio.DateConfirmEvent] = None _is_open: bool = False + def _try_set_value(self, raw_value: str) -> bool: + """ + Parse the given string and update the component's value accordingly. + Returns `True` if the value was successfully updated, `False` otherwise. + """ + # try to parse the date string + try: + # Maybe we can do some more advanced parsing here, but OK for now + date_value = datetime.strptime( + raw_value, self.session._date_format_string + ).date() + + # Update the value + self.value = date_value + return True + + except ValueError: + # Force the old value to stay + self.value = self.value + return False + async def _on_value_change(self, event: rio.DateChangeEvent) -> None: # Close the date picker self._is_open = False @@ -202,7 +241,24 @@ class DateInput(Component): # Chain the event handler await self.call_event_handler(self.on_change, event) - def _on_toggle_open(self, _: rio.PointerEvent) -> None: + async def _on_confirm(self, ev: rio.TextInputConfirmEvent) -> None: + # Close the date picker + self._is_open = False + + was_updated = self._try_set_value(ev.text) + + if was_updated: + await self.call_event_handler( + self.on_change, + rio.DateChangeEvent(self.value), + ) + + await self.call_event_handler( + self.on_confirm, + rio.DateConfirmEvent(self.value), + ) + + def _on_toggle_open(self, _: rio.TextInputFocusEvent) -> None: self._is_open = not self._is_open def _on_close(self) -> None: @@ -210,16 +266,24 @@ class DateInput(Component): def build(self) -> rio.Component: return rio.Popup( - # Place a fake textbox. It's only used for styling and displaying - # the label, if any - anchor=rio.PointerEventListener( - content=make_fake_input_box( - theme=self.session.theme, + # Display the input field + anchor=rio.Stack( + rio.TextInput( label=self.label, - value=self.value.strftime(self.session._date_format_string), + text=self.value.strftime(self.session._date_format_string), + on_gain_focus=self._on_toggle_open, + on_confirm=self._on_confirm, style=self.style, ), - on_press=self._on_toggle_open, + rio.Icon( + "material/calendar_today:fill", + fill="dim", + min_width=1.5, + min_height=1.5, + margin_right=0.5, + align_y=0.5, + align_x=1, + ), ), # Display a calendar so the user can pick a date content=rio.Column( @@ -238,7 +302,7 @@ class DateInput(Component): margin=1, ), color="neutral", - position="center", + position=self.position, alignment=0.5, is_open=self._is_open, )