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

feat: UI Dialog and DialogTrigger Components #953

Merged
merged 43 commits into from
Nov 1, 2024

Conversation

dgodinez-dh
Copy link
Contributor

closes #933
closes #863

Note that there is a bug in the spectrum theme that causes the underlay to render as transparent. It is fixed here:
deephaven/web-client-ui#2267
The underlay may render incorrectly depending on your version of web-client-ui
Because of this bug, we are waiting for a new version before adding a render test for dialog. See #952

@dgodinez-dh dgodinez-dh requested review from a team and bmingles and removed request for a team October 23, 2024 17:33
Comment on lines 13 to 18
flag, set_true, set_false = ui.use_flag()
return [
ui.text(f"{flag}"),
ui.button("Set True", on_press=set_true),
ui.button("Set False", on_press=set_false),
]
Copy link
Member

@mofojed mofojed Oct 25, 2024

Choose a reason for hiding this comment

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

The main draw for this hook is so we don't need to have lambdas like the following, correct?

    flag, set_flag = ui.use_flag(False)
    return [
        ui.text(f"{flag}"),
        ui.button("Set True", on_press=lambda: set_flag(True)),
        ui.button("Set False", on_press=lambda: set_flag(False)),
    ]

Seems convenient for these cases, particularly around something like opening/closing a dialog... My only concern is that for each public hook we publish, careful consideration is required about the API of said hook and it's future usability/maintainability.

With that in mind, I have a couple concerns about this hook as is:

  • use_flag is a name that doesn't make it immediately apparent what's up - the name flag to me implies a setting of some sort, like reading a global flag that's set. I'd rather name this something like use_boolean
  • It's returning two setters instead of one (as typical with use_state), and it would be easy to accidentally mix up the setters (e.g. I do value, set_false, set_true = ui.use_boolean(), no warnings/errors in the code but my boolean will now be the opposite of what I want).
  • Another really common use case with a boolean would be to have a toggle function, which this does not address. You could have another setter function, but that's exacerbating the previous issue

Doing a search for existing React hooks for useBoolean, seems there are a couple hooks that have done something similar:

  1. https://marcoghiani.com/blog/react-custom-hooks-usetoggle-useboolean
  2. https://dev.to/iamludal/react-custom-hooks-useboolean-3m6c
  3. https://usehooks-ts.com/react-hook/use-boolean

I don't want to check in this hook as is... let me check something out first.

Copy link
Member

Choose a reason for hiding this comment

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

Okay, take a look at this (@dgodinez-dh, @bmingles, @dsmmcken, and @jnumainville):

from deephaven import ui
from deephaven.ui._internal import InitializerFunction, UpdaterFunction
from typing import Callable, cast, overload, Protocol, Tuple

class BooleanCallable(Protocol):
    on: Callable[[], None]
    off: Callable[[], None]
    toggle: Callable[[], None]

    def __call__(self, new_value: UpdaterFunction[bool]) -> None: ...

def use_boolean(
    initial_value: bool | InitializerFunction[bool] = False,
) -> Tuple[bool, BooleanCallable]:
    """
    Hook to add a boolean flag variable to your component. The flag will persist across renders.
    This is a convenience hook for when you only need functions to set a flag to True or False.
    For more complex state management, use use_state.

    Args:
        initial_flag: The initial value for the flag.
            It can be True or False, but passing a function will treat it as an initializer function.
            An initializer function is called with no parameters once on the first render to get the initial value.
            After the initial render the argument is ignored.
            Default is False.

    Returns:
        A tuple containing the current value of the flag, a function to set the flag to True, and a function to set the flag to False.
    """
    flag, set_flag = ui.use_state(initial_value)

    on = ui.use_callback(lambda: set_flag(True), [set_flag])
    off = ui.use_callback(lambda: set_flag(False), [set_flag])
    toggle = ui.use_callback(lambda: set_flag(lambda old_value: not old_value), [set_flag])

    def init_callable():
        boolean_callable = cast(BooleanCallable, set_flag)
        boolean_callable.on = on
        boolean_callable.off = off
        boolean_callable.toggle = toggle
        return boolean_callable

    boolean_callable = ui.use_memo(init_callable, [set_flag, on, off, toggle])

    return flag, boolean_callable

# Example usage:
@ui.component
def ui_boolean_example():
    value, set_value = use_boolean()

    return [
        ui.text(f"{value}"),
        ui.checkbox("My value", is_selected=value, on_change=set_value),
        ui.switch("My value", is_selected=value, on_change=set_value),
        ui.button("Set True", on_press=set_value.on),
        ui.button("Set False", variant="negative", on_press=set_value.off),
        ui.button("Toggle", variant="secondary", on_press=set_value.toggle)
    ]

my_boolean_example = ui_boolean_example()

Some things I like about this:

  • Still returns a value/set_value tuple like a regular use_state, so you can still use it with our controls like checkboxes/switches.
  • Can pass convenient .on, .off, .toggle callables still, for the use case you're using it for

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This seems good. I really wanted an open / close, but decided to try to generalize it. I also wanted a toggle, but I didn't want to return a third setter.

Copy link
Contributor

Choose a reason for hiding this comment

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

The proposed api makes sense to me.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I like this. Keeps the api both flexible and clean.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

@dgodinez-dh dgodinez-dh marked this pull request as draft October 28, 2024 12:05
@dgodinez-dh dgodinez-dh marked this pull request as ready for review October 28, 2024 15:57
bmingles
bmingles previously approved these changes Oct 28, 2024
Copy link
Contributor

@bmingles bmingles left a comment

Choose a reason for hiding this comment

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

I reviewed the new boolean hook. Changes look good to me.

Co-authored-by: Joe <josephnumainville@deephaven.io>
jnumainville
jnumainville previously approved these changes Oct 31, 2024
mofojed
mofojed previously approved these changes Oct 31, 2024
Co-authored-by: margaretkennedy <82049573+margaretkennedy@users.noreply.github.com>
@dgodinez-dh dgodinez-dh merged commit 0fbae91 into deephaven:main Nov 1, 2024
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ui.dialog, ui.dialog_trigger implementation docs: ui.dialog
5 participants