Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Keep the Contact Sales demo dialog within the mobile viewport, with scrolling form fields and an always-visible header and Submit button.
Original file line number Diff line number Diff line change
Expand Up @@ -292,12 +292,33 @@ def demo_form(
Returns:
A Reflex form component with all demo form fields
"""
return _demo_form(id_prefix=id_prefix, on_submit=on_submit, **props)


def _demo_form(
id_prefix: str = "",
on_submit: EventType[dict[str, Any]] | None = None,
*,
in_dialog: bool = False,
**props,
) -> rx.Component:
"""Build the shared form with an optional scrolling dialog body.

Args:
id_prefix: Prefix for element IDs, generated when empty.
on_submit: Additional submit handlers.
in_dialog: Whether to separate scrolling fields from a fixed footer.
**props: Additional form properties.

Returns:
The form wrapped in a fragment.
"""
prefix = id_prefix or get_unique_variable_name()
email_id = f"{prefix}_user_email"
extra_on_submit = (
on_submit if isinstance(on_submit, list) else [on_submit] if on_submit else []
)
form = rx.el.form(
fields = (
rx.el.div(
input_field("First name", "John", "first_name", "text", True),
input_field("Last name", "Smith", "last_name", "text", True),
Expand Down Expand Up @@ -367,13 +388,27 @@ def demo_form(
class_name="text-destructive-10 text-sm font-medium px-2 py-1 rounded-md bg-destructive-3 border border-destructive-4",
),
),
button(
"Submit",
type="submit",
class_name="w-full",
)
submit = button("Submit", type="submit", class_name="w-full")
form = rx.el.form(
*(
(
rx.el.div(
*fields,
class_name="flex min-h-0 flex-col lg:gap-6 gap-2 overflow-y-auto overscroll-contain p-6 pt-2",
),
rx.el.div(
submit,
class_name="shrink-0 border-t border-secondary-a4 bg-secondary-1 px-6 py-4",
),
)
if in_dialog
else (*fields, submit)
),
class_name=cn(
"@container flex flex-col lg:gap-6 gap-2 p-6",
"@container flex min-h-0 flex-1 flex-col overflow-hidden"
if in_dialog
else "@container flex flex-col lg:gap-6 gap-2 p-6",
props.pop("class_name", ""),
),
on_submit=[
Expand Down Expand Up @@ -413,29 +448,27 @@ def demo_form_dialog(
dialog.backdrop(),
dialog.popup(
rx.el.div(
rx.el.div(
rx.el.h1(
"Book a Demo",
class_name="text-xl font-bold text-secondary-12",
),
dialog.close(
render_=button(
hi("Cancel01Icon"),
variant="ghost",
size="icon-sm",
class_name="text-secondary-11",
),
),
class_name="flex flex-row justify-between items-center gap-1 px-6 pt-4 -mb-4",
rx.el.h1(
"Book a Demo",
class_name="text-xl font-bold text-secondary-12",
),
demo_form(
id_prefix=id_prefix,
on_submit=on_submit,
class_name="w-full max-w-md",
dialog.close(
render_=button(
hi("Cancel01Icon"),
variant="ghost",
size="icon-sm",
class_name="text-secondary-11",
),
),
class_name="relative isolate overflow-hidden -m-px w-full max-w-md",
class_name="flex shrink-0 flex-row justify-between items-center gap-1 px-6 pt-4",
),
_demo_form(
id_prefix=id_prefix,
on_submit=on_submit,
in_dialog=True,
class_name="w-full max-w-md",
),
class_name="h-fit mt-1 overflow-hidden w-full max-w-md",
class_name="flex flex-col h-fit max-h-[calc(100dvh-2rem)] mt-0 overflow-hidden w-full max-w-md",
),
),
open=demo_form_open_cs.value,
Expand Down
88 changes: 88 additions & 0 deletions tests/integration/tests_playwright/test_demo_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Browser regression tests for the Contact Sales dialog on mobile."""

from collections.abc import Generator

import pytest
from playwright.sync_api import Page, expect

from reflex.testing import AppHarness


def DemoFormApp():
"""Render the shared sales dialog with a trigger."""
from importlib import reload

from reflex_components_internal.blocks.demo_form import demo_form_dialog
from reflex_components_internal.components.icons import others

import reflex as rx

# AppHarness clears memo registrations but leaves package modules cached.
reload(others)

def index():
"""Return the sales dialog.

Returns:
The dialog and its trigger.
"""
return demo_form_dialog(trigger=rx.el.button("Contact sales"))

app = rx.App()
app.add_page(index)


@pytest.fixture(scope="module")
def demo_form_app(
app_harness_env: type[AppHarness],
tmp_path_factory: pytest.TempPathFactory,
) -> Generator[AppHarness, None, None]:
"""Run the sales dialog app in dev and production modes.

Args:
app_harness_env: Harness class for the current environment.
tmp_path_factory: Temporary directory factory.

Yields:
The running app harness.
"""
with app_harness_env.create(
root=tmp_path_factory.mktemp("demo_form"), app_source=DemoFormApp
) as harness:
yield harness


@pytest.mark.parametrize("viewport_height", [400, 664])
def test_mobile_fields_scroll_with_header_and_submit_visible(
demo_form_app: AppHarness, page: Page, viewport_height: int
) -> None:
"""Scroll to the last field without losing the dialog title or Submit.

Args:
demo_form_app: The running sales dialog app.
page: Browser page.
viewport_height: Mobile viewport height in pixels.
"""
assert demo_form_app.frontend_url is not None
page.set_viewport_size({"width": 390, "height": viewport_height})
page.goto(demo_form_app.frontend_url)
page.get_by_role("button", name="Contact sales", exact=True).click()

dialog = page.get_by_role("dialog")
header = dialog.get_by_role("heading", name="Book a Demo")
submit = dialog.get_by_role("button", name="Submit", exact=True)
last_field = dialog.locator('select[name="technical_level"]')

expect(dialog).to_be_in_viewport(ratio=1)
expect(header).to_be_in_viewport(ratio=1)
expect(submit).to_be_in_viewport(ratio=1)
expect(last_field).not_to_be_in_viewport()

dialog.hover()
page.mouse.wheel(0, 10000)

expect(last_field).to_be_in_viewport(ratio=1)
expect(header).to_be_in_viewport(ratio=1)
expect(submit).to_be_in_viewport(ratio=1)
last_field.select_option("Technical")
expect(last_field).to_have_value("Technical")
11 changes: 11 additions & 0 deletions tests/units/reflex_components_internal/blocks/test_demo_form.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for the demo form block."""

import re
from typing import cast

from reflex_base.vars.sequence import LiteralStringVar
from reflex_components_internal.blocks.demo_form import demo_form, demo_form_dialog
Expand Down Expand Up @@ -37,6 +38,16 @@ def test_demo_form_dialog_renders_given_trigger() -> None:
assert TRIGGER_LABEL in rendered


def test_demo_form_standalone_retains_its_layout() -> None:
"""Embedding the form keeps its padding overrides and inline submit button."""
form = demo_form(class_name="p-0").children[0]

assert "p-0" in str(cast(rx.Component, form).class_name)
assert 'type:"submit"' in str(form.children[-1])
assert "overflow-hidden" not in str(cast(rx.Component, form).class_name)
assert "grid-cols-2" in str(cast(rx.Component, form.children[0]).class_name)


def test_demo_form_asks_for_an_optional_phone_number() -> None:
"""The phone number is a tel input that the form can be submitted without."""
rendered = str(demo_form())
Expand Down
Loading