diff --git a/packages/reflex-components-internal/src/reflex_components_internal/__init__.py b/packages/reflex-components-internal/src/reflex_components_internal/__init__.py index 758bed3a180..864a2a5d579 100644 --- a/packages/reflex-components-internal/src/reflex_components_internal/__init__.py +++ b/packages/reflex-components-internal/src/reflex_components_internal/__init__.py @@ -18,6 +18,7 @@ "components.base.link": ["link"], "components.base.menu": ["menu"], "components.base.navigation_menu": ["navigation_menu"], + "components.base.otp_field": ["otp_field"], "components.base.popover": ["popover"], "components.base.preview_card": ["preview_card"], "components.base.scroll_area": ["scroll_area"], diff --git a/packages/reflex-components-internal/src/reflex_components_internal/components/base/otp_field.py b/packages/reflex-components-internal/src/reflex_components_internal/components/base/otp_field.py new file mode 100644 index 00000000000..11de5bfe302 --- /dev/null +++ b/packages/reflex-components-internal/src/reflex_components_internal/components/base/otp_field.py @@ -0,0 +1,206 @@ +"""Custom OTP field component.""" + +from typing import Literal + +from reflex.components.component import Component, ComponentNamespace +from reflex.event import EventHandler, passthrough_event_spec +from reflex.utils.imports import ImportVar +from reflex.vars.base import Var +from reflex_components_internal.components.base_ui import PACKAGE_NAME, BaseUIComponent + +LiteralValidationType = Literal["numeric", "alpha", "alphanumeric", "none"] +LiteralInputMode = Literal[ + "none", "text", "tel", "url", "email", "numeric", "decimal", "search" +] +LiteralOrientation = Literal["horizontal", "vertical"] + +on_value_event_spec = passthrough_event_spec(str, dict) + + +class ClassNames: + """Class names for OTP field components.""" + + ROOT = "flex flex-row items-center gap-2" + INPUT = "size-10 rounded-ui-md border border-secondary-4 bg-white dark:bg-secondary-3 text-secondary-12 text-center text-base font-medium outline-none transition-[color,box-shadow] hover:border-secondary-a6 focus:border-primary-a6 focus:shadow-[0px_0px_0px_2px_var(--primary-4)] data-[disabled]:cursor-not-allowed data-[disabled]:border-secondary-4 data-[disabled]:bg-secondary-3 data-[disabled]:text-secondary-8 data-[invalid]:border-destructive-10 data-[invalid]:focus:border-destructive-a11 data-[invalid]:focus:shadow-[0px_0px_0px_2px_var(--destructive-4)] data-[invalid]:hover:border-destructive-a11 shadow-[0_1px_2px_0_rgba(0,0,0,0.02),0_1px_4px_0_rgba(0,0,0,0.02)] dark:shadow-none dark:border-secondary-5" + SEPARATOR = "text-secondary-9 text-base font-medium select-none" + + +class OTPFieldBaseComponent(BaseUIComponent): + """Base component for OTP field components.""" + + library = f"{PACKAGE_NAME}/otp-field" + + @property + def import_var(self): + """Return the import variable for the OTP field component.""" + return ImportVar(tag="OTPFieldPreview", package_path="", install=False) + + +class OTPFieldRoot(OTPFieldBaseComponent): + """Container that manages state for one-time password entry across multiple slots. Renders a div.""" + + tag = "OTPFieldPreview.Root" + + # The number of input slots. Required. + length: Var[int] + + # Identifies the field when a form is submitted. + name: Var[str] + + # The controlled OTP value. To render an uncontrolled field, use the default_value prop instead. + value: Var[str] + + # The uncontrolled initial value. To render a controlled field, use the value prop instead. + default_value: Var[str] + + # Browser autocomplete hint. Defaults to "one-time-code". + auto_complete: Var[str] + + # The virtual keyboard type for the inputs. + input_mode: Var[LiteralInputMode] + + # Input validation rule. Defaults to "numeric". + validation_type: Var[LiteralValidationType] + + # Whether characters are obscured while typing. Defaults to False. + mask: Var[bool] + + # Whether the form is auto-submitted when all slots fill. Defaults to False. + auto_submit: Var[bool] + + # The id of a