diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d54ad54..20c8503 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,3 +12,5 @@ on: jobs: tests: uses: janosh/workflows/.github/workflows/npm-test-release.yml@main + with: + install-cmd: npm install -f diff --git a/src/lib/MultiSelect.svelte b/src/lib/MultiSelect.svelte index f7afc2d..d0e094c 100644 --- a/src/lib/MultiSelect.svelte +++ b/src/lib/MultiSelect.svelte @@ -1,10 +1,11 @@ (drag_idx = idx)} on:dragover|preventDefault class:active={drag_idx === idx} + style={get_style(option, `selected`)} > @@ -662,6 +653,7 @@ on:blur={() => (activeIndex = null)} role="option" aria-selected="false" + style={get_style(option,`option`)} > diff --git a/src/lib/types.ts b/src/lib/types.ts index 68e508c..7a0b957 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,5 +1,9 @@ export type Option = string | number | ObjectOption +// single CSS string or an object with keys 'option' and 'selected', each a string, +// which only apply to the dropdown list and list of selected options, respectively +export type OptionStyle = string | { option: string; selected: string } + export type ObjectOption = { label: string | number // user-displayed text value?: unknown // associated value, can be anything incl. objects (defaults to label if undefined) @@ -8,6 +12,7 @@ export type ObjectOption = { preselected?: boolean // make this option selected on page load (before any user interaction) disabledTitle?: string // override the default disabledTitle = 'This option is disabled' selectedTitle?: string // tooltip to display when this option is selected and hovered + style?: OptionStyle [key: string]: unknown // allow any other keys users might want } diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..812af1a --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,40 @@ +import type { Option, OptionStyle } from './types' + +// get the label key from an option object or the option itself if it's a string or number +export const get_label = (opt: Option) => { + if (opt instanceof Object) { + if (opt.label === undefined) { + console.error( + `MultiSelect option ${JSON.stringify( + opt, + )} is an object but has no label key`, + ) + } + return opt.label + } + return `${opt}` +} + +export function get_style( + option: { style?: OptionStyle; [key: string]: unknown } | string | number, + key: 'selected' | 'option' | null = null, +) { + if (!option?.style) return null + if (![`selected`, `option`, null].includes(key)) { + console.error(`MultiSelect: Invalid key=${key} for get_style`) + return + } + if (typeof option == `object` && option.style) { + if (typeof option.style == `string`) { + return option.style + } + if (typeof option.style == `object`) { + if (key && key in option.style) return option.style[key] + else { + console.error( + `Invalid style object for option=${JSON.stringify(option)}`, + ) + } + } + } +} diff --git a/src/routes/(demos)/ui/+page.svx b/src/routes/(demos)/ui/+page.svx index c673e7e..3369226 100644 --- a/src/routes/(demos)/ui/+page.svx +++ b/src/routes/(demos)/ui/+page.svx @@ -7,10 +7,17 @@ ({ label, style: `background-color: ${random_color()}` }))} placeholder="Pick your favorite foods" removeAllTitle="Remove all foods" invalid diff --git a/tests/unit/MultiSelect.test.ts b/tests/unit/MultiSelect.test.ts index 82f1158..fd36663 100644 --- a/tests/unit/MultiSelect.test.ts +++ b/tests/unit/MultiSelect.test.ts @@ -1,4 +1,5 @@ import MultiSelect, { type MultiSelectEvents, type Option } from '$lib' +import { get_label, get_style } from '$lib/utils' import { tick } from 'svelte' import { describe, expect, test, vi } from 'vitest' import { doc_query } from '.' @@ -532,7 +533,7 @@ test.each([ [[{ label: `foo` }, { label: `bar` }, { label: `baz` }]], [[{ label: `foo`, value: 1, key: `whatever` }]], ])(`single remove button removes 1 selected option`, async (options) => { - const { get_label } = new MultiSelect({ + new MultiSelect({ target: document.body, props: { options, selected: [...options] }, }) @@ -1181,3 +1182,90 @@ test.each([[true], [-1], [3.5], [`foo`], [{}]])( ) }, ) + +const css_str = `test-style` + +test.each([ + // Invalid key cases + [css_str, `invalid`, `MultiSelect: Invalid key=invalid for get_style`], + // Valid key cases + [css_str, `selected`, css_str], + [css_str, `option`, css_str], + [css_str, null, css_str], + // Object style cases + [ + { selected: `selected-style`, option: `option-style` }, + `selected`, + `selected-style`, + ], + [ + { selected: `selected-style`, option: `option-style` }, + `option`, + `option-style`, + ], + // Invalid object style cases + [ + { invalid: `invalid-style` }, + `selected`, + `Invalid style object for option=${JSON.stringify({ + style: { invalid: `invalid-style` }, + })}`, + ], +])( + `get_style returns correct style for different option and key combinations`, + async (style, key, expected) => { + console.error = vi.fn() + + // @ts-expect-error test invalid option + const result = get_style({ style }, key) + + if (expected.startsWith(`Invalid`) || expected.startsWith(`MultiSelect`)) { + expect(console.error).toHaveBeenCalledTimes(1) + expect(console.error).toHaveBeenCalledWith(expected) + } else { + expect(result).toBe(expected) + } + }, +) + +test.each([ + // Invalid key cases + [`color: red;`, `invalid`, ``], + // Valid key cases + [`color: red;`, `selected`, `color: red;`], + [`color: red;`, `option`, `color: red;`], + [`color: red;`, null, `color: red;`], + // Object style cases + [ + { selected: `color: red;`, option: `color: blue;` }, + `selected`, + `color: red;`, + ], + [ + { selected: `color: red;`, option: `color: blue;` }, + `option`, + `color: blue;`, + ], + // Invalid object style cases + [{ invalid: `color: green;` }, `selected`, ``], +])( + `MultiSelect applies correct styles to
  • elements for different option and key combinations`, + async (style, key, expected_css) => { + const options = [{ label: `foo`, style }] + + new MultiSelect({ + target: document.body, + props: { options, selected: key === `selected` ? options : [] }, + }) + + await tick() + + if (key === `selected`) { + const selected_li = document.querySelector(`ul.selected > li`) + expect(selected_li.style.cssText).toBe(expected_css) + } else if (key === `option`) { + const option_li = document.querySelector(`ul.options > li`) + expect(option_li.style.cssText).toBe(expected_css) + } + }, +)