diff --git a/assets/docs/components/field.md b/assets/docs/components/field.md index ce01dde..4fade29 100644 --- a/assets/docs/components/field.md +++ b/assets/docs/components/field.md @@ -134,7 +134,7 @@ class ClassNames: FIELD_CONTENT = "group/field-content flex flex-1 flex-col gap-0.5 leading-snug" - FIELD_LABEL = "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border border-input *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col" + FIELD_LABEL = "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[:checked]:border-primary/30 has-[:checked]:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border border-input *:data-[slot=field]:p-2.5 dark:has-[:checked]:border-primary/20 dark:has-[:checked]:bg-primary/10 has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col" FIELD_TITLE = "flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50" @@ -296,51 +296,50 @@ field = Field() A single control with label, helper text, and validation. -```python +```text field.root -├── field.label -├── Input / Textarea / switch.root / select.root -├── field.description -└── field.error + |- field.label + |- Input / Textarea / switch.root / select.root + |- field.description + \- field.error ``` ## field.group Related fields in one group. Use `field.separator` between sections when needed. -```python +```text field.group -├── field.root -│ ├── field.label -│ ├── Input / Textarea / switch.root / select.root -│ ├── field.description -│ └── field.error -├── field.separator -└── field.root - ├── field.label - └── Input / Textarea / switch.root / select.root + |- field.root + | |- field.label + | |- Input / Textarea / switch.root / select.root + | |- field.description + | \- field.error + |- field.separator + \- field.root + |- field.label + \- Input / Textarea / switch.root / select.root ``` ## field.set Semantic grouping with a legend and description, usually containing a `field.group`. -```python +```text field.set -├── field.legend -├── field.description -└── field.group - ├── field.root - │ ├── field.label - │ ├── Input / Textarea / switch.root / select.root - │ ├── field.description - │ └── field.error - └── field.root - ├── field.label - └── Input / Textarea / switch.root / select.root + |- field.legend + |- field.description + \- field.group + |- field.root + | |- field.label + | |- Input / Textarea / switch.root / select.root + | |- field.description + | \- field.error + \- field.root + |- field.label + \- Input / Textarea / switch.root / select.root ``` - - `field.root` is the core wrapper for a single field. - `field.content` is a flex column that groups label and description. Not required if you have no description. @@ -501,9 +500,9 @@ def field_demo() -> rx.Component: # Responsive Layout -- **Vertical fields**: Default orientation stacks label, control, and helper text—ideal for mobile-first layouts. +- **Vertical fields**: Default orientation stacks label, control, and helper text—ideal for mobile-first layouts. -- **Horizontal fields**: Set `orientation="horizontal"` on `field.root` to align the label and control side-by-side. Pair with `field.content` to keep descriptions aligned. +- **Horizontal fields**: Set `orientation="horizontal"` on `field.root` to align the label and control side-by-side. Pair with `field.content` to keep descriptions aligned. - **Responsive fields**: Set `orientation="responsive"` for automatic column layouts inside container-aware parents. @@ -530,9 +529,9 @@ field.root( Container that renders a semantic `fieldset` with spacing presets. -| Prop | Type | Default | -| --- | --- | --- | -| `class_name` | `str` | | +| Prop | Type | Default | +| ------------ | ----- | ------- | +| `class_name` | `str` | | ```python field.set( @@ -545,10 +544,10 @@ field.set( Legend element for a `field.set`. Switch to the `"label"` variant to align with standard label sizing. -| Prop | Type | Default | -| --- | --- | --- | -| `variant` | `Literal["legend", "label"]` | `"legend"` | -| `class_name` | `str` | | +| Prop | Type | Default | +| ------------ | ---------------------------- | ---------- | +| `variant` | `Literal["legend", "label"]` | `"legend"` | +| `class_name` | `str` | | ```python field.legend("Notification Preferences", variant="label") @@ -560,9 +559,9 @@ The `field.legend` has two variants: `legend` and `label`. The `label` variant a Layout wrapper that stacks `field.root` components and enables container queries for responsive orientations. -| Prop | Type | Default | -| --- | --- | --- | -| `class_name` | `str` | | +| Prop | Type | Default | +| ------------ | ----- | ------- | +| `class_name` | `str` | | ```python field.group( @@ -576,11 +575,11 @@ field.group( The core wrapper for a single field. Provides orientation control, invalid state styling, and spacing configurations. -| Prop | Type | Default | -| --- | --- | --- | -| `orientation` | `"vertical" | "horizontal" | "responsive"` | `"vertical"` | -| `class_name` | `str` | | -| `data_invalid` | `str` | | +| Prop | Type | Default | +| -------------- | ----------- | ------------ | ------------- | ------------ | +| `orientation` | `"vertical" | "horizontal" | "responsive"` | `"vertical"` | +| `class_name` | `str` | | +| `data_invalid` | `str` | | ```python field.root( @@ -594,9 +593,9 @@ field.root( Flex column that groups control and descriptions when the label sits beside the control. Not required if you have no layout description block. -| Prop | Type | Default | -| --- | --- | --- | -| `class_name` | `str` | | +| Prop | Type | Default | +| ------------ | ----- | ------- | +| `class_name` | `str` | | ```python field.root( @@ -612,10 +611,10 @@ field.root( Label styled for both direct inputs and nested `field` child items. -| Prop | Type | Default | -| --- | --- | --- | -| `html_for` | `str` | | -| `class_name` | `str` | | +| Prop | Type | Default | +| ------------ | ----- | ------- | +| `html_for` | `str` | | +| `class_name` | `str` | | ```python field.label("Email", html_for="email") @@ -625,9 +624,9 @@ field.label("Email", html_for="email") Renders a standalone title with matching label typography properties inside a `field.content` node block. -| Prop | Type | Default | -| --- | --- | --- | -| `class_name` | `str` | | +| Prop | Type | Default | +| ------------ | ----- | ------- | +| `class_name` | `str` | | ```python field.content( @@ -640,9 +639,9 @@ field.content( Helper text slot that automatically line-balances lengthy strings cleanly when utilized inside horizontal configurations. -| Prop | Type | Default | -| --- | --- | --- | -| `class_name` | `str` | | +| Prop | Type | Default | +| ------------ | ----- | ------- | +| `class_name` | `str` | | ```python field.description("We never share your email with anyone.") @@ -652,9 +651,9 @@ field.description("We never share your email with anyone.") Visual divider rule used to separate sections or categories inside a wrapping `field.group` component. Accepts optional inline children contents. -| Prop | Type | Default | -| --- | --- | --- | -| `class_name` | `str` | | +| Prop | Type | Default | +| ------------ | ----- | ------- | +| `class_name` | `str` | | ```python field.separator("Or continue with") @@ -664,9 +663,9 @@ field.separator("Or continue with") Accessible error notification typography container block configured automatically with standard application state layout variables (`role="alert"`). -| Prop | Type | Default | -| --- | --- | --- | -| `class_name` | `str` | | +| Prop | Type | Default | +| ------------ | ----- | ------- | +| `class_name` | `str` | | ```python field.error("Invalid passcode combination provided.") diff --git a/assets/docs/components/menu.md b/assets/docs/components/menu.md index 5b2e647..1a7f3db 100644 --- a/assets/docs/components/menu.md +++ b/assets/docs/components/menu.md @@ -193,27 +193,21 @@ class MenuItem(CoreComponent): props["data-slot"] = "menu-item" cls.set_class_name(ClassNames.ITEM, props) - # 0. Pop out the variant prop so we can apply the data-variant attribute variant = props.pop("variant", "default") - props["data-variant"] = ( - variant # This maps to your Tailwind: data-[variant=destructive] - ) + props["data-variant"] = variant item_id = props.get("id") or f"menu-item-{next(_menu_item_counter)}" props["id"] = item_id - # 1. Pop out any existing user on_click triggers user_on_click = props.pop("on_click", None) click_events = [] - # 2. Append the user's custom event(s) if they provided any if user_on_click is not None: if isinstance(user_on_click, list): click_events.extend(user_on_click) else: click_events.append(user_on_click) - # 3. If close_on_click is active, append the parent-closing script if close_on_click: close_script = rx.call_script( f""" @@ -224,7 +218,6 @@ class MenuItem(CoreComponent): ) click_events.append(close_script) - # 4. Bind the combined chain back to props if there's anything to execute if click_events: props["on_click"] = click_events diff --git a/assets/docs/components/radio-group.md b/assets/docs/components/radio-group.md new file mode 100644 index 0000000..02f14ad --- /dev/null +++ b/assets/docs/components/radio-group.md @@ -0,0 +1,371 @@ +--- +title: "Radio Group" +description: "A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time." +order: 0 +--- + + +## Radio Group, A Set Of Checkable Buttons—Known As Radio Buttons—Where No More Than One Of The Buttons Can Be Checked At A Time. + + +```python +from components.ui.radio_group import radio_group +``` + +```python +from typing import Any + +from reflex.components.component import Component +from reflex.utils.imports import ImportVar +from reflex.vars import FunctionVar, Var +from reflex.vars.base import VarData + +PACKAGE_CN = "clsx-for-tailwind@1.0.0" +CN = Var( + "cn", + _var_data=VarData( + imports={ + PACKAGE_CN: ImportVar(tag="cn"), + }, + ), +).to(FunctionVar) + + +class CoreComponent(Component): + unstyled: Var[bool] + + @classmethod + def set_class_name( + cls, default_class_name: str | Var[str], props: dict[str, Any] + ) -> None: + + if "render_" in props: + return + + props_class_name = props.get("class_name", "") + + if props.pop("unstyled", False): + props["class_name"] = props_class_name + return + + props["class_name"] = cn(default_class_name, props_class_name) + + def _exclude_props(self) -> list[str]: + return [ + *super()._exclude_props(), + "unstyled", + ] + + +def cn(*classes: Var | str | tuple | list | None) -> Var: + return CN.call(*classes).to(str) +``` + +```python +import reflex as rx +from reflex.components.component import ComponentNamespace + +from ..core.core import CoreComponent, cn + + +class ClassNames: + ROOT = "w-full flex flex-col gap-3 disabled:opacity-50 disabled:cursor-not-allowed text-sm" + + ITEM_ROOT = ( + "group relative inline-flex size-4 shrink-0 cursor-pointer items-center " + "justify-center rounded-full border border-input " + "has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 " + "has-[:focus-visible]:border-ring has-[:focus-visible]:ring-3 has-[:focus-visible]:ring-ring/50 " + "has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:ring-3 " + "has-[[aria-invalid=true]]:ring-destructive/20 " + "dark:bg-input/30 " + "has-[:checked]:border-primary " + "peer-has-[[data-slot=field-content]]:mt-0.5 [&:has(~[data-slot=field-content])]:mt-0.5" + ) + + ITEM_INPUT = "peer sr-only" + + ITEM_INDICATOR = "hidden peer-checked:flex items-center justify-center" + + ITEM_INDICATOR_DOT = "size-2 rounded-full bg-primary" + + _KNOWN_INPUT_PROPS = ( + "checked", + "default_checked", + "disabled", + "required", + "id", + ) + + +class RadioGroupRoot(CoreComponent): + @classmethod + def create(cls, *children, disabled: bool = False, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") + props["data-slot"] = "radio-group" + props["role"] = "radiogroup" + if disabled: + props["disabled"] = True + cls.set_class_name(cn(ClassNames.ROOT, custom_classes), props) + return rx.el.fieldset(*children, **props) + + +class RadioGroupItem(CoreComponent): + @classmethod + def create(cls, *children, name: str, value: str, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") + + input_props = {"name": name, "value": value} + for key in list(props.keys()): + if ( + key in ClassNames._KNOWN_INPUT_PROPS + or key.startswith("on_") + or key.startswith("data-") + or key.startswith("aria-") + ): + input_props[key] = props.pop(key) + + input_props["type"] = "radio" + input_props["data-slot"] = "radio-group-item-input" + input_props["class_name"] = ClassNames.ITEM_INPUT + + props["data-slot"] = "radio-group-item" + + if not children: + children = (RadioGroupIndicator.create(),) + + cls.set_class_name(cn(ClassNames.ITEM_ROOT, custom_classes), props) + return rx.el.label(rx.el.input(**input_props), *children, **props) + + +class RadioGroupIndicator(CoreComponent): + @classmethod + def create(cls, *children, **props) -> rx.Component: + if len(children) == 0: + children = (rx.el.span(class_name=ClassNames.ITEM_INDICATOR_DOT),) + props["data-slot"] = "radio-group-item-indicator" + cls.set_class_name(ClassNames.ITEM_INDICATOR, props) + return rx.el.span(*children, **props) + + +class RadioGroup(ComponentNamespace): + root = staticmethod(RadioGroupRoot.create) + item = staticmethod(RadioGroupItem.create) + indicator = staticmethod(RadioGroupIndicator.create) + class_names = ClassNames + + +radio_group = RadioGroup() +``` + +# Examples + +## Description + +Radio group items with a description using the `Field` component. + +```python +def radio_group_description() -> rx.Component: + return radio_group.root( + *[ + field.root( + radio_group.item( + name="spacing", + value=value, + id=f"desc-{value}", + default_checked=(value == "comfortable"), + ), + field.content( + field.label(title, html_for=f"desc-{value}"), + field.description(description), + ), + orientation="horizontal", + ) + for value, title, description in OPTIONS + ], + class_name="w-fit", + ) +``` + +## Choice Card + +Use `field.label` to wrap the entire `Field` for a clickable card-style selection. + +```python +def radio_group_choice_card() -> rx.Component: + return radio_group.root( + *[ + field.label( + field.root( + field.content( + field.title(title), + field.description(description), + ), + radio_group.item( + name="plan", + value=value, + id=f"{value}-plan", + default_checked=(value == "plus"), + ), + orientation="horizontal", + ), + html_for=f"{value}-plan", + ) + for value, title, description in PLANS + ], + class_name="max-w-sm", + ) +``` + +## Fieldset + +Use `field.set` and `field.legend` to group radio items with a label and description. + +```python +def radio_group_fieldset() -> rx.Component: + return field.set( + field.legend("Subscription Plan", variant="label"), + field.description("Yearly and lifetime plans offer significant savings."), + radio_group.root( + *[ + field.root( + radio_group.item( + name="plan", + value=value, + id=f"plan-{value}", + default_checked=(value == "monthly"), + ), + field.label( + label, html_for=f"plan-{value}", class_name="font-normal" + ), + orientation="horizontal", + ) + for value, label in PLANS + ], + ), + class_name="w-full max-w-xs", + ) +``` + +## Disabled + +Use the `disabled` prop on `radio.root` to disable all items. + +```python +def radio_group_disabled() -> rx.Component: + return radio_group.root( + *[ + field.root( + radio_group.item( + name="disabled-demo", + value=value, + id=id_, + disabled=is_disabled, + default_checked=(value == "option2"), + ), + field.label(label, html_for=id_, class_name="font-normal"), + orientation="horizontal", + **({"data-disabled": "true"} if is_disabled else {}), + ) + for value, id_, label, is_disabled in OPTIONS + ], + class_name="w-fit", + ) +``` + +## Invalid + +Use `aria-invalid` on `radio.item` and `data-invalid` on `field.root` to show validation errors. + +```python +def radio_group_invalid() -> rx.Component: + return field.set( + field.legend("Notification Preferences", variant="label"), + field.description("Choose how you want to receive notifications."), + radio_group.root( + *[ + field.root( + radio_group.item( + name="notification-preferences", + value=value, + id_=id_, + default_checked=(value == "email"), + aria_invalid=True, + ), + field.label(label, html_for=id_, class_name="font-normal"), + orientation="horizontal", + data_invalid=True, + ) + for value, id_, label in OPTIONS + ], + ), + class_name="w-full max-w-xs", + ) +``` + +# API Reference + +## radio_group.root + +The container element for a set of radio options. Renders a native `
`. Setting `disabled=True` on the root automatically disables all descendant radio controls without needing to set it on each item individually. + +```python +radio_group.root( + radio_group.item(name="plan", value="free", id_="plan-free"), + radio_group.item(name="plan", value="pro", id_="plan-pro"), + disabled=False, +) +``` + +| Prop | Type | Default | +| ------------ | ------ | --------------- | +| `disabled` | `bool` | `False` | +| `class_name` | `str` | `""` | +| `data_slot` | `str` | `"radio-group"` | +| `role` | `str` | `"radiogroup"` | + +Any additional HTML props passed to `radio_group.root` are forwarded directly to the underlying `
`. + +## radio_group.item + +Represents an individual radio control wrapped in a native `