BasecoatUI components for htmy.

Alert Dialog

Example:

Alert dialog

This is an example alert dialog. It cannot be closed by clicking on the backdrop.


Code example:

from htmy import ComponentType, html

from htmui.basecoat import dialog


def example() -> ComponentType:
    return html.div(
        dialog.show_dialog_button("Show dialog", dialog_id="example-alert-dialog"),
        dialog.dialog(
            id="example-alert-dialog",
            title="Alert dialog",
            description=(
                "This is an example alert dialog. It cannot be closed by clicking on the backdrop."
            ),
            footer=html.footer(
                html.button(
                    "Close",
                    class_="btn",
                    onclick=dialog.handle_close_button_click.close,
                ),
            ),
            close_button=None,
            alert=True,
        ),
    )

Component implementation:

For more details, see the BasecoatUI documentation.

from typing import Literal

from htmy import ComponentType, PropertyValue, SafeStr, html, join_classes

from .button import ButtonSize, ButtonVariant
from .typing import IdToProperty

__version__ = "0.2.0"
__framework__ = "BasecoatUI"
__framework_version__ = "1"
__framework_url__ = "https://basecoatui.com/components/dialog/"


class handle_close_button_click:
    close: str = "this.closest('dialog').close()"
    delete: str = "this.closest('dialog').delete()"


close_icon = SafeStr(
    '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" '
    'stroke-width="1.5" stroke="currentColor" class="size-5">'
    '<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />'
    "</svg>"
)
"""`x-mark` icon from https://heroicons.com/."""


def close_button(
    icon: ComponentType = close_icon,
    *,
    onclick: str = handle_close_button_click.close,
    aria_label: str = "Close dialog",
    class_: str | None = None,
    size: ButtonSize | None = "icon-sm",
    variant: ButtonVariant | None = "ghost",
    **kwargs: PropertyValue,
) -> ComponentType:
    """Close button for dialogs."""
    if variant is not None:
        kwargs["data_variant"] = variant
    if size is not None:
        kwargs["data_size"] = size
    return html.button(
        icon,
        type="button",
        class_=join_classes("btn", class_),
        onclick=onclick,
        aria_label=aria_label,
        **kwargs,
    )


AlertDialogSize = Literal["sm"]


class handle_click:
    close: str = "if (event.target === this) this.close()"
    delete: str = "if (event.target === this) this.delete()"


def dialog(
    *children: ComponentType,
    id: str,
    title: ComponentType,
    description: ComponentType | None = None,
    footer: ComponentType | None = None,
    close_button: ComponentType | None = close_button(),  # noqa: B008
    figure: ComponentType | None = None,
    alert: bool = False,
    size: AlertDialogSize | None = None,
    class_: str | None = None,
    content_class: str | None = None,
    onclick: str = handle_click.close,
    **kwargs: PropertyValue,
) -> ComponentType:
    """
    Dialog or alert-dialog modal.

    Arguments:
        *children: Dialog body content.
        id: Dialog element id.
        title: Dialog title.
        description: Optional description shown under the title.
        footer: Optional footer. Should be an `html.footer(...)` when provided.
        close_button: Optional close button.
        figure: Optional media to show in a `figure` in the header.
        alert: Whether this is an alert dialog.
        size: Dialog size.
        class_: Extra CSS classes for the root `dialog` element.
        content_class: Extra CSS classes for the inner content.
        onclick: Click handler for the root `dialog` element. Ignored for alert dialogs.
        **kwargs: Extra attributes for the `dialog` element.
    """
    title_id = f"{id}-title"
    description_id = f"{id}-description"
    kwargs["aria_labelledby"] = title_id
    if not alert:
        kwargs["onclick"] = onclick
    if size is not None:
        kwargs["data_size"] = size
    if description is not None:
        kwargs["aria_describedby"] = description_id

    return html.dialog(
        html.div(
            html.header(
                html.figure(figure) if figure is not None else None,
                html.h2(title, id=title_id),
                html.p(description, id=description_id) if description is not None else None,
            ),
            html.section(*children) if len(children) > 0 else None,
            footer,
            close_button,
            class_=content_class,
        ),
        id=id,
        class_=join_classes("alert-dialog" if alert else "dialog", class_),
        **kwargs,
    )


class handle_show_button_click:
    @staticmethod
    def show_modal(dialog_id: str) -> PropertyValue:
        return f"document.getElementById('{dialog_id}').showModal()"


def show_dialog_button(
    title: ComponentType,
    *,
    dialog_id: str | None = None,
    onclick: IdToProperty | None = handle_show_button_click.show_modal,
    class_: str | None = None,
    variant: ButtonVariant | None = "outline",
    **kwargs: PropertyValue,
) -> ComponentType:
    """
    Button that opens a dialog.

    Arguments:
        title: Button content.
        dialog_id: Target dialog ID. Required when `onclick` is not `None`.
        onclick: Optional callback that receives `dialog_id` and returns the value of
            the `onclick` property. Defaults to `handle_show_button_click.show_modal()`.
        class_: Extra CSS classes for the button.
        variant: Button variant.
        **kwargs: Extra attributes for the button.
    """
    if onclick is not None:
        if dialog_id is None:
            raise ValueError("dialog_id is required when onclick is set")

        click = onclick(dialog_id)
        if click is not None:
            kwargs["onclick"] = click

    if variant is not None:
        kwargs["data_variant"] = variant

    return html.button(
        title,
        class_=join_classes("btn", class_),
        type="button",
        **kwargs,
    )