diff --git a/ludic/catalog/forms.py b/ludic/catalog/forms.py index d773134..be99316 100644 --- a/ludic/catalog/forms.py +++ b/ludic/catalog/forms.py @@ -2,7 +2,7 @@ from collections.abc import Callable, Mapping from dataclasses import dataclass -from typing import Any, Literal, get_type_hints, override +from typing import Any, Literal, NotRequired, get_type_hints, override from ludic.attrs import ( Attrs, @@ -13,6 +13,7 @@ TextAreaAttrs, ) from ludic.base import BaseElement +from ludic.catalog.typography import Paragraph from ludic.components import Component from ludic.html import div, form, input, label, option, select, style, textarea from ludic.types import ( @@ -214,6 +215,70 @@ def render(self) -> div: return div(*elements) +class ChoiceFieldAttrs(FieldAttrs, InputAttrs): + """Attributes of the component ``InputField``. + + The attributes are subclassed from :class:`FieldAttrs` and :class:`InputAttrs`. + """ + + choices: list[tuple[str, str]] + selected: NotRequired[str] + + +class ChoiceField(FormField[NoChildren, ChoiceFieldAttrs]): + """Represents the HTML ``input`` element with an optional ``label`` element.""" + + styles = style.use( + lambda theme: { + ".form-field p.form-label": { + "margin-block-end": theme.sizes.xxs, + "font-weight": "bold", + }, + ".form-field * + *": { + "margin-block-start": theme.sizes.xxxxs, + }, + ".form-field input[type=radio]": { + "inline-size": "auto", + "vertical-align": "middle", + "height": theme.sizes.m, + "margin-inline": theme.sizes.m, + }, + ".form-field input[type=radio] + label": { + "display": "inline-block", + "height": theme.sizes.m, + "font-weight": "normal", + "margin-block": "0", + }, + } + ) + + @override + def render(self) -> div: + attrs = self.attrs_for(input) + attrs.setdefault("type", "radio") + + elements: list[ComplexChildren] = [] + + if text := self.attrs.get("label"): + elements.append(Paragraph(text, classes=["form-label"])) + + for value, text in self.attrs["choices"]: + elements.append( + div( + input( + id=value, + value=value, + checked=bool(value and value == self.attrs.get("selected")), + **attrs, + ), + label(text, for_=value), + classes=["choice-field"], + ) + ) + + return div(*elements) + + class TextAreaField(FormField[PrimitiveChildren, TextAreaFieldAttrs]): """Represents the HTML ``textarea`` element with an optional ``label`` element.""" diff --git a/tests/test_catalog.py b/tests/test_catalog.py index a6cff26..a0aa8d8 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -1,4 +1,4 @@ -from ludic.catalog.forms import Form, InputField, TextAreaField +from ludic.catalog.forms import ChoiceField, Form, InputField, TextAreaField from ludic.catalog.headers import H1, H2, H3, H4, Anchor from ludic.catalog.items import Key, Pairs, Value from ludic.catalog.messages import ( @@ -217,3 +217,26 @@ def test_messages() -> None: assert MessageDanger("test").to_html() == ( '
test
' ) + + +def test_choice_field() -> None: + assert ChoiceField( + choices=[("yes", "Yes"), ("no", "No")], + selected="yes", + name="yes_no", + label="Test", + ).to_html() == ( + '
' + '

Test

' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + ) # fmt: skip