Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added the dateinput and timeinput widgets for toga/web #2176

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions changes/2176.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added the DateInput and TimeInput widgets implementations to the toga web project.
dgmouris marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion docs/reference/data/widgets_by_platform.csv
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ MainWindow,Core Component,:class:`~toga.MainWindow`,The main window of the appli
ActivityIndicator,General Widget,:class:`~toga.ActivityIndicator`,A spinning activity animation,|y|,|y|,,,,|b|,
Button,General Widget,:class:`~toga.Button`,Basic clickable Button,|y|,|y|,|y|,|y|,|y|,|b|,|b|
Canvas,General Widget,:class:`~toga.Canvas`,A drawing area for 2D vector graphics.,|y|,|y|,|y|,|y|,|y|,,
DateInput,General Widget,:class:`~toga.DateInput`,A widget to select a calendar date,,,|y|,,|y|,,
DateInput,General Widget,:class:`~toga.DateInput`,A widget to select a calendar date,,,|y|,,|y|,|b|,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TimeInput will also need to be updated.

DetailedList,General Widget,:class:`~toga.DetailedList`,"An ordered list of content where each item has an icon, a main heading, and a line of supplementary text.",|y|,|y|,|b|,|y|,|y|,,
Divider,General Widget,:class:`~toga.Divider`,A horizontal or vertical line,|y|,|y|,|y|,,,|b|,
ImageView,General Widget,:class:`~toga.ImageView`,A widget that displays an image,|y|,|y|,|y|,|y|,|y|,,
Expand Down
4 changes: 4 additions & 0 deletions web/src/toga_web/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .widgets.activityindicator import ActivityIndicator
from .widgets.box import Box
from .widgets.button import Button
from .widgets.dateinput import DateInput
from .widgets.divider import Divider

# from .widgets.canvas import Canvas
Expand All @@ -32,6 +33,7 @@

# from .widgets.table import Table
from .widgets.textinput import TextInput
from .widgets.timeinput import TimeInput

# from .widgets.tree import Tree
# from .widgets.webview import WebView
Expand Down Expand Up @@ -59,6 +61,7 @@ def not_implemented(feature):
"Box",
"Button",
# 'Canvas',
"DateInput",
"Divider",
# 'DetailedList',
# 'ImageView',
Expand All @@ -76,6 +79,7 @@ def not_implemented(feature):
"Switch",
# 'Table',
"TextInput",
"TimeInput",
# 'Tree',
# 'WebView',
# 'Window',
Expand Down
50 changes: 50 additions & 0 deletions web/src/toga_web/widgets/dateinput.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import datetime

from toga_web.libs import create_proxy

from .base import Widget


def py_date(native_date):
return datetime.datetime.strptime(native_date, "%Y-%m-%d").date()


def native_date(py_date):
return py_date.strftime("%Y-%m-%d")


class DateInput(Widget):
def create(self):
self._return_listener = None
self.native = self._create_native_widget("sl-input")
self.native.setAttribute("type", "date")

self.set_value(datetime.date.today().strftime("%Y-%m-%d"))
self.native.addEventListener("sl-change", create_proxy(self.on_change))

def get_value(self):
return py_date(self.native.value)

def set_value(self, value):
if value is None:
self.native.value = ""
self.native.value = value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't appear to be fully working with initial values. In my testing, on Safari, the current date is being displayed on every date widget in the example app; on Chrome (on macOS), I get dd/mm/yyyy as the default values.

The "with max" and "with min and max" date examples should have an initial date of 2021-04-02; "any" and "with min" should have no initial value (although if the browser defaults to the current date as a browser behavior, that's fine).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is impacting the behavior difference - but I'm using AU date formatting order, so I see "6/11/2023" as todays' date (6 Nov 2023). I'm not sure the extent to which browsers will be rejecting the default value of "%Y-%m-%d" on the basis of localised validation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only tested this on chrome, I'll test this on firefox as well to see the differences (my apologies).

The "with max" and "with min and max" date examples should have an initial date of 2021-04-02; "any" and "with min"

but I'll definitely change this piece to have this functionality.


def on_change(self, event):
self.interface.on_change(None)

def get_min_date(self):
if self.native.min:
return py_date(self.native.min)
return datetime.date(1800, 1, 1)

def get_max_date(self):
if self.native.max:
return py_date(self.native.max)
return datetime.date(8999, 12, 31)

def set_min_date(self, value):
self.native.min = native_date(value)

def set_max_date(self, value):
self.native.max = native_date(value)
57 changes: 57 additions & 0 deletions web/src/toga_web/widgets/timeinput.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import datetime

from toga_web.libs import create_proxy

from .base import Widget


def py_time(native_time):
return datetime.time.fromisoformat(native_time)


def native_time(py_time):
return py_time.strftime("%H:%M")


class TimeInput(Widget):
def create(self):
self._return_listener = None
self.native = self._create_native_widget("sl-input")
self.native.setAttribute("type", "time")

self.set_value(datetime.datetime.now().time().strftime("%H:%M"))
self.native.addEventListener("sl-change", create_proxy(self.on_change))

def on_change(self, event):
self.interface.on_change(None)

def get_value(self):
return py_time(self.native.value)

def set_value(self, value):
if value is None:
self.native.value = ""
self.native.value = self._format_time(value)

def set_min_time(self, value):
self.native.min = self._format_time(value)

def set_max_time(self, value):
self.native.max = self._format_time(value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These min/max values don't appear to be applied - I can set times before the min/after the max in the example app.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to know the way you'd like this handled, the way I'm thinking is to essentially set the value for that time to the minimum or maximum (which ever is relevant).

The only thing here is that browsers don't enforce a min/max on the time input and I couldn't find the max/min on the shoelace components.

I'm certainly open to anything here but I think it might be a bit hidden if we set the value here to the max or min time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, Android has a similar limitation. The approach we've taken there is to catch the "on change" event, and when it occurs, explicitly use the API to set the value of the widget to the value that was just set. This might seems like a no-op, but it has the effect of clipping the time to the min/max bounds.


def get_min_time(self):
if self.native.min:
return py_time(self.native.min)
return datetime.time(0, 0, 0)

def get_max_time(self):
if self.native.max:
return py_time(self.native.max)
return datetime.time(23, 59, 59)

def _format_time(self, value):
if isinstance(value, str):
value = native_time(py_time(value))
if isinstance(value, datetime.time):
value = native_time(value)
return value
Loading