Fixxed bugs From form and Fields

This commit is contained in:
2025-08-16 12:37:38 +05:30
parent acc2d2c767
commit a7f4b59c36
5 changed files with 285 additions and 169 deletions
+2 -10
View File
@@ -89,7 +89,7 @@ const helpCls = "mt-2 text-sm text-gray-500 dark:text-neutral-500";
const errCls = "text-sm text-red-600 mt-2";
const groupCls = "relative flex items-stretch";
const addonBase =
"px-4 inline-flex items-center min-w-fit rounded-s-md border border-e-0 border-gray-200 bg-gray-50 text-sm text-gray-500 dark:bg-neutral-700 dark:border-neutral-700 dark:text-neutral-400";
"px-4 inline-flex items-center min-w-fit rounded-md border border-gray-200 bg-gray-50 text-sm text-gray-500 dark:bg-neutral-700 dark:border-neutral-700 dark:text-neutral-400";
const controlWrap = "relative";
---
@@ -144,8 +144,6 @@ const controlWrap = "relative";
}),
p.prefixText && "rounded-l-none",
p.suffixText && "rounded-r-none",
p.iconLeft && "rounded-l-none",
p.iconRight && "rounded-r-none",
p.error &&
"border-danger focus-visible:ring-danger/40",
)}
@@ -217,8 +215,6 @@ const controlWrap = "relative";
"block w-full border border-gray-200 shadow-sm rounded-lg text-sm focus:z-10 focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 file:bg-gray-50 file:border-0 file:me-4 file:py-3 file:px-4 dark:file:bg-neutral-700 dark:file:text-neutral-400",
p.prefixText && "rounded-l-none",
p.suffixText && "rounded-r-none",
p.iconLeft && "rounded-l-none",
p.iconRight && "rounded-r-none",
p.error &&
"border-danger focus-visible:ring-danger/40",
)}
@@ -281,8 +277,6 @@ const controlWrap = "relative";
: p.size === "lg"
? "text-base p-4"
: "text-sm p-3",
p.iconLeft && "rounded-l-none",
p.iconRight && "rounded-r-none",
p.error && "border-danger focus-visible:ring-danger/40",
)}
data-initial={typeof p.value === "string" ? p.value : ""}
@@ -309,9 +303,7 @@ const controlWrap = "relative";
}
class={cn(
selectBase({ size: p.size ?? "md" }),
"appearance-none bg-[length:16px_16px] pr-9",
p.iconLeft && "rounded-l-none",
p.iconRight && "rounded-r-none",
"appearance-none bg-none pr-9",
p.error && "border-danger focus-visible:ring-danger/40",
)}
>
+98 -65
View File
@@ -18,12 +18,13 @@ interface Props extends HTMLAttributes<"div"> {
required?: boolean;
disabled?: boolean;
options: Option[];
value?: string[]; // initial selected values
placeholder?: string; // shown when empty
searchable?: boolean; // show search box in dropdown
clearable?: boolean; // show clear (x) button
maxSelections?: number; // optional selection cap
value?: string[];
placeholder?: string;
searchable?: boolean;
clearable?: boolean;
maxSelections?: number;
size?: "sm" | "md" | "lg";
display?: "text" | "chips";
class?: string;
}
@@ -32,8 +33,9 @@ const p = Astro.props as Props;
const fieldId = p.id ?? p.name;
const descId = p.description ? `${fieldId}-desc` : undefined;
const errId = `${fieldId}-err`;
const selected = new Set(p.value ?? []);
const summary = selected.size
const summarySSR = selected.size
? Array.from(selected)
.map((v) => p.options.find((o) => o.value === v)?.label ?? v)
.join(", ")
@@ -47,11 +49,15 @@ const sizeCls =
: "h-10 px-3 text-sm";
const controlCls =
"py-2.5 sm:py-3 px-4 block w-full border-gray-200 rounded-lg sm:text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600";
"py-2.5 sm:py-3 px-4 block w-full border-gray-200 rounded-lg sm:text-sm " +
"focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none " +
"dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600";
const chipBase =
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs bg-muted text-foreground";
const containerId = `ms-${Math.random().toString(36).slice(2)}`;
const displayMode = p.display ?? "text";
---
<div class={cn("space-y-1.5", p.class)} data-field={p.name} id={containerId}>
@@ -67,9 +73,6 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
}
<div class="relative">
{
/** Visible control is a readonly input so validation styles apply here */
}
<input
id={fieldId}
type="text"
@@ -79,21 +82,28 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
aria-describedby={[descId, p.error ? errId : undefined]
.filter(Boolean)
.join(" ") || undefined}
class={cn(controlCls, sizeCls, "pr-10 cursor-pointer")}
class={cn(
controlCls,
sizeCls,
"pr-10 cursor-pointer",
displayMode === "chips" && "pl-3",
)}
placeholder={p.placeholder ?? "Select..."}
value={summary}
value={displayMode === "text" ? summarySSR : ""}
readonly
data-control
{...p.disabled ? { disabled: true } : {}}
/>
{/** Chevron / Clear */}
<div class="absolute inset-y-0 right-0 flex items-center gap-1 pr-2">
{
p.clearable !== false && selected.size > 0 && !p.disabled && (
p.clearable !== false && !p.disabled && (
<button
type="button"
class="p-1 rounded hover:bg-muted"
class={cn(
"p-1 rounded hover:bg-muted",
selected.size === 0 && "hidden",
)}
data-ms-clear
aria-label="Clear selection"
>
@@ -111,28 +121,15 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
</button>
</div>
{
/** Selected chips (optional; comment out if you prefer only summary text) */
}
{
selected.size > 0 && (
<div class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 flex flex-wrap gap-1 pr-14">
{Array.from(selected)
.slice(0, 3)
.map((v) => {
const lab =
p.options.find((o) => o.value === v)?.label ??
v;
return <span class={chipBase}>{lab}</span>;
})}
{selected.size > 3 && (
<span class={chipBase}>+{selected.size - 3}</span>
)}
</div>
)
}
<div
data-ms-chips
class={cn(
"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 flex flex-wrap gap-1 pr-14",
displayMode === "chips" ? "" : "hidden",
)}
>
</div>
{/** Dropdown */}
<div
class="absolute border-border bg-popover text-popover-foreground shadow-lg hidden mt-2 z-50 w-full max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700"
id={`${containerId}-panel`}
@@ -198,19 +195,25 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
<button
type="button"
class="text-sm text-primary hover:underline disabled:opacity-50"
data-ms-select-all>Select all</button
data-ms-select-all
>
Select all
</button>
<div class="flex items-center gap-2">
<button
type="button"
class="text-sm text-muted-foreground hover:underline"
data-ms-cancel>Cancel</button
data-ms-cancel
>
Cancel
</button>
<button
type="button"
class="text-sm text-primary hover:underline"
data-ms-apply>Apply</button
data-ms-apply
>
Apply
</button>
</div>
</div>
</div>
@@ -236,6 +239,7 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
fieldId,
name: p.name,
maxSelections: p.maxSelections ?? null,
displayMode,
}}
>
const root = document.getElementById(containerId);
@@ -248,39 +252,72 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
const btnCan = root.querySelector("[data-ms-cancel]");
const btnApp = root.querySelector("[data-ms-apply]");
const sch = root.querySelector("[data-ms-search]");
const chips = root.querySelector("[data-ms-chips]");
const checks = Array.from(
root.querySelectorAll(
`input[type="checkbox"][name="${CSS.escape(name)}"]`,
),
);
let open = false;
const origPlaceholder = input.getAttribute("placeholder") || "Select...";
function setExpanded(
v,
{ focusSearch = false, refocusInput = false } = {},
) {
function setExpanded(v, { focusSearch = false } = {}) {
open = v;
input.setAttribute("aria-expanded", String(open));
panel.classList.toggle("hidden", !open);
if (open && focusSearch) sch?.focus();
if (!open && refocusInput) input.focus();
}
function makeChip(label) {
const span = document.createElement("span");
span.className =
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs bg-muted text-foreground";
span.textContent = label;
return span;
}
function toggleClear(show) {
if (!btnClr) return;
btnClr.classList.toggle("hidden", !show);
}
function updateSummary() {
const selected = checks.filter((c) => c.checked);
const labels = selected
.slice(0, 3)
.map(
(c) =>
c
.closest("[data-ms-item]")
?.querySelector("span")
?.textContent?.trim() || c.value,
);
const extra = Math.max(0, selected.length - 3);
input.value = selected.length
? labels.join(", ") + (extra ? ` +${extra}` : "")
: "";
const labels = selected.map(
(c) =>
c
.closest("[data-ms-item]")
?.querySelector("span")
?.textContent?.trim() || c.value,
);
if (displayMode === "text") {
const preview = labels.slice(0, 3).join(", ");
const extra = Math.max(0, labels.length - 3);
input.value = labels.length
? preview + (extra ? ` +${extra}` : "")
: "";
input.placeholder = labels.length ? "" : origPlaceholder;
if (chips) {
chips.innerHTML = "";
chips.classList.add("hidden");
}
} else {
input.value = "";
input.placeholder = labels.length ? "" : origPlaceholder;
if (!chips) return;
chips.innerHTML = "";
const max = 3;
labels
.slice(0, max)
.forEach((lab) => chips.appendChild(makeChip(lab)));
const extra = labels.length - max;
if (extra > 0) chips.appendChild(makeChip(`+${extra}`));
chips.classList.toggle("hidden", labels.length === 0);
}
toggleClear(labels.length > 0);
}
function enforceMax() {
@@ -308,7 +345,6 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
updateSummary();
}
// Open/close
btnTgl?.addEventListener("click", () =>
setExpanded(!open, { focusSearch: !open }),
);
@@ -322,21 +358,20 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
}
});
// Actions
btnClr?.addEventListener("click", (e) => {
e.stopPropagation();
clearAll();
});
btnAll?.addEventListener("click", selectAll);
btnCan?.addEventListener("click", () => setExpanded(false)); // no refocus
btnApp?.addEventListener("click", () => setExpanded(false)); // no refocus
btnCan?.addEventListener("click", () => setExpanded(false));
btnApp?.addEventListener("click", () => setExpanded(false));
list.addEventListener("change", () => {
enforceMax();
updateSummary();
input.dispatchEvent(new Event("change", { bubbles: true }));
});
// Search filter
sch?.addEventListener("input", () => {
const q = sch.value.trim().toLowerCase();
root.querySelectorAll("[data-ms-item]").forEach((li) => {
@@ -345,12 +380,10 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
});
});
// Close on outside click without refocusing input
document.addEventListener("click", (e) => {
if (!root.contains(e.target)) setExpanded(false); // note: no refocusInput
if (!root.contains(e.target)) setExpanded(false);
});
// Init
enforceMax();
updateSummary();
</script>