diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a5e5eaa..64a4ca05d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added a `toast` component with plain-text or Markdown content, icons, colors, six screen placements, configurable auto-dismiss timing, optional manual dismissal, URL-fragment triggers, and automatic stacking of queued notifications. - `sqlpage.send_mail` now supports rich email bodies. Use `body_html` for a caller-provided HTML alternative, or `body_md` to render Markdown as HTML. Messages retain a plain-text alternative; `body` may be omitted when `body_md` is used, and `body_md` and `body_html` cannot be combined. - Form `options_source` URLs now preserve existing query parameters when adding the dynamic `search` parameter. + - Searchable single-select form fields now close their dropdown after an option is selected. - Map coordinates that are not a pair of numbers, like a latitude with no longitude, are now reported in the browser console and skipped, instead of breaking the whole map. - Stacked charts now stack their series by `x` value instead of by point order, which used to give wrong totals when a series was missing a point. - `line`, `area`, `scatter`, `bubble` and `heatmap` charts with text labels on the x axis now line their series up by label, leaving a gap where a series skips one. diff --git a/examples/official-site/examples/form.sql b/examples/official-site/examples/form.sql index 913074e53..9ab19ff53 100644 --- a/examples/official-site/examples/form.sql +++ b/examples/official-site/examples/form.sql @@ -57,9 +57,17 @@ SELECT 'website' AS name, 'url' AS type, 'https://example.com' AS placeholder, SELECT 'header' AS type, 'Selection Types' AS label; -SELECT 'country' AS name, 'select' AS type, +SELECT 'country' AS name, 'select' AS type, '[{"label": "United States", "value": "US"}, {"label": "Canada", "value": "CA"}, {"label": "United Kingdom", "value": "GB"}]' AS options, - '**Select** (SQLPage custom) - Dropdown menu. Use for single choice from many options. Add `multiple` for multi-select. Use `searchable` for long lists. Set `dropdown` for enhanced UI.' AS description_md; + '**select**: basic dropdown menu. Use for single choice from many options' AS description_md; + +SELECT 'region' AS name, 'select' AS type, true as searchable, + '[{"label": "North America", "value": "NA"}, {"label": "South America", "value": "SA"}, {"label": "Europe", "value": "EU"}]' AS options, + '**select** with searchable: dropdown menu with searchable options' AS description_md; + +SELECT 'title' AS name, 'select' AS type, true as multiple, true as searchable, + '[{"label": "professor", "value": "professor"}, {"label": "doctor", "value": "doctor"}, {"label": "lord", "value": "lord"}]' AS options, + '**select** with multiple: dropdown menu with multiple selections' AS description_md; SELECT 'gender' AS name, 'radio' AS type, 'Male' AS value, 'Male' AS label, '**Radio** - Radio button for mutually exclusive choices. Create multiple rows with same `name` for a radio group. One option can be selected. Use for 2-5 options.' AS description_md; diff --git a/package-lock.json b/package-lock.json index 7d97b7b69..7512fdb1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,9 +86,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -106,9 +103,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -126,9 +120,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -146,9 +137,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ diff --git a/sqlpage/tomselect.js b/sqlpage/tomselect.js index 9460bf3e6..214608fa8 100644 --- a/sqlpage/tomselect.js +++ b/sqlpage/tomselect.js @@ -30,10 +30,8 @@ function sqlpage_select_dropdown_individual(s) { searchField: "label", create: s.dataset.create_new, maxOptions: null, - onItemAdd: function () { - this.setTextboxValue(""); - this.refreshOptions(); - }, + closeAfterSelect: !s.multiple, + clearAfterSelect: true, }); if (is_focused) tom.focus(); s.form?.addEventListener("reset", async () => { diff --git a/tests/end-to-end/globals.d.ts b/tests/end-to-end/globals.d.ts index 47ba8d7f0..1efc29329 100644 --- a/tests/end-to-end/globals.d.ts +++ b/tests/end-to-end/globals.d.ts @@ -5,6 +5,7 @@ interface TomSelectInstance { getValue(): string | string[]; setTextboxValue(value: string): void; focus(): void; + open(): void; options: Record; } diff --git a/tests/end-to-end/official-site.spec.ts b/tests/end-to-end/official-site.spec.ts index 7c9325b4c..4b3bd4681 100644 --- a/tests/end-to-end/official-site.spec.ts +++ b/tests/end-to-end/official-site.spec.ts @@ -526,6 +526,60 @@ test("form select combines initial options with remote search results", async ({ }); }); +test("form type=select searchable=true", async ({ page }) => { + await page.goto(`${BASE}/examples/form`); + + const form = page.locator("form").filter({ + has: page.locator('select[name="region"]'), + }); + const regionSelect = form.locator('select[name="region"]'); + const regionField = form.locator("label").filter({ + has: page.locator('select[name="region"]'), + }); + const regionCombobox = regionField.locator('input[role="combobox"]'); + const dropdown = regionField.getByRole("listbox"); + const selectedRegion = (name: string) => + regionField.getByText(name, { exact: true }).filter({ visible: true }); + + await expect(selectedRegion("North America")).toBeVisible(); + await expect(regionSelect).toHaveValue("NA"); + + await selectedRegion("North America").click(); + await expect(dropdown).toBeVisible(); + await expect(dropdown.getByRole("option")).toHaveCount(3); + + await regionCombobox.fill("south"); + await expect(dropdown.getByRole("option")).toHaveCount(1); + const southAmerica = dropdown.getByRole("option", { + name: "South America", + exact: true, + }); + await expect(southAmerica).toBeVisible(); + + await southAmerica.click(); + await page.evaluate( + () => + new Promise((resolve) => + requestAnimationFrame(() => requestAnimationFrame(() => resolve())), + ), + ); + await expect(dropdown).not.toBeVisible(); + await expect(regionCombobox).toHaveAttribute("aria-expanded", "false"); + await expect(regionSelect).toHaveValue("SA"); + await expect(selectedRegion("South America")).toBeVisible(); + + const terms = form.getByLabel("I accept the terms and conditions"); + await form + .locator("label") + .filter({ has: page.locator('input[name="terms"]') }) + .click(); + await expect(terms).toBeChecked(); + await form.getByRole("button", { name: /submit/i }).click(); + + await expect(page).toHaveURL(/\/examples\/show_variables\.sql$/); + await expect(page.getByText(":region = SA", { exact: true })).toBeVisible(); +}); + test("modal", async ({ page }) => { await page.goto(`${BASE}/documentation.sql?component=modal#component`); const openButton = page.getByRole("button", { name: "Open a simple modal" });