Improve search select

This commit is contained in:
2026-06-07 09:01:18 +02:00
parent afc16aabbb
commit a6384fc003
4 changed files with 109 additions and 205 deletions
+28 -7
View File
@@ -26,9 +26,13 @@ class SearchSelectOption(TypedDict):
# removed border and border-default-medium, see later if it's needed # removed border and border-default-medium, see later if it's needed
_CONTAINER_CLASS = "relative rounded-base bg-neutral-secondary-medium" _CONTAINER_CLASS = "relative rounded-base bg-neutral-secondary-medium"
_PILLS_CLASS = "flex flex-wrap gap-1 p-2 empty:hidden" # The pills and the search box share one flex-wrap row so the widget reads as a
# single field; the pills wrapper uses `contents` so its pills/hidden inputs
# flow as direct participants of that row, inline with the search input.
_FIELD_CLASS = "flex flex-wrap items-center gap-1 p-2"
_PILLS_CLASS = "contents"
_SEARCH_CLASS = ( _SEARCH_CLASS = (
"block w-full border-0 bg-transparent text-sm text-heading p-2 " "flex-1 min-w-[8rem] border-0 bg-transparent text-sm text-heading "
"focus:ring-0 focus:outline-hidden placeholder:text-body" "focus:ring-0 focus:outline-hidden placeholder:text-body"
) )
_OPTIONS_CLASS = ( _OPTIONS_CLASS = (
@@ -99,7 +103,13 @@ def SearchSelect(
options = [_normalize_option(o) for o in (options or [])] options = [_normalize_option(o) for o in (options or [])]
# ── Pills + their hidden inputs (the submitted channel) ── # ── Pills + their hidden inputs (the submitted channel) ──
# Multi-select renders a removable Pill per value; single-select renders no
# pill — the committed label shows inside the search box instead, with a
# lone hidden input carrying the value. Both keep the hidden input(s) inside
# `[data-ss-pills]` so the JS reads/writes values uniformly.
pills_children: list[SafeText] = [] pills_children: list[SafeText] = []
search_value = ""
if multi_select:
for option in selected: for option in selected:
pills_children.append( pills_children.append(
Pill( Pill(
@@ -110,6 +120,10 @@ def SearchSelect(
) )
) )
pills_children.append(_hidden_input(name, option["value"])) pills_children.append(_hidden_input(name, option["value"]))
elif selected:
option = selected[0]
pills_children.append(_hidden_input(name, option["value"]))
search_value = option["label"]
pills = Component( pills = Component(
tag_name="div", tag_name="div",
@@ -118,15 +132,22 @@ def SearchSelect(
) )
# ── Search box (NO name — the query is never submitted) ── # ── Search box (NO name — the query is never submitted) ──
search = Component( search_attrs: list[HTMLAttribute] = [
tag_name="input",
attributes=[
("data-ss-search", ""), ("data-ss-search", ""),
("type", "text"), ("type", "text"),
("placeholder", placeholder), ("placeholder", placeholder),
("autocomplete", "off"), ("autocomplete", "off"),
("class", _SEARCH_CLASS), ("class", _SEARCH_CLASS),
], ]
if search_value:
search_attrs.append(("value", search_value))
search = Component(tag_name="input", attributes=search_attrs)
# ── Field row: pills + search box combined into one visual field ──
field = Component(
tag_name="div",
attributes=[("data-ss-field", ""), ("class", _FIELD_CLASS)],
children=[pills, search],
) )
# ── Options panel (pre-rendered only when there is no search_url) ── # ── Options panel (pre-rendered only when there is no search_url) ──
@@ -164,7 +185,7 @@ def SearchSelect(
return Component( return Component(
tag_name="div", tag_name="div",
attributes=container_attrs, attributes=container_attrs,
children=[pills, search, options_panel], children=[field, options_panel],
) )
+6 -179
View File
@@ -293,85 +293,27 @@
--leading-5: 20px; --leading-5: 20px;
--radius-base: 12px; --radius-base: 12px;
--color-body: var(--color-gray-600); --color-body: var(--color-gray-600);
--color-body-subtle: var(--color-gray-500);
--color-heading: var(--color-gray-900); --color-heading: var(--color-gray-900);
--color-fg-brand-subtle: var(--color-blue-200);
--color-fg-brand: var(--color-blue-700); --color-fg-brand: var(--color-blue-700);
--color-fg-brand-strong: var(--color-blue-900);
--color-fg-success: var(--color-emerald-700);
--color-fg-success-strong: var(--color-emerald-900);
--color-fg-danger: var(--color-rose-700);
--color-fg-danger-strong: var(--color-rose-900);
--color-fg-warning-subtle: var(--color-orange-600);
--color-fg-warning: var(--color-orange-900);
--color-fg-yellow: var(--color-yellow-400);
--color-fg-disabled: var(--color-gray-400); --color-fg-disabled: var(--color-gray-400);
--color-fg-purple: var(--color-purple-600);
--color-fg-cyan: var(--color-cyan-600);
--color-fg-indigo: var(--color-indigo-600);
--color-fg-pink: var(--color-pink-600);
--color-fg-lime: var(--color-lime-600);
--color-neutral-primary-soft: var(--color-white); --color-neutral-primary-soft: var(--color-white);
--color-neutral-primary: var(--color-white); --color-neutral-primary: var(--color-white);
--color-neutral-primary-medium: var(--color-white); --color-neutral-primary-medium: var(--color-white);
--color-neutral-primary-strong: var(--color-white);
--color-neutral-secondary-soft: var(--color-gray-50); --color-neutral-secondary-soft: var(--color-gray-50);
--color-neutral-secondary: var(--color-gray-50); --color-neutral-secondary: var(--color-gray-50);
--color-neutral-secondary-medium: var(--color-gray-50); --color-neutral-secondary-medium: var(--color-gray-50);
--color-neutral-secondary-strong: var(--color-gray-50); --color-neutral-secondary-strong: var(--color-gray-50);
--color-neutral-secondary-strongest: var(--color-gray-50);
--color-neutral-tertiary-soft: var(--color-gray-100);
--color-neutral-tertiary: var(--color-gray-100); --color-neutral-tertiary: var(--color-gray-100);
--color-neutral-tertiary-medium: var(--color-gray-100); --color-neutral-tertiary-medium: var(--color-gray-100);
--color-neutral-quaternary: var(--color-gray-200); --color-neutral-quaternary: var(--color-gray-200);
--color-neutral-quaternary-medium: var(--color-gray-200);
--color-gray: var(--color-gray-300);
--color-brand-softer: var(--color-blue-50);
--color-brand-soft: var(--color-blue-100); --color-brand-soft: var(--color-blue-100);
--color-brand: var(--color-blue-700); --color-brand: var(--color-blue-700);
--color-brand-medium: var(--color-blue-200); --color-brand-medium: var(--color-blue-200);
--color-brand-strong: var(--color-blue-800); --color-brand-strong: var(--color-blue-800);
--color-success-soft: var(--color-emerald-50);
--color-success: var(--color-emerald-700);
--color-success-medium: var(--color-emerald-100);
--color-success-strong: var(--color-emerald-800);
--color-danger-soft: var(--color-rose-50);
--color-danger: var(--color-rose-700);
--color-danger-medium: var(--color-rose-100);
--color-danger-strong: var(--color-rose-800);
--color-warning-soft: var(--color-orange-50);
--color-warning: var(--color-orange-500);
--color-warning-medium: var(--color-orange-100);
--color-warning-strong: var(--color-orange-700);
--color-dark-soft: var(--color-gray-800);
--color-dark: var(--color-gray-800); --color-dark: var(--color-gray-800);
--color-dark-strong: var(--color-gray-900);
--color-disabled: var(--color-gray-100);
--color-purple: var(--color-purple-500);
--color-sky: var(--color-sky-500);
--color-teal: var(--color-teal-600);
--color-pink: var(--color-pink-600);
--color-cyan: var(--color-cyan-500);
--color-fuchsia: var(--color-fuchsia-600);
--color-indigo: var(--color-indigo-600);
--color-orange: var(--color-orange-400);
--color-buffer: var(--color-white);
--color-buffer-medium: var(--color-white);
--color-buffer-strong: var(--color-white);
--color-muted: var(--color-gray-50);
--color-light-subtle: var(--color-gray-100);
--color-light: var(--color-gray-100); --color-light: var(--color-gray-100);
--color-light-medium: var(--color-gray-100);
--color-default-subtle: var(--color-gray-200);
--color-default: var(--color-gray-200); --color-default: var(--color-gray-200);
--color-default-medium: var(--color-gray-200); --color-default-medium: var(--color-gray-200);
--color-default-strong: var(--color-gray-200);
--color-success-subtle: var(--color-emerald-200);
--color-danger-subtle: var(--color-rose-200);
--color-warning-subtle: var(--color-orange-200);
--color-brand-subtle: var(--color-blue-200);
--color-brand-light: var(--color-blue-600);
--color-dark-subtle: var(--color-gray-800);
--color-dark-backdrop: var(--color-gray-950); --color-dark-backdrop: var(--color-gray-950);
--color-accent: #7c3aed; --color-accent: #7c3aed;
} }
@@ -878,18 +820,12 @@
.start-0 { .start-0 {
inset-inline-start: calc(var(--spacing) * 0); inset-inline-start: calc(var(--spacing) * 0);
} }
.end-1 {
inset-inline-end: calc(var(--spacing) * 1);
}
.end-1\.5 { .end-1\.5 {
inset-inline-end: calc(var(--spacing) * 1.5); inset-inline-end: calc(var(--spacing) * 1.5);
} }
.top-0 { .top-0 {
top: calc(var(--spacing) * 0); top: calc(var(--spacing) * 0);
} }
.top-1 {
top: calc(var(--spacing) * 1);
}
.top-1\/2 { .top-1\/2 {
top: calc(1 / 2 * 100%); top: calc(1 / 2 * 100%);
} }
@@ -911,9 +847,6 @@
.bottom-0 { .bottom-0 {
bottom: calc(var(--spacing) * 0); bottom: calc(var(--spacing) * 0);
} }
.bottom-1 {
bottom: calc(var(--spacing) * 1);
}
.bottom-1\.5 { .bottom-1\.5 {
bottom: calc(var(--spacing) * 1.5); bottom: calc(var(--spacing) * 1.5);
} }
@@ -1482,6 +1415,9 @@
.block { .block {
display: block; display: block;
} }
.contents {
display: contents;
}
.flex { .flex {
display: flex; display: flex;
} }
@@ -1540,15 +1476,9 @@
.h-full { .h-full {
height: 100%; height: 100%;
} }
.max-h-40 {
max-height: calc(var(--spacing) * 40);
}
.max-h-full { .max-h-full {
max-height: 100%; max-height: 100%;
} }
.min-h-\[28px\] {
min-height: 28px;
}
.min-h-screen { .min-h-screen {
min-height: 100vh; min-height: 100vh;
} }
@@ -1617,15 +1547,9 @@
text-align: center; text-align: center;
} }
} }
.w-1 {
width: calc(var(--spacing) * 1);
}
.w-1\/2 { .w-1\/2 {
width: calc(1 / 2 * 100%); width: calc(1 / 2 * 100%);
} }
.w-2 {
width: calc(var(--spacing) * 2);
}
.w-2\.5 { .w-2\.5 {
width: calc(var(--spacing) * 2.5); width: calc(var(--spacing) * 2.5);
} }
@@ -1722,6 +1646,9 @@
border-color: var(--color-brand); border-color: var(--color-brand);
} }
} }
.min-w-\[8rem\] {
min-width: 8rem;
}
.flex-1 { .flex-1 {
flex: 1; flex: 1;
} }
@@ -1734,9 +1661,6 @@
.shrink-0 { .shrink-0 {
flex-shrink: 0; flex-shrink: 0;
} }
.border-collapse {
border-collapse: collapse;
}
.-translate-x-full { .-translate-x-full {
--tw-translate-x: -100%; --tw-translate-x: -100%;
translate: var(--tw-translate-x) var(--tw-translate-y); translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -1753,10 +1677,6 @@
--tw-translate-x: 100%; --tw-translate-x: 100%;
translate: var(--tw-translate-x) var(--tw-translate-y); translate: var(--tw-translate-x) var(--tw-translate-y);
} }
.-translate-y-1 {
--tw-translate-y: calc(var(--spacing) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-y-1\/2 { .-translate-y-1\/2 {
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1); --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y); translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -1799,9 +1719,6 @@
.list-disc { .list-disc {
list-style-type: disc; list-style-type: disc;
} }
.appearance-none {
appearance: none;
}
.grid-cols-1 { .grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr)); grid-template-columns: repeat(1, minmax(0, 1fr));
} }
@@ -2145,9 +2062,6 @@
.bg-amber-50 { .bg-amber-50 {
background-color: var(--color-amber-50); background-color: var(--color-amber-50);
} }
.bg-black {
background-color: var(--color-black);
}
.bg-black\/70 { .bg-black\/70 {
background-color: color-mix(in srgb, #000 70%, transparent); background-color: color-mix(in srgb, #000 70%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@@ -2172,9 +2086,6 @@
background-color: color-mix(in oklab, var(--color-brand) 15%, transparent); background-color: color-mix(in oklab, var(--color-brand) 15%, transparent);
} }
} }
.bg-dark-backdrop {
background-color: var(--color-dark-backdrop);
}
.bg-dark-backdrop\/70 { .bg-dark-backdrop\/70 {
background-color: color-mix(in srgb, oklch(13% 0.028 261.692) 70%, transparent); background-color: color-mix(in srgb, oklch(13% 0.028 261.692) 70%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@@ -2193,18 +2104,12 @@
.bg-gray-500 { .bg-gray-500 {
background-color: var(--color-gray-500); background-color: var(--color-gray-500);
} }
.bg-gray-800 {
background-color: var(--color-gray-800);
}
.bg-gray-800\/20 { .bg-gray-800\/20 {
background-color: color-mix(in srgb, oklch(27.8% 0.033 256.848) 20%, transparent); background-color: color-mix(in srgb, oklch(27.8% 0.033 256.848) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-gray-800) 20%, transparent); background-color: color-mix(in oklab, var(--color-gray-800) 20%, transparent);
} }
} }
.bg-gray-900 {
background-color: var(--color-gray-900);
}
.bg-gray-900\/50 { .bg-gray-900\/50 {
background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 50%, transparent); background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 50%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@@ -2319,18 +2224,6 @@
fill: white !important; fill: white !important;
} }
} }
.apexcharts-gridline {
stroke: var(--color-default) !important;
.dark & {
stroke: var(--color-default) !important;
}
}
.apexcharts-xcrosshairs {
stroke: var(--color-default) !important;
.dark & {
stroke: var(--color-default) !important;
}
}
.apexcharts-ycrosshairs { .apexcharts-ycrosshairs {
stroke: var(--color-default) !important; stroke: var(--color-default) !important;
.dark & { .dark & {
@@ -2389,9 +2282,6 @@
.px-6 { .px-6 {
padding-inline: calc(var(--spacing) * 6); padding-inline: calc(var(--spacing) * 6);
} }
.py-0 {
padding-block: calc(var(--spacing) * 0);
}
.py-0\.5 { .py-0\.5 {
padding-block: calc(var(--spacing) * 0.5); padding-block: calc(var(--spacing) * 0.5);
} }
@@ -2453,9 +2343,6 @@
color: heading !important; color: heading !important;
} }
} }
.pb-1 {
padding-bottom: calc(var(--spacing) * 1);
}
.pb-16 { .pb-16 {
padding-bottom: calc(var(--spacing) * 16); padding-bottom: calc(var(--spacing) * 16);
} }
@@ -2622,9 +2509,6 @@
.text-balance { .text-balance {
text-wrap: balance; text-wrap: balance;
} }
.text-wrap {
text-wrap: wrap;
}
.whitespace-nowrap { .whitespace-nowrap {
white-space: nowrap; white-space: nowrap;
} }
@@ -2751,9 +2635,6 @@
.italic { .italic {
font-style: italic; font-style: italic;
} }
.no-underline {
text-decoration-line: none;
}
.no-underline\! { .no-underline\! {
text-decoration-line: none !important; text-decoration-line: none !important;
} }
@@ -2817,10 +2698,6 @@
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
} }
.backdrop-filter {
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
}
.transition { .transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
@@ -2985,11 +2862,6 @@
background-color: var(--color-gray-50); background-color: var(--color-gray-50);
} }
} }
.empty\:hidden {
&:empty {
display: none;
}
}
.hover\:scale-110 { .hover\:scale-110 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@@ -4042,51 +3914,6 @@
} }
} }
} }
.\[\&\:\:-webkit-slider-thumb\]\:relative {
&::-webkit-slider-thumb {
position: relative;
}
}
.\[\&\:\:-webkit-slider-thumb\]\:z-10 {
&::-webkit-slider-thumb {
z-index: 10;
}
}
.\[\&\:\:-webkit-slider-thumb\]\:z-20 {
&::-webkit-slider-thumb {
z-index: 20;
}
}
.\[\&\:\:-webkit-slider-thumb\]\:h-4 {
&::-webkit-slider-thumb {
height: calc(var(--spacing) * 4);
}
}
.\[\&\:\:-webkit-slider-thumb\]\:w-4 {
&::-webkit-slider-thumb {
width: calc(var(--spacing) * 4);
}
}
.\[\&\:\:-webkit-slider-thumb\]\:cursor-pointer {
&::-webkit-slider-thumb {
cursor: pointer;
}
}
.\[\&\:\:-webkit-slider-thumb\]\:appearance-none {
&::-webkit-slider-thumb {
appearance: none;
}
}
.\[\&\:\:-webkit-slider-thumb\]\:rounded-full {
&::-webkit-slider-thumb {
border-radius: calc(infinity * 1px);
}
}
.\[\&\:\:-webkit-slider-thumb\]\:bg-brand {
&::-webkit-slider-thumb {
background-color: var(--color-brand);
}
}
.\[\&\:first-of-type_button\]\:rounded-s-lg { .\[\&\:first-of-type_button\]\:rounded-s-lg {
&:first-of-type button { &:first-of-type button {
border-start-start-radius: var(--radius-lg); border-start-start-radius: var(--radius-lg);
+48 -5
View File
@@ -1,7 +1,10 @@
/** /**
* SearchSelect widget — a search box paired with a dropdown of options. * SearchSelect widget — a search box paired with a dropdown of options.
* Single/multi select; chosen items render as removable pills, each backed by a * Multi-select renders chosen items as removable pills (inline with the search
* hidden <input> so existing Django form validation keeps working. * box), each backed by a hidden <input>. Single-select renders no pill: the
* committed label lives inside the search box (which doubles as a combobox —
* focus clears it to search, picking an option fills it), with a lone hidden
* <input> carrying the value. Both keep hidden inputs so Django validation works.
* *
* Mirrors selectable_filter.js: initAll() on DOMContentLoaded + htmx:afterSwap, * Mirrors selectable_filter.js: initAll() on DOMContentLoaded + htmx:afterSwap,
* each widget guarded with el._ssInit. * each widget guarded with el._ssInit.
@@ -120,8 +123,44 @@
} }
} }
search.addEventListener("focus", runSearch); // ── Single-select combobox: the search box shows the committed label;
search.addEventListener("input", runSearch); // focusing clears it to search, blurring restores it (or deselects). ──
if (!multi) container._ssLabel = search.value;
search.addEventListener("focus", function () {
if (!multi) {
// Hide the committed label so the box becomes a fresh search field.
search.value = "";
container._ssDirty = false;
}
runSearch();
});
search.addEventListener("input", function () {
if (!multi) container._ssDirty = true;
runSearch();
});
if (!multi) {
search.addEventListener("blur", function () {
// Defer so an option click (which fires before blur settles) wins.
setTimeout(function () {
if (container._ssDirty && search.value.trim() === "") {
// User intentionally cleared the box → deselect.
pills.innerHTML = "";
container._ssLabel = "";
emitChange(null);
} else {
// Focused-and-left, or typed a partial query without picking →
// restore the committed label (no-op right after a selection).
search.value = container._ssLabel || "";
}
}, 120);
});
}
// Clicking an option must not blur the input before the click selects.
options.addEventListener("mousedown", function (e) {
e.preventDefault();
});
// ── Option click → select ── // ── Option click → select ──
options.addEventListener("click", function (e) { options.addEventListener("click", function (e) {
@@ -152,9 +191,13 @@
addPill(option); addPill(option);
} }
} else { } else {
// Single-select: no pill — show the label in the search box and keep a
// lone hidden input under [data-ss-pills] for submission.
pills.innerHTML = ""; pills.innerHTML = "";
addPill(option); pills.appendChild(buildHidden(option.value));
search.value = option.label; search.value = option.label;
container._ssLabel = option.label;
container._ssDirty = false;
hidePanel(); hidePanel();
} }
emitChange(option); emitChange(option);
+14 -1
View File
@@ -63,9 +63,10 @@ class SearchSelectComponentTest(unittest.TestCase):
self.assertIn('data-search-url="/api/games/search"', html) self.assertIn('data-search-url="/api/games/search"', html)
self.assertIn('data-multi="true"', html) self.assertIn('data-multi="true"', html)
def test_selected_renders_pills_and_hidden_inputs(self): def test_multi_selected_renders_pills_and_hidden_inputs(self):
html = SearchSelect( html = SearchSelect(
name="games", name="games",
multi_select=True,
selected=[{"value": 7, "label": "Game A", "data": {"platform": "2"}}], selected=[{"value": 7, "label": "Game A", "data": {"platform": "2"}}],
) )
self.assertIn("data-pill", html) self.assertIn("data-pill", html)
@@ -75,6 +76,18 @@ class SearchSelectComponentTest(unittest.TestCase):
# name. The leading space avoids matching the container's data-name. # name. The leading space avoids matching the container's data-name.
self.assertEqual(html.count(' name="games"'), 1) self.assertEqual(html.count(' name="games"'), 1)
def test_single_selected_has_no_pill_and_value_in_search_box(self):
html = SearchSelect(
name="games",
selected=[{"value": 7, "label": "Game A", "data": {"platform": "2"}}],
)
# single-select renders no pill — the label lives in the search box
self.assertNotIn("data-pill", html)
self.assertIn('value="Game A"', html)
# the value is still submitted via a lone hidden input
self.assertIn('<input type="hidden" name="games" value="7">', html)
self.assertEqual(html.count(' name="games"'), 1)
def test_search_box_has_no_name(self): def test_search_box_has_no_name(self):
html = SearchSelect(name="games") html = SearchSelect(name="games")
self.assertIn("data-ss-search", html) self.assertIn("data-ss-search", html)