From e7f2d119d450b6ddfee67d497b099544f65df043 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Wed, 10 Sep 2025 05:27:37 -0400 Subject: [PATCH 1/4] Add prop `fuzzy: boolean = true` to `MultiSelect` and `CmdPalette` for fuzzy option filtering (#334) * improve type safety by extending HTMLAttributes in components where appropriate replaces untyped [key: string]: unknown ...rest props - update package.json deps - turn off 'svelte/no-navigation-without-resolve' rule in eslint.config.js * enhance tooltip attachment: add style and disabled args - Add support for custom styles and handling of disabled state. - new unit tests for tooltip * add prop fuzzy: boolean = true to MultiSelect and CmdPalette to enable fuzzy option filtering - Added fuzzy matching option to highlight_matches to highlight text the same as its filtered - Updated HighlightOptions type to include fuzzy property. - Refactored highlight_matches logic to handle both fuzzy and substring matching. - Improved unit tests for highlight_matches to cover various matching scenarios. - Updated MultiSelect and CmdPalette components to utilize fuzzy matching. * address coderabbit comments * tweaks * fix MultiSelect arrow key navigation wrap-around logic to include user message - Updated arrow key navigation to include user message when applicable - Adjusted active index calculation to account for user message presence - Improved unit tests for MultiSelect to verify navigation behavior with user messages * - fix the logic for slicing matching options to maxOptions slice(0, Math.max(0, maxOptions ?? 0) || Infinity) treats 0 as falsy and shows all options. Honor 0 explicitly - {#each matchingOptions.slice(0, Math.max(0, maxOptions ?? 0) || Infinity) as + {#each matchingOptions.slice(0, maxOptions == null ? Infinity : Math.max(0, maxOptions)) as option_item, idx (duplicates ? [key(option_item), idx] : key(option_item)) } - introduce a new `selected` constant to streamline the selection state handling - adjust aria-selected attribute to reflect the selected state accurately - enhance unit tests to ensure correct behavior with various maxOptions values * make min_items = 3 prop of PrevNext --- .pre-commit-config.yaml | 2 +- eslint.config.js | 1 + package.json | 22 +- readme.md | 8 +- src/lib/CmdPalette.svelte | 23 +- src/lib/CopyButton.svelte | 34 +- src/lib/Icon.svelte | 4 +- src/lib/MultiSelect.svelte | 110 +++--- src/lib/PrevNext.svelte | 13 +- src/lib/RadioButtons.svelte | 19 +- src/lib/Toggle.svelte | 7 +- src/lib/attachments.ts | 175 +++++++-- src/lib/index.ts | 1 + src/lib/types.ts | 42 +- src/lib/utils.ts | 79 ++-- src/routes/(demos)/attachments/+page.md | 33 +- tests/playwright/MultiSelect.test.ts | 94 +++++ tests/vitest/CmdPalette.svelte.test.ts | 353 ++++++++++++++++- tests/vitest/CodeExample.test.ts | 25 ++ tests/vitest/CopyButton.test.ts | 120 ++++++ tests/vitest/MultiSelect.svelte.test.ts | 45 ++- tests/vitest/PrevNext.test.ts | 4 +- tests/vitest/Toggle.test.ts | 14 + tests/vitest/attachments.test.ts | 489 +++++++++++------------- tests/vitest/utils.test.ts | 170 ++------ tsconfig.json | 6 +- 26 files changed, 1253 insertions(+), 640 deletions(-) create mode 100644 tests/vitest/CopyButton.test.ts diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d1a4b8d..11b0b615 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,7 +43,7 @@ repos: exclude: changelog\.md - repo: https://github.com/pre-commit/mirrors-eslint - rev: v9.33.0 + rev: v9.35.0 hooks: - id: eslint types: [file] diff --git a/eslint.config.js b/eslint.config.js index a85e97b2..918db0b2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,6 +15,7 @@ export default [ ], '@stylistic/quotes': [`error`, `backtick`, { avoidEscape: true }], 'svelte/no-at-html-tags': `off`, + 'svelte/no-navigation-without-resolve': `off`, }, }, { diff --git a/package.json b/package.json index 60da9da2..0e435ad3 100644 --- a/package.json +++ b/package.json @@ -13,30 +13,30 @@ "svelte": "^5.35.6" }, "devDependencies": { - "@playwright/test": "^1.54.2", - "@stylistic/eslint-plugin": "^5.2.3", + "@playwright/test": "^1.55.0", + "@stylistic/eslint-plugin": "^5.3.1", "@sveltejs/adapter-static": "^3.0.9", - "@sveltejs/kit": "^2.28.0", - "@sveltejs/package": "2.4.1", - "@sveltejs/vite-plugin-svelte": "^6.1.1", - "@types/node": "^24.2.1", + "@sveltejs/kit": "^2.37.0", + "@sveltejs/package": "2.5.0", + "@sveltejs/vite-plugin-svelte": "^6.1.4", + "@types/node": "^24.3.1", "@vitest/coverage-v8": "^3.2.4", - "eslint": "^9.33.0", - "eslint-plugin-svelte": "^3.11.0", + "eslint": "^9.35.0", + "eslint-plugin-svelte": "^3.12.1", "happy-dom": "^18.0.1", "hastscript": "^9.0.1", "mdsvex": "^0.12.6", "mdsvexamples": "^0.5.0", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", - "svelte": "^5.38.1", + "svelte": "^5.38.7", "svelte-check": "^4.3.1", "svelte-preprocess": "^6.0.3", "svelte-toc": "^0.6.2", "svelte2tsx": "^0.7.42", "typescript": "5.9.2", - "typescript-eslint": "^8.39.1", - "vite": "^7.1.2", + "typescript-eslint": "^8.42.0", + "vite": "^7.1.4", "vitest": "^3.2.4" }, "keywords": [ diff --git a/readme.md b/readme.md index 344a58e1..a35f7619 100644 --- a/readme.md +++ b/readme.md @@ -643,10 +643,10 @@ For example, here's how you might annoy your users with an alert every time one ```svelte { - if (e.detail.type === 'add') alert(`You added ${e.detail.option}`) - if (e.detail.type === 'remove') alert(`You removed ${e.detail.option}`) - if (e.detail.type === 'removeAll') alert(`You removed ${e.detail.options}`) + onchange={(event) => { + if (event.detail.type === 'add') alert(`You added ${event.detail.option}`) + if (event.detail.type === 'remove') alert(`You removed ${event.detail.option}`) + if (event.detail.type === 'removeAll') alert(`You removed ${event.detail.options}`) }} /> ``` diff --git a/src/lib/CmdPalette.svelte b/src/lib/CmdPalette.svelte index 03f05bdb..eed5d674 100644 --- a/src/lib/CmdPalette.svelte +++ b/src/lib/CmdPalette.svelte @@ -1,14 +1,15 @@ -

MultiSelect in Modal Demo

+

Portalled MultiSelect in Modal Demo

- -{#if show_modal} +{#if open_modal} {/if} diff --git a/tests/playwright/MultiSelect.test.ts b/tests/playwright/MultiSelect.test.ts index 54226341..c60bf267 100644 --- a/tests/playwright/MultiSelect.test.ts +++ b/tests/playwright/MultiSelect.test.ts @@ -747,8 +747,54 @@ test.describe(`portal feature`, () => { await expect(portalled_foods_options).not.toBeAttached() }) + test(`mobile touch selection works with portal enabled`, async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }) + await page.goto(`/portal`, { waitUntil: `networkidle` }) + await page.getByRole(`button`, { name: `Open Modal` }).click() + + const modal_content = page.locator(`div.modal-content.modal`) + const languages_input = modal_content.locator( + `div.multiselect input[placeholder='Choose languages...']`, + ) + + await languages_input.click() + + const portalled_languages_options = page.locator( + `body > ul.options[aria-expanded="true"]:has(li:has-text("${languages[0]}"))`, + ) + await expect(portalled_languages_options).toBeVisible() + + // Simulate the race condition that causes the bug + const dropdown_stays_open = await page.evaluate(() => { + const outerDiv = document.querySelector(`div.multiselect`) + const portalled_option = document.querySelector( + `body > ul.options li[role="option"]`, + ) + const dropdown = document.querySelector(`body > ul.options`) + + if (!outerDiv || !portalled_option || !dropdown) return false + + // Simulate click outside event on portalled element + const click_event = new MouseEvent(`click`, { bubbles: true }) + Object.defineProperty(click_event, `target`, { value: portalled_option }) + globalThis.dispatchEvent(click_event) + + return dropdown.getAttribute(`aria-expanded`) === `true` + }) + + // Without fix: dropdown closes, test times out. With fix: dropdown stays open, test passes. + expect(dropdown_stays_open).toBe(true) + + await portalled_languages_options.getByRole(`option`, { + name: languages[0], + exact: true, + }).click() + await expect(modal_content.getByRole(`button`, { name: `Remove ${languages[0]}` })) + .toBeVisible() + }) + test(`dropdowns in modal render in body when portal is active`, async ({ page }) => { - await page.goto(`/modal`, { waitUntil: `networkidle` }) + await page.goto(`/portal`, { waitUntil: `networkidle` }) await page.getByRole(`button`, { name: `Open Modal` }).click() From ad02b140032f405c1ef66aaacf1912d296239518 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sat, 4 Oct 2025 16:03:03 -0700 Subject: [PATCH 3/4] immediately hide tooltip on scroll + icon fixes in MultiSelect + type fixes in CmdPalette and PrevNext - unit tests for tooltip to verify new scroll behavior - remove RadioButtons component and related tests --- .pre-commit-config.yaml | 2 +- package.json | 30 ++++----- src/lib/CmdPalette.svelte | 11 +-- src/lib/MultiSelect.svelte | 19 +++--- src/lib/PrevNext.svelte | 23 +++---- src/lib/RadioButtons.svelte | 108 ------------------------------ src/lib/attachments.ts | 4 ++ src/lib/index.ts | 1 - tests/vitest/RadioButtons.test.ts | 45 ------------- tests/vitest/attachments.test.ts | 44 ++++++++++-- 10 files changed, 86 insertions(+), 201 deletions(-) delete mode 100644 src/lib/RadioButtons.svelte delete mode 100644 tests/vitest/RadioButtons.test.ts diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 51e16ca4..ef1813ae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,7 +43,7 @@ repos: exclude: changelog\.md - repo: https://github.com/pre-commit/mirrors-eslint - rev: v9.36.0 + rev: v9.37.0 hooks: - id: eslint types: [file] diff --git a/package.json b/package.json index 93376664..abfc2f2b 100644 --- a/package.json +++ b/package.json @@ -13,30 +13,30 @@ "svelte": "^5.35.6" }, "devDependencies": { - "@playwright/test": "^1.55.0", + "@playwright/test": "^1.55.1", "@stylistic/eslint-plugin": "^5.4.0", - "@sveltejs/adapter-static": "^3.0.9", - "@sveltejs/kit": "^2.42.2", - "@sveltejs/package": "2.5.3", - "@sveltejs/vite-plugin-svelte": "^6.2.0", - "@types/node": "^24.5.2", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.43.8", + "@sveltejs/package": "2.5.4", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@types/node": "^24.6.2", "@vitest/coverage-v8": "^3.2.4", - "eslint": "^9.36.0", - "eslint-plugin-svelte": "^3.12.3", - "happy-dom": "^18.0.1", + "eslint": "^9.37.0", + "eslint-plugin-svelte": "^3.12.4", + "happy-dom": "^19.0.2", "hastscript": "^9.0.1", "mdsvex": "^0.12.6", "mdsvexamples": "^0.5.0", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", - "svelte": "^5.39.3", - "svelte-check": "^4.3.1", + "svelte": "^5.39.8", + "svelte-check": "^4.3.2", "svelte-preprocess": "^6.0.3", "svelte-toc": "^0.6.2", - "svelte2tsx": "^0.7.43", - "typescript": "5.9.2", - "typescript-eslint": "^8.44.0", - "vite": "^7.1.6", + "svelte2tsx": "^0.7.44", + "typescript": "5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.9", "vitest": "^3.2.4" }, "keywords": [ diff --git a/src/lib/CmdPalette.svelte b/src/lib/CmdPalette.svelte index eed5d674..f287b7c1 100644 --- a/src/lib/CmdPalette.svelte +++ b/src/lib/CmdPalette.svelte @@ -9,7 +9,8 @@ action: (label: string) => void } - interface Props extends Omit, `options`> { + interface Props + extends Omit>, `options`> { actions: Action[] triggers?: string[] close_keys?: string[] @@ -46,14 +47,16 @@ } function close_if_outside(event: MouseEvent) { - const target = event.target as HTMLElement + const target = event.target + if (!target || !(target instanceof HTMLElement)) return if (open && !dialog?.contains(target) && !target.closest(`ul.options`)) { open = false } } - function trigger_action_and_close(data: { option: Option }) { - const { action, label } = data.option as Action + function trigger_action_and_close({ option }: { option: Option }) { + const { action, label } = (option ?? {}) as Action + if (!action) return action(label) open = false } diff --git a/src/lib/MultiSelect.svelte b/src/lib/MultiSelect.svelte index aa28a738..7e2a192d 100644 --- a/src/lib/MultiSelect.svelte +++ b/src/lib/MultiSelect.svelte @@ -437,7 +437,8 @@ event.stopPropagation() // Only remove option if it wouldn't violate minSelect if (minSelect === null || selected.length > minSelect) { - remove(selected.at(-1) as Option, event) + const last_option = selected.at(-1) + if (last_option) remove(last_option, event) } // Don't prevent default, allow normal backspace behavior if not removing } // make first matching option active on any keypress (if none of the above special cases match) @@ -597,7 +598,7 @@ window.addEventListener(`scroll`, update_position, true) window.addEventListener(`resize`, update_position) - $effect.pre(() => { + $effect(() => { if (open && target_node) update_position() else node.hidden = true }) @@ -669,8 +670,7 @@ {:else} {/if}
    + {/if} {/if} @@ -741,7 +741,7 @@ {autocomplete} {inputmode} {pattern} - placeholder={selected.length == 0 ? placeholder : null} + placeholder={selected.length === 0 ? placeholder : null} aria-invalid={invalid ? `true` : null} ondrop={() => false} onmouseup={open_dropdown} @@ -782,8 +782,7 @@ {:else} @@ -807,7 +806,7 @@ {#if removeIcon} {@render removeIcon()} {:else} - + {/if} {/if} @@ -917,7 +916,7 @@ {@const text_input_is_duplicate = selected.map(get_label).includes(searchText)} {@const is_dupe = !duplicates && text_input_is_duplicate && `dupe`} {@const can_create = Boolean(allowUserOptions && createOptionMsg) && `create`} - {@const no_match = Boolean(matchingOptions?.length == 0 && noMatchingOptionsMsg) && + {@const no_match = Boolean(matchingOptions?.length === 0 && noMatchingOptionsMsg) && `no-match`} {@const msgType = is_dupe || can_create || no_match} {#if msgType} diff --git a/src/lib/PrevNext.svelte b/src/lib/PrevNext.svelte index f9d3cc89..60a19a5c 100644 --- a/src/lib/PrevNext.svelte +++ b/src/lib/PrevNext.svelte @@ -2,17 +2,18 @@ import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' - export type Item = string | [string, unknown] - type T = $$Generic + export type Item = [string, unknown] interface Props extends Omit, `children` | `onkeyup`> { - items?: T[] + items?: (string | Item)[] node?: string current?: string log?: `verbose` | `errors` | `silent` nav_options?: { replace_state: boolean; no_scroll: boolean } titles?: { prev: string; next: string } - onkeyup?: ((obj: { prev: Item; next: Item }) => Record) | null + onkeyup?: + | ((obj: { prev: Item; next: Item }) => Record) + | null prev_snippet?: Snippet<[{ item: Item }]> children?: Snippet<[{ kind: `prev` | `next`; item: Item }]> between?: Snippet<[]> @@ -26,10 +27,7 @@ log = `errors`, nav_options = { replace_state: true, no_scroll: true }, titles = { prev: `← Previous`, next: `Next →` }, - onkeyup = ({ prev, next }) => ({ - ArrowLeft: prev[0], - ArrowRight: next[0], - }), + onkeyup = ({ prev, next }) => ({ ArrowLeft: prev[0], ArrowRight: next[0] }), prev_snippet, children, between, @@ -40,9 +38,9 @@ // Convert items to consistent [key, value] format let items_arr = $derived( - (items ?? []).map(( - itm, - ) => (typeof itm === `string` ? [itm, itm] : itm)) as Item[], + (items ?? []).map( + (itm) => (typeof itm === `string` ? [itm, itm] : itm), + ) as Item[], ) // Calculate prev/next items with wraparound @@ -71,9 +69,10 @@ function handle_keyup(event: KeyboardEvent) { if (!onkeyup) return if ((items_arr?.length ?? 0) < min_items) return + if (!prev || !next) return const key_map = onkeyup({ prev, next }) const to = key_map[event.key] - if (to) { + if (to !== undefined) { const { replace_state, no_scroll } = nav_options const [scroll_x, scroll_y] = no_scroll ? [window.scrollX, window.scrollY] diff --git a/src/lib/RadioButtons.svelte b/src/lib/RadioButtons.svelte deleted file mode 100644 index 2f19ab45..00000000 --- a/src/lib/RadioButtons.svelte +++ /dev/null @@ -1,108 +0,0 @@ - - -
    - {#each options as option (JSON.stringify(option))} - {@const label = get_label(option)} - {@const active = selected != null && - label === get_label(selected as unknown as GenericOption)} - - {/each} -
    - - diff --git a/src/lib/attachments.ts b/src/lib/attachments.ts index 9c420882..a1d2c169 100644 --- a/src/lib/attachments.ts +++ b/src/lib/attachments.ts @@ -531,8 +531,12 @@ export const tooltip = (options: TooltipOptions = {}): Attachment => (node: Elem events.forEach((event, idx) => element.addEventListener(event, handlers[idx])) + // Hide tooltip when user scrolls + globalThis.addEventListener(`scroll`, hide_tooltip, true) + return () => { events.forEach((event, idx) => element.removeEventListener(event, handlers[idx])) + globalThis.removeEventListener(`scroll`, hide_tooltip, true) const original_title = element.getAttribute(`data-original-title`) if (original_title) { diff --git a/src/lib/index.ts b/src/lib/index.ts index 21e9f8ef..76b1dc13 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -8,7 +8,6 @@ export { default as GitHubCorner } from './GitHubCorner.svelte' export { default as Icon } from './Icon.svelte' export { default, default as MultiSelect } from './MultiSelect.svelte' export { default as PrevNext } from './PrevNext.svelte' -export { default as RadioButtons } from './RadioButtons.svelte' export { default as Toggle } from './Toggle.svelte' export * from './types' export * from './utils' diff --git a/tests/vitest/RadioButtons.test.ts b/tests/vitest/RadioButtons.test.ts deleted file mode 100644 index 18b5bb06..00000000 --- a/tests/vitest/RadioButtons.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { RadioButtons } from '$lib' -import { mount } from 'svelte' -import { afterEach, beforeEach, describe, expect, test } from 'vitest' - -describe(`RadioButtons`, () => { - let target: HTMLElement - - beforeEach(() => (target = document.body)) - afterEach(() => (target.innerHTML = ``)) - - const options = [`Option 1`, `Option 2`, `Option 3`] - - test(`renders radio buttons for each option`, () => { - mount(RadioButtons, { target, props: { options } }) - const inputs = target.querySelectorAll(`input[type='radio']`) - expect(inputs).toHaveLength(3) - }) - - test(`selects correct option when value is set`, () => { - mount(RadioButtons, { target, props: { options, value: `Option 2` } }) - const inputs = target.querySelectorAll( - `input[type='radio']`, - ) as NodeListOf - - // Check that the correct input has the value attribute matching the selected value - expect(inputs[1].value).toBe(`Option 2`) - // The component may not automatically set checked state, so just verify the value is correct - expect(Array.from(inputs).some((input) => input.value === `Option 2`)).toBe( - true, - ) - }) - - test(`has consistent name attribute`, () => { - mount(RadioButtons, { target, props: { options } }) - const inputs = target.querySelectorAll(`input[type='radio']`) - const names = Array.from(inputs).map((input) => input.getAttribute(`name`)) - expect(new Set(names).size).toBe(1) - }) - - test(`renders with custom name`, () => { - mount(RadioButtons, { target, props: { options, name: `custom-name` } }) - const inputs = target.querySelectorAll(`input[type='radio']`) - expect(inputs[0].getAttribute(`name`)).toBe(`custom-name`) - }) -}) diff --git a/tests/vitest/attachments.test.ts b/tests/vitest/attachments.test.ts index b4f9aef9..59a09238 100644 --- a/tests/vitest/attachments.test.ts +++ b/tests/vitest/attachments.test.ts @@ -398,6 +398,34 @@ describe(`tooltip`, () => { expect(element.hasAttribute(`data-original-title`)).toBe(true) }) + + it(`should hide tooltip on scroll`, () => { + vi.useFakeTimers() + const element = create_element() + element.title = `test` + mock_bounds(element) + setup_tooltip(element, { delay: 0 }) + + element.dispatchEvent(new MouseEvent(`mouseenter`, { bubbles: true })) + vi.runAllTimers() + expect(document.querySelector(`.custom-tooltip`)).toBeTruthy() + + globalThis.dispatchEvent(new Event(`scroll`, { bubbles: true })) + expect(document.querySelector(`.custom-tooltip`)).toBeFalsy() + + vi.useRealTimers() + }) + + it(`should remove scroll listener on cleanup`, () => { + const element = create_element() + element.title = `test` + const spy = vi.spyOn(globalThis, `removeEventListener`) + + setup_tooltip(element)?.() + + expect(spy).toHaveBeenCalledWith(`scroll`, expect.any(Function), true) + spy.mockRestore() + }) }) describe(`Global State Management`, () => { @@ -715,10 +743,10 @@ describe(`highlight_matches`, () => { it.each([ // Early returns - [`CSS not supported`, undefined, `test`, `test`, false, 0, 0], - [`no query`, true, ``, `test`, false, 0, 0], - [`CSS not supported (fuzzy)`, undefined, `auo`, `auo`, true, 0, 0], - [`no query (fuzzy)`, true, ``, `auo`, true, 0, 0], + [`CSS not supported`, undefined, `test`, `test`, false, 0, undefined], + [`no query`, true, ``, `test`, false, 0, undefined], + [`CSS not supported (fuzzy)`, undefined, `auo`, `auo`, true, 0, undefined], + [`no query (fuzzy)`, true, ``, `auo`, true, 0, undefined], // Substring highlighting (fuzzy=false) [ @@ -728,6 +756,7 @@ describe(`highlight_matches`, () => { `test`, false, 1, + undefined, ], [ `multiple matches`, @@ -736,6 +765,7 @@ describe(`highlight_matches`, () => { `test`, false, 1, + undefined, ], [ `case insensitive`, @@ -744,6 +774,7 @@ describe(`highlight_matches`, () => { `test`, false, 1, + undefined, ], [ `no matches`, @@ -752,10 +783,11 @@ describe(`highlight_matches`, () => { `xyz`, false, 1, + undefined, ], // Fuzzy highlighting (fuzzy=true) - [`fuzzy match`, true, `

    allow-user-options

    `, `auo`, true, 1], + [`fuzzy match`, true, `

    allow-user-options

    `, `auo`, true, 1, undefined], [ `fuzzy case insensitive`, true, @@ -763,6 +795,7 @@ describe(`highlight_matches`, () => { `auo`, true, 1, + undefined, ], [ `fuzzy no matches`, @@ -771,6 +804,7 @@ describe(`highlight_matches`, () => { `xyz`, true, 1, + undefined, ], [ `skip with node_filter`, From 9c0427bc389ad38706c50f3efca2999a2e94b111 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sat, 4 Oct 2025 16:16:29 -0700 Subject: [PATCH 4/4] v11.2.4 --- changelog.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 9878ee6c..a95a2784 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## [v11.2.4](https://github.com/janosh/svelte-multiselect/compare/v11.2.3...v11.2.4) + +> 4 October 2025 + +- Add prop `fuzzy: boolean = true` to `MultiSelect` and `CmdPalette` for fuzzy option filtering (#334) by @janosh +- fix broken mobile touch selection with portal enabled (#336) by @janosh +- immediately hide tooltip on scroll + icon fixes in MultiSelect + type fixes in CmdPalette and PrevNext by @janosh + ## [v11.2.3](https://github.com/janosh/svelte-multiselect/compare/v11.2.2...v11.2.3) > 13 August 2025 diff --git a/package.json b/package.json index abfc2f2b..3e624c5a 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "homepage": "https://janosh.github.io/svelte-multiselect", "repository": "https://github.com/janosh/svelte-multiselect", "license": "MIT", - "version": "11.2.3", + "version": "11.2.4", "type": "module", "svelte": "./dist/index.js", "bugs": "https://github.com/janosh/svelte-multiselect/issues",