Skip to content

Commit 2259a2b

Browse files
authored
ENG-6154: Input component (#22)
1 parent cfefa2a commit 2259a2b

File tree

3 files changed

+199
-2
lines changed

3 files changed

+199
-2
lines changed

reflex_ui/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"components.base.checkbox": ["checkbox"],
1111
"components.base.dialog": ["dialog"],
1212
"components.base.gradient_profile": ["gradient_profile"],
13+
"components.base.input": ["input"],
1314
"components.base.link": ["link"],
1415
"components.base.popover": ["popover"],
1516
"components.base.scroll_area": ["scroll_area"],

reflex_ui/components/base/input.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""Custom input component."""
2+
3+
from typing import Literal
4+
5+
from reflex.components.component import Component, ComponentNamespace
6+
from reflex.components.el import Button, Div, Span
7+
from reflex.components.el import Input as ReflexInput
8+
from reflex.event import EventHandler, passthrough_event_spec, set_focus, set_value
9+
from reflex.utils.imports import ImportVar
10+
from reflex.vars import Var
11+
from reflex.vars.base import get_unique_variable_name
12+
13+
from reflex_ui.components.base_ui import PACKAGE_NAME, BaseUIComponent
14+
from reflex_ui.components.icons.hugeicon import hi
15+
from reflex_ui.utils.twmerge import cn
16+
17+
INPUT_SIZE_VARIANTS = {
18+
"xs": "px-1.5 h-7 rounded-xs gap-1.5",
19+
"sm": "px-2 h-8 rounded-sm gap-2",
20+
"md": "px-2.5 h-9 rounded-md gap-2",
21+
"lg": "px-3 h-10 rounded-lg gap-2.5",
22+
"xl": "px-3.5 h-12 rounded-xl gap-3",
23+
}
24+
25+
LiteralControlSize = Literal["xs", "sm", "md", "lg", "xl"]
26+
27+
DEFAULT_INPUT_ATTRS = {
28+
"autoComplete": "off",
29+
"autoCapitalize": "none",
30+
"autoCorrect": "off",
31+
"spellCheck": "false",
32+
}
33+
34+
35+
class ClassNames:
36+
"""Class names for input components."""
37+
38+
INPUT = "outline-none bg-transparent text-secondary-12 placeholder:text-secondary-9 text-sm leading-normal peer disabled:text-secondary-8 disabled:placeholder:text-secondary-8 w-full data-[disabled]:pointer-events-none font-medium"
39+
DIV = "flex flex-row items-center focus-within:shadow-[0px_0px_0px_2px_var(--primary-4)] focus-within:border-primary-a6 not-data-[invalid]:focus-within:hover:border-primary-a6 bg-secondary-1 shrink-0 border border-secondary-a4 hover:border-secondary-a6 transition-all text-secondary-9 [&_svg]:pointer-events-none has-data-[disabled]:border-secondary-4 has-data-[disabled]:bg-secondary-3 has-data-[disabled]:text-secondary-8 has-data-[disabled]:cursor-not-allowed cursor-text has-data-[invalid]:border-destructive-10 has-data-[invalid]:focus-within:border-destructive-a11 has-data-[invalid]:focus-within:shadow-[0px_0px_0px_2px_var(--destructive-4)] has-data-[invalid]:hover:border-destructive-a11"
40+
41+
42+
class InputBaseComponent(BaseUIComponent):
43+
"""Base component for an input."""
44+
45+
library = f"{PACKAGE_NAME}/input"
46+
47+
@property
48+
def import_var(self):
49+
"""Return the import variable for the input component."""
50+
return ImportVar(tag="Input", package_path="", install=False)
51+
52+
53+
class InputRoot(InputBaseComponent, ReflexInput):
54+
"""Root component for an input."""
55+
56+
tag = "Input"
57+
on_value_change: EventHandler[passthrough_event_spec(str, dict)]
58+
render_: Var[Component]
59+
60+
@classmethod
61+
def create(cls, *children, **props) -> ReflexInput:
62+
"""Create a high level input component with simplified API."""
63+
props["data-slot"] = "input"
64+
cls.set_class_name(ClassNames.INPUT, props)
65+
return super().create(*children, **props)
66+
67+
68+
class HighLevelInput(InputBaseComponent):
69+
"""High level wrapper for the Input component with simplified API."""
70+
71+
# Size of the input.
72+
size: Var[LiteralControlSize]
73+
74+
# Icon to display in the input.
75+
icon: Var[str]
76+
77+
# Whether to show the clear button.
78+
show_clear_button: Var[bool]
79+
80+
# Events to fire when the clear button is clicked.
81+
clear_events: Var[list[EventHandler]]
82+
83+
_el_input_props = {
84+
"default_value",
85+
"on_value_change",
86+
"accept",
87+
"alt",
88+
"auto_complete",
89+
"auto_focus",
90+
"capture",
91+
"checked",
92+
"default_checked",
93+
"form",
94+
"form_action",
95+
"form_enc_type",
96+
"form_method",
97+
"form_no_validate",
98+
"form_target",
99+
"list",
100+
"max",
101+
"max_length",
102+
"min_length",
103+
"min",
104+
"multiple",
105+
"pattern",
106+
"placeholder",
107+
"read_only",
108+
"required",
109+
"src",
110+
"step",
111+
"type",
112+
"value",
113+
"on_key_down",
114+
"on_key_up",
115+
"on_change",
116+
"on_focus",
117+
"on_blur",
118+
"disabled",
119+
"name",
120+
}
121+
122+
@classmethod
123+
def create(cls, *children, **props) -> Div:
124+
"""Create a high level input component with simplified API."""
125+
# Extract and prepare input props
126+
input_props = {k: props.pop(k) for k in cls._el_input_props & props.keys()}
127+
128+
# Extract component props
129+
icon = props.pop("icon", "")
130+
size = props.pop("size", "md")
131+
id = props.pop("id", get_unique_variable_name())
132+
class_name = props.pop("class_name", "")
133+
show_clear_button = props.pop("show_clear_button", True)
134+
clear_events = props.pop("clear_events", [])
135+
# Configure input with merged attributes
136+
input_props.update(
137+
{
138+
"id": id,
139+
"custom_attrs": {
140+
**DEFAULT_INPUT_ATTRS,
141+
**input_props.get("custom_attrs", {}),
142+
},
143+
}
144+
)
145+
146+
return Div.create(
147+
(
148+
Span.create(
149+
hi(icon, class_name="text-secondary-9 size-4 pointer-events-none"),
150+
aria_hidden="true",
151+
)
152+
if icon
153+
else None
154+
),
155+
InputRoot.create(**input_props),
156+
(cls._create_clear_button(id, clear_events) if show_clear_button else None),
157+
*children,
158+
on_click=set_focus(id),
159+
class_name=cn(f"{ClassNames.DIV} {INPUT_SIZE_VARIANTS[size]}", class_name),
160+
**props,
161+
)
162+
163+
@staticmethod
164+
def _create_clear_button(id: str, clear_events: list[EventHandler]) -> Component:
165+
"""Create the clear button component."""
166+
return Button.create(
167+
hi("CancelCircleIcon"),
168+
type="reset",
169+
on_click=[
170+
set_value(id, ""),
171+
set_focus(id).stop_propagation,
172+
*clear_events,
173+
],
174+
class_name="opacity-100 peer-placeholder-shown:opacity-0 hover:text-secondary-12 transition-colors peer-placeholder-shown:pointer-events-none peer-disabled:pointer-events-none peer-disabled:opacity-0 h-full",
175+
)
176+
177+
def _exclude_props(self) -> list[str]:
178+
return [
179+
*super()._exclude_props(),
180+
"size",
181+
"icon",
182+
"show_clear_button",
183+
"clear_events",
184+
]
185+
186+
187+
class Input(ComponentNamespace):
188+
"""Namespace for Input components."""
189+
190+
root = staticmethod(InputRoot.create)
191+
__call__ = staticmethod(HighLevelInput.create)
192+
193+
194+
input = Input()

reflex_ui/components/base/select.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class ClassNames:
2828
VALUE = "flex-1 text-left"
2929
ICON = "flex size-4 text-secondary-10 group-data-[disabled]/trigger:text-current"
3030
POPUP = "group/popup max-h-[17.25rem] overflow-y-auto origin-(--transform-origin) p-1 border border-secondary-a4 bg-secondary-1 shadow-large transition-[transform,scale,opacity] data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 outline-none scrollbar-thin scrollbar-thumb-secondary-9 scrollbar-track-transparent"
31-
ITEM = "grid min-w-(--anchor-width) grid-cols-[1fr_auto] items-center gap-2 text-sm select-none font-[450] group-data-[side=none]/popup:min-w-[calc(var(--anchor-width)+1rem)] data-[selected]:text-secondary-12 text-secondary-11 cursor-pointer placeholder:text-secondary-11 data-[selected]:font-medium outline-none data-[highlighted]:bg-secondary-3 scroll-m-1"
31+
ITEM = "grid min-w-(--anchor-width) grid-cols-[1fr_auto] items-center gap-2 text-sm select-none font-[450] group-data-[side=none]/popup:min-w-[calc(var(--anchor-width)+1rem)] data-[selected]:text-secondary-12 text-secondary-11 cursor-pointer placeholder:text-secondary-9 data-[selected]:font-medium outline-none data-[highlighted]:bg-secondary-3 scroll-m-1"
3232
ITEM_INDICATOR = "text-current"
3333
ITEM_TEXT = "text-start"
3434
GROUP = "p-1"
@@ -504,7 +504,9 @@ def create(cls, *children, **props) -> Component:
504504
SelectTrigger.create(
505505
render_=button(
506506
SelectValue.create(),
507-
SelectIcon.create(select_arrow(class_name="size-4")),
507+
SelectIcon.create(
508+
select_arrow(class_name="size-4 text-secondary-9")
509+
),
508510
variant="outline",
509511
size=size,
510512
class_name=ClassNames.TRIGGER,

0 commit comments

Comments
 (0)