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)
+ }
+ },
+)