Convert onSwap widgets to custom elements (issue #18)

Replaces the four onSwap-based widgets with TypeScript custom elements
following the pattern from PR #16. Each widget gets a class extending
HTMLElement with connectedCallback/disconnectedCallback, typed props via
register_element + gen_element_types codegen, and lives in ts/elements/.

- range-slider: RangeSliderElement; Python uses _RangeSlider builder
- date-range-picker: DateRangePickerElement; Python uses _DateRangePicker builder
- search-select: SearchSelectElement; Python uses _SearchSelect builder;
  data-* attrs become plain attrs (data-name -> name, data-search-url -> search-url, etc.)
- filter-bar: FilterBarElement; props carry preset URLs; onclick/onsubmit
  attrs replaced with data-filter-bar-* sentinel attrs; all window.* globals removed

Deletes ts/range_slider.ts, ts/search_select.ts, ts/date_range_picker.ts,
ts/filter_bar.ts. Updates all tests and e2e pages to use the new element
selectors and script paths (dist/elements/<tag>.js).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 14:22:59 +02:00
parent 4652f1ff55
commit 82416e149d
33 changed files with 2301 additions and 2168 deletions
+18 -19
View File
@@ -64,10 +64,10 @@ class SearchSelectComponentTest(unittest.TestCase):
name="games", search_url="/api/games/search", multi_select=True
)
)
self.assertIn("data-search-select", html)
self.assertIn('data-name="games"', html)
self.assertIn('data-search-url="/api/games/search"', html)
self.assertIn('data-multi="true"', html)
self.assertIn("<search-select", html)
self.assertIn('name="games"', html)
self.assertIn('search-url="/api/games/search"', html)
self.assertIn('multi="true"', html)
def test_multi_selected_renders_pills_and_hidden_inputs(self):
html = str(
@@ -80,9 +80,8 @@ class SearchSelectComponentTest(unittest.TestCase):
self.assertIn("data-pill", html)
self.assertIn('<input name="games" value="7" type="hidden">', html)
self.assertIn('data-platform="2"', html)
# exactly one submitted value (the hidden input) — the search box has no
# name. The leading space avoids matching the container's data-name.
self.assertEqual(html.count(' name="games"'), 1)
# two occurrences: the <search-select name="games"> tag + the hidden input.
self.assertEqual(html.count(' name="games"'), 2)
def test_single_selected_has_no_pill_and_value_in_search_box(self):
html = str(
@@ -96,13 +95,13 @@ class SearchSelectComponentTest(unittest.TestCase):
self.assertIn('value="Game A"', html)
# the value is still submitted via a lone hidden input
self.assertIn('<input name="games" value="7" type="hidden">', html)
self.assertEqual(html.count(' name="games"'), 1)
self.assertEqual(html.count(' name="games"'), 2)
def test_search_box_has_no_name(self):
html = str(SearchSelect(name="games"))
self.assertIn("data-search-select-search", html)
# container exposes data-name, never a submittable name on the search box
self.assertEqual(html.count(' name="games"'), 0)
# <search-select name="games"> is the tag; the search box carries no name
self.assertEqual(html.count(' name="games"'), 1)
def test_tuple_options_are_normalized(self):
html = str(SearchSelect(name="t", options=[("1", "One")]))
@@ -149,11 +148,11 @@ class SearchSelectComponentTest(unittest.TestCase):
def test_prefetch_attribute_and_defaults(self):
# Default prefetch is 0 in SearchSelect
html_default = str(SearchSelect(name="t"))
self.assertIn('data-prefetch="0"', html_default)
self.assertIn('prefetch="0"', html_default)
# Custom prefetch is rendered
html_custom = str(SearchSelect(name="t", prefetch=42))
self.assertIn('data-prefetch="42"', html_custom)
self.assertIn('prefetch="42"', html_custom)
class FilterSelectComponentTest(unittest.TestCase):
@@ -164,12 +163,12 @@ class FilterSelectComponentTest(unittest.TestCase):
def test_is_filter_mode_on_shared_shell(self):
html = str(FilterSelect(field_name="type"))
# Reuses the SearchSelect shell (data-search-select) but flags filter mode.
self.assertIn("data-search-select", html)
self.assertIn('data-search-select-mode="filter"', html)
self.assertIn('data-name="type"', html)
# No name is submitted — state is read from the DOM into the filter JSON.
self.assertEqual(html.count(' name="type"'), 0)
# FilterSelect is a <search-select> with filter-mode="true".
self.assertIn("<search-select", html)
self.assertIn('filter-mode="true"', html)
self.assertIn('name="type"', html)
# <search-select name="type"> carries the name; state is read from DOM into filter JSON.
self.assertEqual(html.count(' name="type"'), 1)
def test_value_rows_have_include_exclude_buttons(self):
html = str(FilterSelect(field_name="type", options=[("g", "Game")]))
@@ -238,7 +237,7 @@ class FilterSelectComponentTest(unittest.TestCase):
self.assertIn(
'data-search-select-modifier-option="NOT_NULL"', html
) # still pinned
self.assertIn('data-prefetch="20"', html)
self.assertIn('prefetch="20"', html)
def test_search_url_pills_use_resolved_labels(self):
# A selected value outside the fetched window still shows its label.