From 33864bed990c97711c7d53dd1aa82af6481d7a9d Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sun, 27 Feb 2022 17:18:50 +0000 Subject: [PATCH] More tests (#51) * bump pre-commit/mirrors-eslint to fix false-pos undefined vars from {@const} destructuring * document all props in readme (removeBtnTitle, removeAllTitle, defaultDisabledTitle were missing) * test all props are mentioned in readme * add CSS resets for margin + padding of input + button * add test for remove all button (probably brittle), need to figure out how to isolate test each into its own DOM --- .pre-commit-config.yaml | 2 +- readme.md | 47 ++++++++++++++++--------------- src/components/Example.svelte | 3 -- src/lib/MultiSelect.svelte | 11 ++++++-- static/global.css | 3 +- tests/multiselect.test.ts | 53 ++++++++++++++++++++++++++++++++--- 6 files changed, 84 insertions(+), 35 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index abe4103..3fc6d86 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: exclude: yarn.lock - repo: https://github.com/pre-commit/mirrors-eslint - rev: v8.6.0 + rev: v8.10.0 hooks: - id: eslint types: [file] diff --git a/readme.md b/readme.md index 3f7daa5..fd422a1 100644 --- a/readme.md +++ b/readme.md @@ -75,7 +75,7 @@ yarn add -D svelte-multiselect `Spring`, ] - let selected + let selected = [] Favorite Web Frameworks? @@ -92,26 +92,29 @@ Full list of props/bindable variables for this component:
-| name | default | description | -| :----------------- | :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `options` | required prop | Array of strings/numbers or `Option` objects that will be listed in the dropdown. See `src/lib/index.ts` for admissible fields. The `label` is the only mandatory one. It must also be unique. | -| `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. | -| `searchText` | `` | Text the user-entered to filter down on the list of options. Binds both ways, i.e. can also be used to set the input text. | -| `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. | -| `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. | -| `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. | -| `selectedLabels` | `[]` | Labels of currently selected options. | -| `selectedValues` | `[]` | Values of currently selected options. | -| `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. | -| `readonly` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. | -| `placeholder` | `undefined` | String shown in the text input when no option is selected. | -| `input` | `undefined` | Handle to the `` DOM node. | -| `id` | `undefined` | Applied to the `` element for associating HTML form `
@@ -238,7 +241,7 @@ If you only want to make small adjustments, you can pass the following CSS varia - `div.multiselect > ul.selected > li > input` - `color: var(--sms-text-color, inherit)`: Input text color. - `div.multiselect > ul.selected > li` - - `background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue))`: Background of selected options. + - `background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15))`: Background of selected options. - `height: var(--sms-selected-li-height)`: Height of selected options. - `ul.selected > li button:hover, button.remove-all:hover, button:focus` - `color: var(--sms-remove-x-hover-focus-color, lightskyblue)`: Color of the cross-icon buttons for removing all or individual selected options when in `:focus` or `:hover` state. diff --git a/src/components/Example.svelte b/src/components/Example.svelte index 28b2106..745ae19 100644 --- a/src/components/Example.svelte +++ b/src/components/Example.svelte @@ -7,10 +7,7 @@ let activeWeb: Option let selectedML: Option[] - // used to show user hint to try arrow keys but only once - let neverActive = true let showConfetti = false - $: if (activeWeb) neverActive = false const placeholder = `Take your pick...` const filterFunc = (op: Option, searchText: string) => { diff --git a/src/lib/MultiSelect.svelte b/src/lib/MultiSelect.svelte index 3580ede..4f087e1 100644 --- a/src/lib/MultiSelect.svelte +++ b/src/lib/MultiSelect.svelte @@ -36,7 +36,6 @@ export let removeBtnTitle = `Remove` export let removeAllTitle = `Remove all` - // https://github.com/sveltejs/svelte/issues/6964 export let defaultDisabledTitle = `This option is disabled` export let allowUserOptions: boolean | 'append' = false export let autoScroll = true @@ -50,7 +49,7 @@ if (!Array.isArray(selected)) console.error(`selected prop must be an array`) onMount(() => { - selected = _options.filter((op) => op?.preselected) + selected = _options.filter((op) => op?.preselected) ?? [] }) let wiggle = false @@ -366,7 +365,7 @@ display above those of another following shortly after it --> padding: 1pt 2pt 1pt 5pt; transition: 0.3s; white-space: nowrap; - background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue)); + background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15)); height: var(--sms-selected-li-height); } :where(div.multiselect > ul.selected > li button, button.remove-all) { @@ -383,6 +382,7 @@ display above those of another following shortly after it --> cursor: pointer; outline: none; padding: 0 2pt; + margin: 0; /* CSS reset */ } :where(ul.selected > li button:hover, button.remove-all:hover, button:focus) { color: var(--sms-remove-x-hover-focus-color, lightskyblue); @@ -391,6 +391,10 @@ display above those of another following shortly after it --> transform: scale(1.04); } + :where(div.multiselect input) { + margin: auto 0; /* CSS reset */ + padding: 0; /* CSS reset */ + } :where(div.multiselect > ul.selected > li > input) { border: none; outline: none; @@ -410,6 +414,7 @@ display above those of another following shortly after it --> outline: none; z-index: -1; opacity: 0; + pointer-events: none; } :where(div.multiselect > ul.options) { diff --git a/static/global.css b/static/global.css index f0cd883..965e857 100644 --- a/static/global.css +++ b/static/global.css @@ -18,10 +18,9 @@ body { body > div { display: flex; margin: auto; - max-width: 1000px; + max-width: 1100px; } main { - max-width: 50em; margin: auto; margin-bottom: 3em; width: 100%; diff --git a/tests/multiselect.test.ts b/tests/multiselect.test.ts index 0a5be0a..7812912 100644 --- a/tests/multiselect.test.ts +++ b/tests/multiselect.test.ts @@ -1,12 +1,15 @@ -import { fireEvent, render } from '@testing-library/svelte' -import { expect, test } from 'vitest' +import { cleanup, fireEvent, render } from '@testing-library/svelte' +import { readFileSync } from 'fs' +import { afterEach, expect, test } from 'vitest' import MultiSelect from '../src/lib' const options = [`Banana`, `Watermelon`, `Apple`, `Dates`, `Mango`] const placeholder = `Select fruits` +afterEach(cleanup) + test(`can focus input, enter text, toggle hidden options and select an option`, async () => { - const { getByPlaceholderText, getByText, container } = render(MultiSelect, { + const { getByText, container, getByPlaceholderText } = render(MultiSelect, { options, placeholder, }) @@ -15,7 +18,7 @@ test(`can focus input, enter text, toggle hidden options and select an option`, expect(ul_ops?.classList.contains(`hidden`)).to.equal(true) - const input = getByPlaceholderText(`Select fruits`) + const input = getByPlaceholderText(placeholder) await fireEvent.focus(input) await fireEvent.input(input, { target: { value: `Apple` } }) @@ -34,6 +37,48 @@ test(`can focus input, enter text, toggle hidden options and select an option`, expect(apple_sel.textContent?.trim()).toBe(`Apple`) }) +test(`readme documents all props`, () => { + const readme = readFileSync(`readme.md`, `utf8`) + + const instance = new MultiSelect({ + target: document.body, + props: { options }, + }) + + for (const prop of Object.keys(instance.$$.props)) { + expect(readme).to.contain(prop) + } +}) + +test(`remove all button`, async () => { + const { container, getByPlaceholderText } = render(MultiSelect, { + options, + placeholder, + }) + + const input = getByPlaceholderText(placeholder) + await fireEvent.focus(input) + + const ul_ops = container.querySelector(`ul.options`) + + expect(ul_ops?.children.length).toBe(options.length) + + const li_ops = container.querySelector(`ul.options`)?.children + for (const li of li_ops ?? []) { + await fireEvent.mouseDown(li) + } + + const ul_sel = container.querySelector(`ul.selected`) + // make sure all options are selected + expect(ul_sel?.textContent).toContain(`Mango Apple Banana`) + + const rm_all_btn = container.querySelector(`button[title='Remove all']`) + await fireEvent.mouseUp(rm_all_btn) + + const ul_sel_after = container.querySelector(`ul.selected`) + expect(ul_sel_after?.textContent).toBe(` `) // only input left +}) + test(`default export from index.ts is same as component file`, async () => { const { default: comp } = await import(`../src/lib/MultiSelect.svelte`) expect(comp).toBe(MultiSelect)