Improve search select
This commit is contained in:
@@ -26,9 +26,13 @@ class SearchSelectOption(TypedDict):
|
||||
|
||||
# removed border and border-default-medium, see later if it's needed
|
||||
_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 = (
|
||||
"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"
|
||||
)
|
||||
_OPTIONS_CLASS = (
|
||||
@@ -99,17 +103,27 @@ def SearchSelect(
|
||||
options = [_normalize_option(o) for o in (options or [])]
|
||||
|
||||
# ── 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] = []
|
||||
for option in selected:
|
||||
pills_children.append(
|
||||
Pill(
|
||||
option["label"],
|
||||
value=str(option["value"]),
|
||||
removable=True,
|
||||
attributes=_data_attributes(option["data"]),
|
||||
search_value = ""
|
||||
if multi_select:
|
||||
for option in selected:
|
||||
pills_children.append(
|
||||
Pill(
|
||||
option["label"],
|
||||
value=str(option["value"]),
|
||||
removable=True,
|
||||
attributes=_data_attributes(option["data"]),
|
||||
)
|
||||
)
|
||||
)
|
||||
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(
|
||||
tag_name="div",
|
||||
@@ -118,15 +132,22 @@ def SearchSelect(
|
||||
)
|
||||
|
||||
# ── Search box (NO name — the query is never submitted) ──
|
||||
search = Component(
|
||||
tag_name="input",
|
||||
attributes=[
|
||||
("data-ss-search", ""),
|
||||
("type", "text"),
|
||||
("placeholder", placeholder),
|
||||
("autocomplete", "off"),
|
||||
("class", _SEARCH_CLASS),
|
||||
],
|
||||
search_attrs: list[HTMLAttribute] = [
|
||||
("data-ss-search", ""),
|
||||
("type", "text"),
|
||||
("placeholder", placeholder),
|
||||
("autocomplete", "off"),
|
||||
("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) ──
|
||||
@@ -164,7 +185,7 @@ def SearchSelect(
|
||||
return Component(
|
||||
tag_name="div",
|
||||
attributes=container_attrs,
|
||||
children=[pills, search, options_panel],
|
||||
children=[field, options_panel],
|
||||
)
|
||||
|
||||
|
||||
|
||||
+6
-179
@@ -293,85 +293,27 @@
|
||||
--leading-5: 20px;
|
||||
--radius-base: 12px;
|
||||
--color-body: var(--color-gray-600);
|
||||
--color-body-subtle: var(--color-gray-500);
|
||||
--color-heading: var(--color-gray-900);
|
||||
--color-fg-brand-subtle: var(--color-blue-200);
|
||||
--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-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: 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: var(--color-gray-50);
|
||||
--color-neutral-secondary-medium: 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-medium: var(--color-gray-100);
|
||||
--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: var(--color-blue-700);
|
||||
--color-brand-medium: var(--color-blue-200);
|
||||
--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-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-medium: var(--color-gray-100);
|
||||
--color-default-subtle: var(--color-gray-200);
|
||||
--color-default: 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-accent: #7c3aed;
|
||||
}
|
||||
@@ -878,18 +820,12 @@
|
||||
.start-0 {
|
||||
inset-inline-start: calc(var(--spacing) * 0);
|
||||
}
|
||||
.end-1 {
|
||||
inset-inline-end: calc(var(--spacing) * 1);
|
||||
}
|
||||
.end-1\.5 {
|
||||
inset-inline-end: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.top-0 {
|
||||
top: calc(var(--spacing) * 0);
|
||||
}
|
||||
.top-1 {
|
||||
top: calc(var(--spacing) * 1);
|
||||
}
|
||||
.top-1\/2 {
|
||||
top: calc(1 / 2 * 100%);
|
||||
}
|
||||
@@ -911,9 +847,6 @@
|
||||
.bottom-0 {
|
||||
bottom: calc(var(--spacing) * 0);
|
||||
}
|
||||
.bottom-1 {
|
||||
bottom: calc(var(--spacing) * 1);
|
||||
}
|
||||
.bottom-1\.5 {
|
||||
bottom: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
@@ -1482,6 +1415,9 @@
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
.contents {
|
||||
display: contents;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
@@ -1540,15 +1476,9 @@
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
.max-h-40 {
|
||||
max-height: calc(var(--spacing) * 40);
|
||||
}
|
||||
.max-h-full {
|
||||
max-height: 100%;
|
||||
}
|
||||
.min-h-\[28px\] {
|
||||
min-height: 28px;
|
||||
}
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
@@ -1617,15 +1547,9 @@
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.w-1 {
|
||||
width: calc(var(--spacing) * 1);
|
||||
}
|
||||
.w-1\/2 {
|
||||
width: calc(1 / 2 * 100%);
|
||||
}
|
||||
.w-2 {
|
||||
width: calc(var(--spacing) * 2);
|
||||
}
|
||||
.w-2\.5 {
|
||||
width: calc(var(--spacing) * 2.5);
|
||||
}
|
||||
@@ -1722,6 +1646,9 @@
|
||||
border-color: var(--color-brand);
|
||||
}
|
||||
}
|
||||
.min-w-\[8rem\] {
|
||||
min-width: 8rem;
|
||||
}
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1734,9 +1661,6 @@
|
||||
.shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.-translate-x-full {
|
||||
--tw-translate-x: -100%;
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
@@ -1753,10 +1677,6 @@
|
||||
--tw-translate-x: 100%;
|
||||
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 {
|
||||
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
@@ -1799,9 +1719,6 @@
|
||||
.list-disc {
|
||||
list-style-type: disc;
|
||||
}
|
||||
.appearance-none {
|
||||
appearance: none;
|
||||
}
|
||||
.grid-cols-1 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
@@ -2145,9 +2062,6 @@
|
||||
.bg-amber-50 {
|
||||
background-color: var(--color-amber-50);
|
||||
}
|
||||
.bg-black {
|
||||
background-color: var(--color-black);
|
||||
}
|
||||
.bg-black\/70 {
|
||||
background-color: color-mix(in srgb, #000 70%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
@@ -2172,9 +2086,6 @@
|
||||
background-color: color-mix(in oklab, var(--color-brand) 15%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-dark-backdrop {
|
||||
background-color: var(--color-dark-backdrop);
|
||||
}
|
||||
.bg-dark-backdrop\/70 {
|
||||
background-color: color-mix(in srgb, oklch(13% 0.028 261.692) 70%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
@@ -2193,18 +2104,12 @@
|
||||
.bg-gray-500 {
|
||||
background-color: var(--color-gray-500);
|
||||
}
|
||||
.bg-gray-800 {
|
||||
background-color: var(--color-gray-800);
|
||||
}
|
||||
.bg-gray-800\/20 {
|
||||
background-color: color-mix(in srgb, oklch(27.8% 0.033 256.848) 20%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
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 {
|
||||
background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 50%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
@@ -2319,18 +2224,6 @@
|
||||
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 {
|
||||
stroke: var(--color-default) !important;
|
||||
.dark & {
|
||||
@@ -2389,9 +2282,6 @@
|
||||
.px-6 {
|
||||
padding-inline: calc(var(--spacing) * 6);
|
||||
}
|
||||
.py-0 {
|
||||
padding-block: calc(var(--spacing) * 0);
|
||||
}
|
||||
.py-0\.5 {
|
||||
padding-block: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
@@ -2453,9 +2343,6 @@
|
||||
color: heading !important;
|
||||
}
|
||||
}
|
||||
.pb-1 {
|
||||
padding-bottom: calc(var(--spacing) * 1);
|
||||
}
|
||||
.pb-16 {
|
||||
padding-bottom: calc(var(--spacing) * 16);
|
||||
}
|
||||
@@ -2622,9 +2509,6 @@
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
.text-wrap {
|
||||
text-wrap: wrap;
|
||||
}
|
||||
.whitespace-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -2751,9 +2635,6 @@
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.no-underline {
|
||||
text-decoration-line: none;
|
||||
}
|
||||
.no-underline\! {
|
||||
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,);
|
||||
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-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));
|
||||
@@ -2985,11 +2862,6 @@
|
||||
background-color: var(--color-gray-50);
|
||||
}
|
||||
}
|
||||
.empty\:hidden {
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.hover\:scale-110 {
|
||||
&: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 {
|
||||
border-start-start-radius: var(--radius-lg);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/**
|
||||
* SearchSelect widget — a search box paired with a dropdown of options.
|
||||
* Single/multi select; chosen items render as removable pills, each backed by a
|
||||
* hidden <input> so existing Django form validation keeps working.
|
||||
* Multi-select renders chosen items as removable pills (inline with the search
|
||||
* 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,
|
||||
* each widget guarded with el._ssInit.
|
||||
@@ -120,8 +123,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
search.addEventListener("focus", runSearch);
|
||||
search.addEventListener("input", runSearch);
|
||||
// ── Single-select combobox: the search box shows the committed label;
|
||||
// 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 ──
|
||||
options.addEventListener("click", function (e) {
|
||||
@@ -152,9 +191,13 @@
|
||||
addPill(option);
|
||||
}
|
||||
} 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 = "";
|
||||
addPill(option);
|
||||
pills.appendChild(buildHidden(option.value));
|
||||
search.value = option.label;
|
||||
container._ssLabel = option.label;
|
||||
container._ssDirty = false;
|
||||
hidePanel();
|
||||
}
|
||||
emitChange(option);
|
||||
|
||||
@@ -63,9 +63,10 @@ class SearchSelectComponentTest(unittest.TestCase):
|
||||
self.assertIn('data-search-url="/api/games/search"', 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(
|
||||
name="games",
|
||||
multi_select=True,
|
||||
selected=[{"value": 7, "label": "Game A", "data": {"platform": "2"}}],
|
||||
)
|
||||
self.assertIn("data-pill", html)
|
||||
@@ -75,6 +76,18 @@ class SearchSelectComponentTest(unittest.TestCase):
|
||||
# name. The leading space avoids matching the container's data-name.
|
||||
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):
|
||||
html = SearchSelect(name="games")
|
||||
self.assertIn("data-ss-search", html)
|
||||
|
||||
Reference in New Issue
Block a user