Give SearchSelect the same border + focus as native inputs
The SearchSelect wrapper had no border and no focus styling, so it looked unlike a native input (which has border-default-medium → border-brand on focus). Add border-default-medium + focus-within:border-brand/ring to the shared container class — focus-within because the focusable element is the inner search box. This also makes filter-bar comboboxes consistent with the other filter inputs, which already have borders. e2e asserts the wrapper border matches a native input's at rest and turns brand on focus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -63,15 +63,21 @@ LabeledOption = tuple[str, str]
|
||||
# widget reads as a single clickable field; the pills wrapper uses `contents`
|
||||
# so its pills/hidden inputs flow as direct participants of that row, inline
|
||||
# with the search input. The options panel is absolute, so it sits outside the
|
||||
# flex flow. (border omitted intentionally — see if it's needed later.)
|
||||
# flex flow.
|
||||
# Border + focus styling mirror a native input (INPUT_CLASS): border-default-medium
|
||||
# normally, brand border + ring on focus. The search input is the focusable
|
||||
# element, so the focus state is expressed on the wrapper with focus-within: (and
|
||||
# the inner input suppresses its own ring — see _SEARCH_CLASS).
|
||||
# The widget owns its disabled appearance: when any control inside it is
|
||||
# :disabled (e.g. add_purchase.ts disabling the search input), the wrapper fades
|
||||
# via :has() — the same opacity-50 a disabled native input uses (see
|
||||
# _DISABLED_CONTROL in games/forms.py), so the two look identical. Callers only
|
||||
# toggle the control's `disabled`, never styles.
|
||||
_CONTAINER_CLASS = (
|
||||
"relative flex flex-wrap items-center gap-1 p-2 "
|
||||
f"rounded-base bg-neutral-secondary-medium {DISABLED_WITHIN_CLASS}"
|
||||
"relative flex flex-wrap items-center gap-1 p-2 rounded-base "
|
||||
"bg-neutral-secondary-medium border border-default-medium "
|
||||
"focus-within:border-brand focus-within:ring-1 focus-within:ring-brand "
|
||||
f"{DISABLED_WITHIN_CLASS}"
|
||||
)
|
||||
_PILLS_CLASS = "contents"
|
||||
# disabled:cursor-not-allowed matches the wrapper's cursor so hovering across
|
||||
|
||||
@@ -154,6 +154,30 @@ def test_add_purchase_related_game_is_flat_game_search(
|
||||
expect(related).to_have_attribute("data-search-url", "/api/games/search")
|
||||
|
||||
|
||||
def test_searchselect_border_matches_native_input(
|
||||
authenticated_page: Page, live_server
|
||||
):
|
||||
"""A SearchSelect's wrapper has the same border as a native input, and turns
|
||||
brand on focus (via focus-within on the wrapper, since the inner search box
|
||||
is what's focused)."""
|
||||
page = authenticated_page
|
||||
page.goto(f"{live_server.url}{reverse('games:add_purchase')}")
|
||||
price = page.locator("#id_price") # always-enabled native input
|
||||
wrapper = page.locator("#id_platform")
|
||||
search = page.locator("#id_platform [data-search-select-search]")
|
||||
border = "el => getComputedStyle(el).borderColor"
|
||||
|
||||
rest = price.evaluate(border)
|
||||
assert wrapper.evaluate(border) == rest # same border at rest
|
||||
|
||||
search.focus()
|
||||
focused_wrapper = wrapper.evaluate(border)
|
||||
price.focus()
|
||||
focused_input = price.evaluate(border)
|
||||
assert focused_wrapper == focused_input # same brand border on focus
|
||||
assert focused_wrapper != rest # focus actually changes it
|
||||
|
||||
|
||||
def test_add_game_syncs_sort_name_from_name(authenticated_page: Page, live_server):
|
||||
"""Typing into Name live-fills Sort name (sync bound to the add form, not
|
||||
the navbar logout form which is the first <form> on the page)."""
|
||||
|
||||
@@ -2825,6 +2825,10 @@
|
||||
--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.ring {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.ring-2 {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
|
||||
Reference in New Issue
Block a user