Extract _combobox_shell from SearchSelect

Pull the domain-agnostic combobox skeleton (pills region, search box, options
panel with its no-results node, outer container) into a private _combobox_shell
helper. SearchSelect now builds its form-specific pills and option rows and
delegates assembly to the shell. Rendered markup is byte-identical; a structural
test guards the fixed region order so future builders (e.g. a filter variant)
can share the shell without drift.

https://claude.ai/code/session_01XzhXvMvw42CQGc9kmin3GS
This commit is contained in:
Claude
2026-06-07 21:58:43 +00:00
committed by Lukáš Kucharczyk
parent 547894d8d0
commit e2cbd4a9f4
2 changed files with 67 additions and 22 deletions
+53 -22
View File
@@ -84,6 +84,50 @@ def _option_row(option: SearchSelectOption) -> SafeText:
)
def _combobox_shell(
*,
container_attributes: list[HTMLAttribute],
pills: SafeText,
search_attributes: list[HTMLAttribute],
options_children: list[SafeText],
always_visible: bool,
items_visible: int,
) -> SafeText:
"""Assemble the shared, domain-agnostic combobox skeleton.
Every combobox built on top of this shell has the same three regions in the
same order: the ``pills`` region, the search box, and the options panel (which
always carries a trailing no-results node). Callers supply the already-built
``pills`` region, the ``search_attributes`` for the text box, the
``options_children`` (value rows plus any pinned pseudo-options), and the
``container_attributes`` that carry the widget's identity and behaviour flags.
The shell knows nothing about how individual rows or pills look.
"""
search = Component(tag_name="input", attributes=search_attributes)
no_results = Component(
tag_name="div",
attributes=[("data-ss-no-results", ""), ("class", _NO_RESULTS_CLASS)],
children=["No results"],
)
options_class = _OPTIONS_CLASS if always_visible else _OPTIONS_CLASS + " hidden"
options_panel = Component(
tag_name="div",
attributes=[
("data-ss-options", ""),
("style", f"max-height: {items_visible * _ROW_HEIGHT_REM:.2f}rem"),
("class", options_class),
],
children=[*options_children, no_results],
)
return Component(
tag_name="div",
attributes=container_attributes,
children=[pills, search, options_panel],
)
def SearchSelect(
*,
name: str,
@@ -144,27 +188,11 @@ def SearchSelect(
search_attrs.append(("autofocus", ""))
if search_value:
search_attrs.append(("value", search_value))
search = Component(tag_name="input", attributes=search_attrs)
# ── Options panel (pre-rendered only when there is no search_url) ──
option_rows = [_option_row(o) for o in options] if not search_url else []
no_results = Component(
tag_name="div",
attributes=[("data-ss-no-results", ""), ("class", _NO_RESULTS_CLASS)],
children=["No results"],
)
options_class = _OPTIONS_CLASS if always_visible else _OPTIONS_CLASS + " hidden"
options_panel = Component(
tag_name="div",
attributes=[
("data-ss-options", ""),
("style", f"max-height: {items_visible * _ROW_HEIGHT_REM:.2f}rem"),
("class", options_class),
],
children=[*option_rows, no_results],
)
container_attrs: list[HTMLAttribute] = [
container_attributes: list[HTMLAttribute] = [
("data-search-select", ""),
("data-name", name),
("data-search-url", search_url),
@@ -176,12 +204,15 @@ def SearchSelect(
("class", _CONTAINER_CLASS),
]
if id:
container_attrs.append(("id", id))
container_attributes.append(("id", id))
return Component(
tag_name="div",
attributes=container_attrs,
children=[pills, search, options_panel],
return _combobox_shell(
container_attributes=container_attributes,
pills=pills,
search_attributes=search_attrs,
options_children=option_rows,
always_visible=always_visible,
items_visible=items_visible,
)
+14
View File
@@ -106,6 +106,20 @@ class SearchSelectComponentTest(unittest.TestCase):
)
self.assertNotIn('data-ss-option=""', html)
def test_shell_region_order_pills_search_options(self):
# The shared shell assembles the three regions in a fixed order; option
# rows precede the trailing no-results node inside the options panel.
html = SearchSelect(name="t", options=[("1", "One")])
pills = html.index("data-ss-pills")
search = html.index("data-ss-search")
options = html.index("data-ss-options")
option_row = html.index('data-ss-option=""')
no_results = html.index("data-ss-no-results")
self.assertLess(pills, search)
self.assertLess(search, options)
self.assertLess(options, option_row)
self.assertLess(option_row, no_results)
class SearchLabelTest(django.test.TestCase):
@classmethod