diff --git a/packages/core/package.json b/packages/core/package.json index a7f3b8347..519ba9af2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -19,12 +19,13 @@ }, "./prime.css": "./prime.css", "./plugins": "./plugins.ts", - "./theme": "./theme.ts" + "./theme": "./src/lib/theme.ts" }, "svelte": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ "dist", + "src/lib/theme.ts", "theme.ts", "plugins.ts", "prime.css", @@ -86,6 +87,7 @@ "publint": "^0.2.6", "svelte": "^4.2.8", "svelte-check": "^3.6.2", + "svelte-select": "^5.8.3", "tailwindcss": "^3.3.7", "tslib": "^2.6.2", "type-fest": "^4.8.3", diff --git a/packages/core/src/lib/select/__tests__/searchable-select.spec.ts b/packages/core/src/lib/select/__tests__/autocomplete-input.spec.ts similarity index 86% rename from packages/core/src/lib/select/__tests__/searchable-select.spec.ts rename to packages/core/src/lib/select/__tests__/autocomplete-input.spec.ts index fb21a22f7..947ce5c1f 100644 --- a/packages/core/src/lib/select/__tests__/searchable-select.spec.ts +++ b/packages/core/src/lib/select/__tests__/autocomplete-input.spec.ts @@ -3,10 +3,9 @@ import { act, render, screen, within } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; import type { ComponentProps } from 'svelte'; -import { SearchableSelect as Subject, InputStates } from '$lib'; +import { AutocompleteInput as Subject, InputStates } from '$lib'; const onChange = vi.fn(); -const onMultiChange = vi.fn(); const onFocus = vi.fn(); const onBlur = vi.fn(); const detailedOptions = [ @@ -29,7 +28,6 @@ const renderSubject = (props: Partial> = {}) => { return render(Subject, { options: stringOptions, onChange, - onMultiChange, onFocus, onBlur, ...props, @@ -175,7 +173,7 @@ describe('SearchableSelect', () => { it('closes the listbox if no options', async () => { const user = userEvent.setup(); - renderSubject({ exclusive: true, sort: 'reduce' }); + renderSubject(); const { search } = getResults(); await user.type(search, 'asdf'); @@ -362,7 +360,7 @@ describe('SearchableSelect', () => { // Define new options const newOptions = [ - { value: 'New Option 1' }, + 'New Option 1', { value: 'opt1', label: 'New Option 2' }, { value: 'opt3', label: 'New Option 3', icon: 'apple' as const }, ]; @@ -386,7 +384,6 @@ describe('SearchableSelect', () => { const user = userEvent.setup(); renderSubject({ options: detailedOptions, - exclusive: true, }); const { search } = getResults(); @@ -411,7 +408,6 @@ describe('SearchableSelect', () => { const user = userEvent.setup(); renderSubject({ options: detailedOptions, - exclusive: true, }); const { search } = getResults(); @@ -464,24 +460,10 @@ describe('SearchableSelect', () => { expect(onChange).not.toHaveBeenCalled(); }); - it('keeps last selected value if menu is closed with escape (non exclusive)', async () => { + it('keeps last selected value if menu is closed with escape', async () => { const user = userEvent.setup(); renderSubject(); - const { search } = getResults(); - await user.type(search, 'testFoo{Enter}'); - expect(onChange).toHaveBeenCalledWith('testFoo'); - expect(search).toHaveValue('testFoo'); - onChange.mockReset(); - await user.type(search, 'ohNoIMeantToClickElsewhereOops{Escape}{Tab}'); - expect(search).toHaveValue('testFoo'); - expect(onChange).not.toHaveBeenCalled(); - }); - - it('keeps last selected value if menu is closed with escape (exclusive)', async () => { - const user = userEvent.setup(); - renderSubject({ exclusive: true }); - const { search } = getResults(); await user.type(search, 'the other{Enter}'); expect(onChange).toHaveBeenCalledWith('the other side'); @@ -492,89 +474,6 @@ describe('SearchableSelect', () => { expect(onChange).not.toHaveBeenCalled(); }); - it('has an "other" option when not exclusive', async () => { - const user = userEvent.setup(); - renderSubject(); - - const { search } = getResults(); - await user.type(search, 'hello'); - const { options } = getResults(); - - expect(options).toHaveLength(3); - expect(options[0]).toHaveAccessibleName('hello from'); - expect(options[1]).toHaveAccessibleName('the other side'); - expect(options[2]).toHaveAccessibleName('hello'); - expect(options[2]).toHaveAttribute('aria-selected', 'false'); - }); - - it('sets an "other" option as active when no search matches', async () => { - const user = userEvent.setup(); - renderSubject(); - - const { search } = getResults(); - await user.type(search, 'asdf'); - const { options } = getResults(); - - expect(options[2]).toHaveAccessibleName('asdf'); - expect(options[2]).toHaveAttribute('aria-selected', 'true'); - expect(search).toHaveAttribute('aria-activedescendant', options[2]?.id); - }); - - it('has no "other" option when value empty', async () => { - const user = userEvent.setup(); - renderSubject(); - - const { search } = getResults(); - await user.click(search); - const { options } = getResults(); - - expect(options).toHaveLength(2); - }); - - it('has no "other" option when value matches', async () => { - const user = userEvent.setup(); - renderSubject(); - - const { search } = getResults(); - await user.type(search, 'hello from'); - const { options } = getResults(); - - expect(options).toHaveLength(2); - }); - - it('has no "other" option when exclusive', async () => { - const user = userEvent.setup(); - renderSubject({ exclusive: true }); - - const { search } = getResults(); - await user.type(search, 'hello'); - const { options } = getResults(); - - expect(options).toHaveLength(2); - }); - - it('has an "other" option when value matches exclusivity function', async () => { - const user = userEvent.setup(); - renderSubject({ exclusive: (value: string) => value === 'hello' }); - - const { search } = getResults(); - await user.type(search, 'hello'); - const { options } = getResults(); - - expect(options).toHaveLength(3); - }); - - it('adds a prefix to the "other" option display text', async () => { - const user = userEvent.setup(); - renderSubject({ otherOptionPrefix: 'You said:' }); - - const { search } = getResults(); - await user.type(search, 'hello'); - const { options } = getResults(); - - expect(options[2]).toHaveAccessibleName('You said: hello'); - }); - it('closes listbox on escape', async () => { const user = userEvent.setup(); renderSubject(); @@ -755,7 +654,7 @@ describe('SearchableSelect', () => { expect(search).toHaveAttribute('aria-expanded', 'false'); }); - describe('multiple mode', () => { + describe.skip('multiple mode', () => { it('can select multiple options without closing', async () => { const user = userEvent.setup(); renderSubject({ multiple: true }); diff --git a/packages/core/src/lib/select/autocomplete-input.svelte b/packages/core/src/lib/select/autocomplete-input.svelte new file mode 100644 index 000000000..669ca8dc6 --- /dev/null +++ b/packages/core/src/lib/select/autocomplete-input.svelte @@ -0,0 +1,158 @@ + + + diff --git a/packages/core/src/lib/select/index.ts b/packages/core/src/lib/select/index.ts index 83c03a00a..06cb55fc0 100644 --- a/packages/core/src/lib/select/index.ts +++ b/packages/core/src/lib/select/index.ts @@ -1,5 +1,5 @@ export { SortOptions, type SortOption } from './search'; -export { default as SearchableSelect } from './searchable-select.svelte'; +export { default as AutocompleteInput } from './autocomplete-input.svelte'; export { default as Multiselect } from './multiselect.svelte'; export { default as Select } from './select.svelte'; export { default as SelectInput } from './select-input.svelte'; diff --git a/packages/core/src/lib/select/searchable-select.svelte b/packages/core/src/lib/select/searchable-select.svelte deleted file mode 100644 index 6ae7df9d4..000000000 --- a/packages/core/src/lib/select/searchable-select.svelte +++ /dev/null @@ -1,437 +0,0 @@ - - - - -{#if isExpanded} - -
    - {#each allOptions as { option, highlight } (option.value)} - {@const isActive = activeOption?.option === option} - {@const isSelected = multiple ? false : isActive} - {@const isChecked = multiple - ? values.includes(option.value) - : undefined} - {@const isOther = otherOption?.option === option} - {@const descriptionID = uniqueId('combobox-list-item-description')} - - {#if isOther && allOptions.length > 1} -
  • - {/if} - -
  • handleSelect(option)} - bind:this={optionElements[option.value]} - > -
    - - - {#if multiple} - - {/if} - {#if option.icon} - - {/if} -
    -

    - {#if highlight !== undefined} - {@const [prefix, match, suffix] = highlight} - {prefix}{match}{suffix} - {:else if isOther && otherOptionPrefix} - {otherOptionPrefix} {optionDisplayValue(option)} - {:else} - {optionDisplayValue(option)} - {/if} -

    - {#if option.description} -

    - {option.description} -

    - {/if} -
    -
    -
  • - {/each} - -
-
-{/if} diff --git a/packages/core/theme.ts b/packages/core/src/lib/theme.ts similarity index 100% rename from packages/core/theme.ts rename to packages/core/src/lib/theme.ts diff --git a/packages/core/src/routes/+page.svelte b/packages/core/src/routes/+page.svelte index 3fbe6f95f..487bb0772 100644 --- a/packages/core/src/routes/+page.svelte +++ b/packages/core/src/routes/+page.svelte @@ -1,5 +1,6 @@