From 47c4c3c99ce347d1bad100fbd9c11901cbebcba8 Mon Sep 17 00:00:00 2001 From: DTCurrie Date: Thu, 24 Aug 2023 10:01:24 -0400 Subject: [PATCH 01/21] Add searchable select --- packages/core/src/lib/icon/icon.svelte | 26 +- packages/core/src/lib/index.ts | 2 +- packages/core/src/lib/input/input.svelte | 30 +- .../select/{ => __tests__}/select.spec.svelte | 5 +- .../lib/select/{ => __tests__}/select.spec.ts | 15 +- .../src/lib/select/searchable-select.svelte | 273 ++++++++++++++++++ .../core/src/lib/select/select-menu.svelte | 72 +++++ packages/core/src/lib/select/select.svelte | 67 ++++- packages/core/src/lib/select/utils.ts | 114 ++++++++ packages/core/src/routes/+page.svelte | 128 +++++--- 10 files changed, 636 insertions(+), 96 deletions(-) rename packages/core/src/lib/select/{ => __tests__}/select.spec.svelte (81%) rename packages/core/src/lib/select/{ => __tests__}/select.spec.ts (84%) create mode 100644 packages/core/src/lib/select/searchable-select.svelte create mode 100644 packages/core/src/lib/select/select-menu.svelte create mode 100644 packages/core/src/lib/select/utils.ts diff --git a/packages/core/src/lib/icon/icon.svelte b/packages/core/src/lib/icon/icon.svelte index 73e13a51..59d59cf7 100644 --- a/packages/core/src/lib/icon/icon.svelte +++ b/packages/core/src/lib/icon/icon.svelte @@ -17,15 +17,15 @@ import { paths } from './icons'; type Size = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'; -/** - * The size of the icon. - */ +/** The name of the icon. */ +export let name: string; + +/** The size of the icon. */ export let size: Size = 'base'; -/** - * The name of the icon. - */ -export let name = ''; +/** Additional CSS classes to pass to the button. */ +let extraClasses: cx.Argument = ''; +export { extraClasses as cx }; const hasNameProperty = Object.hasOwn(paths, name); const sizes: Record = { @@ -45,10 +45,14 @@ const sizes: Record = { https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label --> {#if icon !== ''} - - - + {/if} diff --git a/packages/core/src/lib/select/select.spec.svelte b/packages/core/src/lib/select/__tests__/select.spec.svelte similarity index 81% rename from packages/core/src/lib/select/select.spec.svelte rename to packages/core/src/lib/select/__tests__/select.spec.svelte index 27f3be8b..7be149bb 100644 --- a/packages/core/src/lib/select/select.spec.svelte +++ b/packages/core/src/lib/select/__tests__/select.spec.svelte @@ -10,12 +10,11 @@ - https://github.com/testing-library/svelte-testing-library/issues/48 --> setKeyboardControl(false)} + /> + + + + + {#if sortedOptions.length > 0} + {#each searchedOptions as { search, option }, index (option)} +
  • + +
  • + {/each} + {:else} +
  • No matching results
  • + {/if} +
    + diff --git a/packages/core/src/lib/select/select-menu.svelte b/packages/core/src/lib/select/select-menu.svelte new file mode 100644 index 00000000..c777e31f --- /dev/null +++ b/packages/core/src/lib/select/select-menu.svelte @@ -0,0 +1,72 @@ + + + + +
    + {#if heading} + + {heading} + + {/if} + + {#if button !== undefined} + + {/if} +
    + + diff --git a/packages/core/src/lib/select/select.svelte b/packages/core/src/lib/select/select.svelte index 7e2f15da..354c767c 100644 --- a/packages/core/src/lib/select/select.svelte +++ b/packages/core/src/lib/select/select.svelte @@ -13,10 +13,19 @@ For selecting from a list of options. --> + + @@ -40,26 +75,28 @@ $: isError = state === 'error'; class={cx( 'peer h-[30px] w-full appearance-none border px-2 py-1.5 text-xs leading-tight outline-none', { - 'border-light bg-white hover:border-gray-6 focus:border-gray-9': - !disabled && !isError, - 'pointer-events-none border-disabled-light bg-disabled-light text-disabled-dark': + 'border-light hover:border-gray-6 focus:border-gray-9 bg-white': + !disabled && !isError && !isWarn, + 'border-disabled-light focus:border-disabled-dark bg-disabled-light text-disabled-dark cursor-not-allowed': disabled, - 'border-light hover:border-medium focus:border-gray-9 ': - !disabled && !isError, - 'border-info-dark focus:outline-[1.5px] focus:-outline-offset-1 focus:outline-info-dark': - isInfo, - 'border-warning-bright focus:outline-[1.5px] focus:-outline-offset-1 focus:outline-warning-bright': + 'border-warning-bright hover:outline-warning-bright focus:outline-warning-bright hover:outline-[1.5px] hover:-outline-offset-1 focus:outline-[1.5px] focus:-outline-offset-1': isWarn, - 'border-danger-dark focus:outline-[1.5px] focus:-outline-offset-1 focus:outline-danger-dark': + 'border-danger-dark hover:outline-danger-dark focus:outline-danger-dark hover:outline-[1.5px hover:-outline-offset-1 focus:outline-[1.5px] focus:-outline-offset-1': isError, } )} {...$$restProps} - on:input + on:input={onInput} + on:mousedown={onMouseDown} + on:keydown={onKeyDown} > - + diff --git a/packages/core/src/lib/select/utils.ts b/packages/core/src/lib/select/utils.ts new file mode 100644 index 00000000..548e6d6e --- /dev/null +++ b/packages/core/src/lib/select/utils.ts @@ -0,0 +1,114 @@ +export interface SearchMatch { + search: string[] | undefined; + option: string; +} + +const addSpecialCharacterEscapes = (value: string) => + // eslint-disable-next-line no-useless-escape + value.replaceAll(/[-[\]{}()*+?.,\\^$|#]/gu, '\\$&').replaceAll(/\s/gu, '\\s'); + +const matchEntries = (entries: string[], searchTerm: string) => { + const results: Record = {}; + const toMatch = addSpecialCharacterEscapes(searchTerm); + const initialCharacterMatch = new RegExp(`^${toMatch}`, 'iu'); + const anyMatch = new RegExp(toMatch, 'giu'); + + for (const datum of entries) { + let index = -1; + const words = datum.split(' '); + + for (const [i, word] of words.entries()) { + if (initialCharacterMatch.test(word)) { + index = 0; + break; + } else if (anyMatch.test(word)) { + index = i + 1; + } + } + + if (results[index] === undefined) { + results[index] = [datum]; + } else { + results[index]!.push(datum); + } + } + + return results; +}; + +const sortMatches = (arr: SearchMatch[]) => + arr.sort((a, b) => { + const aIndex = a.search?.[1] ?? -1; + const bIndex = b.search?.[1] ?? -1; + return aIndex < bIndex ? -1 : 1; + }); + +export type SortOptions = 'default' | 'reduce' | 'off'; + +export const isElementInScrollView = (element: Element) => { + const { top, bottom } = element.getBoundingClientRect(); + const parentRect = + element.parentElement!.parentElement!.getBoundingClientRect(); + + return bottom < parentRect.bottom && top > parentRect.top; +}; + +export const applySearchHighlight = ( + options: string[], + value: string +): SearchMatch[] => { + if (!value) { + return options.map((option) => ({ search: undefined, option })); + } + + const matches = []; + const noMatches = []; + const escaped = addSpecialCharacterEscapes(value); + + for (const option of options) { + const match = option.match(new RegExp(escaped, 'iu')); + + if (match?.index === undefined) { + noMatches.push({ search: undefined, option }); + continue; + } + + const beginning = option.slice(0, match.index); + const middle = option.slice(match.index, match.index + escaped.length); + const end = option.slice(match.index + escaped.length); + matches.push({ search: [beginning, middle, end], option }); + } + + const sorted = sortMatches(matches); + return [...sorted, ...noMatches]; +}; + +export const matchAndSortEntries = ( + entries: string[], + searchTerm: string, + reduce: boolean +) => { + const results = matchEntries(entries, searchTerm); + const finalResults: string[] = []; + + if (reduce) { + for (const key of Object.keys(results)) { + if (Number.parseInt(key, 10) !== -1) { + const sorted = results[key] || []; + finalResults.push(...sorted); + } + } + + return finalResults; + } + + for (const key of Object.keys(results)) { + const sorted = results[key] || []; + finalResults.push(...sorted); + } + + return finalResults; +}; + +export const clickedOutside = (event: Event, inner: Element) => + event.target !== null && !inner.contains(event.target as Node); diff --git a/packages/core/src/routes/+page.svelte b/packages/core/src/routes/+page.svelte index 8d0314e8..2ac94e72 100644 --- a/packages/core/src/routes/+page.svelte +++ b/packages/core/src/routes/+page.svelte @@ -26,13 +26,15 @@ import TableHeaderCell from '$lib/table/table-header-cell.svelte'; import TableHeader from '$lib/table/table-header.svelte'; import TableRow from '$lib/table/table-row.svelte'; import Select from '$lib/select/select.svelte'; +import SearchableSelect from '$lib/select/searchable-select.svelte'; let buttonClickedTimes = 0;
    + +

    Badge

    - +

    Breadcrumbs

    + +

    Button

    +
    - @@ -86,8 +91,10 @@ let buttonClickedTimes = 0; + +

    IconButton

    +
    - +

    Collapse

    +
    + +

    Pill

    +
    -
    -
    - + +

    Input

    +
    -
    - + +

    Text Input

    +
    + +

    Numeric Input

    - -
    + +

    Slider Input

    + +

    Datetime Input

    - -
    + +

    Select

    +
    - - - - - -
    + +

    Searchable Select

    + +
    + + +
    + +
    + + +
    + +
    + + +
    + + +

    Label

    -
    + +

    Tooltip

    - This element has a top tooltip.
    This is the tooltip text!
    @@ -403,8 +457,9 @@ let buttonClickedTimes = 0;
    + +

    Icon

    -

    + +

    Radio

    +
    -
    + +

    Tabs

    -
    + +

    Switch

    -
    - + + +

    Context Menu

    @@ -583,6 +644,7 @@ let buttonClickedTimes = 0; +

    Table

    - - - - + +

    Notify

    Date: Thu, 24 Aug 2023 17:57:28 -0400 Subject: [PATCH 02/21] Break down search/sort logic and additional cleanup. --- packages/core/package.json | 4 +- packages/core/src/lib/select/dom-utils.ts | 10 + packages/core/src/lib/select/search.ts | 197 ++++++++++++++++++ .../src/lib/select/searchable-select.svelte | 70 +++---- .../core/src/lib/select/select-menu.svelte | 16 +- packages/core/src/lib/select/utils.ts | 114 ---------- packages/core/src/routes/+page.svelte | 13 ++ pnpm-lock.yaml | 17 +- 8 files changed, 278 insertions(+), 163 deletions(-) create mode 100644 packages/core/src/lib/select/dom-utils.ts create mode 100644 packages/core/src/lib/select/search.ts delete mode 100644 packages/core/src/lib/select/utils.ts diff --git a/packages/core/package.json b/packages/core/package.json index f10760ad..813dfe43 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -37,7 +37,8 @@ }, "dependencies": { "@mdi/js": "^7.2.96", - "classnames": "^2.3.2" + "classnames": "^2.3.2", + "lodash": "^4.17.21" }, "devDependencies": { "@floating-ui/dom": "^1.5.1", @@ -49,6 +50,7 @@ "@testing-library/jest-dom": "^6.0.0", "@testing-library/svelte": "^4.0.3", "@testing-library/user-event": "^14.4.3", + "@types/lodash": "^4.14.197", "@types/testing-library__jest-dom": "^5.14.9", "@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/parser": "^6.4.0", diff --git a/packages/core/src/lib/select/dom-utils.ts b/packages/core/src/lib/select/dom-utils.ts new file mode 100644 index 00000000..88650507 --- /dev/null +++ b/packages/core/src/lib/select/dom-utils.ts @@ -0,0 +1,10 @@ +export const isOptionInScrollView = (option: Element) => { + const { top, bottom } = option.getBoundingClientRect(); + const parentRect = + option.parentElement!.parentElement!.getBoundingClientRect(); + + return bottom < parentRect.bottom && top > parentRect.top; +}; + +export const clickedOutside = (event: Event, inner: Element) => + event.target !== null && !inner.contains(event.target as Node); diff --git a/packages/core/src/lib/select/search.ts b/packages/core/src/lib/select/search.ts new file mode 100644 index 00000000..a745c60e --- /dev/null +++ b/packages/core/src/lib/select/search.ts @@ -0,0 +1,197 @@ +import escapeRegExp from 'lodash/escapeRegExp'; + +export type SortOptions = 'default' | 'reduce' | 'off'; + +/** + * A breakdown of how a search result should be highlighted. `before` is the + * text preceeding the highlighted portion of the result, `highlight` is the + * part of the result that should be highlighted, and `after` is the text + * succeeding the highlighted portion of the result. + */ +export type SearchHighlight = [ + before: string, + highlight: string, + after: string, +]; + +/** + * A search result with potential matches. + * + * @export + * @interface SearchResult + */ +export interface SearchResult { + /** + * How the option should be highlighted + * + * @type {(SearchHighlight | undefined)} + * @memberof SearchResult + */ + highlight: SearchHighlight | undefined; + + /** + * The select option that had a potential match. + * + * @type {string} + * @memberof SearchResult + */ + option: string; +} + +/** + * A map of search results prioritized by where the match occured in the + * option. + */ +export type SearchMatches = { + [priority: number]: string[]; + 0: string[]; + '-1': string[]; +}; + +/** + * Checks the passed select option for a match with the passed search term, + * and returns the match with a priority based on the result: + * - `0` is any matches on the initial character of a word + * - The index of the word in the option that matched + * - `-1` for non-matches + */ +const getMatchPriority = (option: string, searchTerm: string): number => { + const words = option.split(' '); + + // Match on the initial character of any word in the option + const initialCharacterMatch = new RegExp(`^${searchTerm}`, 'iu'); + const anyMatch = new RegExp(searchTerm, 'giu'); + + for (const [i, word] of words.entries()) { + if (initialCharacterMatch.test(word)) { + // Match on an initial character is highest priority + return 0; + } + + if (anyMatch.test(word)) { + /* + * Matches on other characters are lower priority, so we add 1 to + * prioritize them below matching on an initial character. + */ + return i + 1; + } + } + + // Don't prioritize if there are no matches + return -1; +}; + +/** + * Checks each passed option if it matches with the passed search term, and + * returns a prioritized map of the results. + */ +const prioritizeMatches = (options: string[], searchTerm: string) => { + const results: SearchMatches = { + '0': [], + '-1': [], + }; + + for (const option of options) { + const priority = getMatchPriority(option, searchTerm); + results[priority] ||= []; + results[priority]!.push(option); + } + + return results; +}; + +/** + * Prioritized the passed options based on where a potential match occured + * with the passed search term. If reduce is true, options with no match (-1 + * priority) will not be included in the results. + */ +const getPrioritizedOptions = ( + options: string[], + searchTerm: string, + reduce: boolean +) => { + const prioritized = prioritizeMatches(options, searchTerm); + const results: string[] = []; + + for (const key of Object.keys(prioritized)) { + const priority = Number.parseInt(key, 10); + if (reduce && priority === -1) { + continue; + } + + const sorted = prioritized[priority] ?? []; + results.push(...sorted); + } + + return results; +}; + +/** + * Returns the passed option broken down into segments to apply highlighting + * to the portion of the option that matches the passed search term. + */ +const getSearchHighlight = ( + option: string, + searchTerm: string +): SearchHighlight | undefined => { + const match = option.match(new RegExp(searchTerm, 'iu')); + + if (match === null || match.index === undefined) { + return undefined; + } + + const { index } = match; + const beginning = option.slice(0, index); + const middle = option.slice(index, index + searchTerm.length); + const end = option.slice(index + searchTerm.length); + return [beginning, middle, end]; +}; + +/** + * Sorts the passed results by the `highlight` segment of the result. + */ +const sortByHighlight = (results: SearchResult[]) => + results.sort((a, b) => { + const aIndex = a.highlight?.[1] ?? -1; + const bIndex = b.highlight?.[1] ?? -1; + return aIndex < bIndex ? -1 : 1; + }); + +/** + * Searches against the passed options with the passed search term before + * sorting them and breaking them down into segments to allow for highlighting. + * + * @param options The select options to be searched and sorted + * @param searchTerm The term to match against each of the `options` + * @param reduce If true, will not return options that did not match the search term + * @returns {SearchResult[]} The search results + */ +export const getSearchResults = ( + options: string[], + searchTerm?: string, + reduce: boolean = false +): SearchResult[] => { + if (!searchTerm) { + return options.map((option) => ({ highlight: undefined, option })); + } + + const matches: SearchResult[] = []; + const noMatches = []; + const escaped = escapeRegExp(searchTerm); + const prioritized = getPrioritizedOptions(options, escaped, reduce); + + for (const option of prioritized) { + const highlight = getSearchHighlight(option, escaped); + const result = { highlight, option }; + + if (highlight === undefined) { + noMatches.push(result); + continue; + } + + matches.push(result); + } + + const sorted = sortByHighlight(matches); + return [...sorted, ...noMatches]; +}; diff --git a/packages/core/src/lib/select/searchable-select.svelte b/packages/core/src/lib/select/searchable-select.svelte index 4486c747..4fba5af0 100644 --- a/packages/core/src/lib/select/searchable-select.svelte +++ b/packages/core/src/lib/select/searchable-select.svelte @@ -1,26 +1,24 @@ @@ -36,21 +36,21 @@ export let heading = ''; {#if button !== undefined} {/if}
    diff --git a/packages/core/svelte.config.js b/packages/core/svelte.config.js index 6faff035..a15c2697 100644 --- a/packages/core/svelte.config.js +++ b/packages/core/svelte.config.js @@ -23,6 +23,7 @@ const config = { '../postcss.config.js', '../svelte.config.js', '../tailwind.config.ts', + '../plugins.ts', '../theme.ts', ], }), diff --git a/packages/core/tailwind.config.ts b/packages/core/tailwind.config.ts index c14e7c6f..02fa121f 100644 --- a/packages/core/tailwind.config.ts +++ b/packages/core/tailwind.config.ts @@ -1,8 +1,10 @@ import type { Config } from 'tailwindcss'; import { theme } from './theme'; +import { plugins } from './plugins'; export default { content: ['./src/**/*.{ts,svelte}'], + plugins, theme, variants: { extend: {}, diff --git a/packages/core/theme.ts b/packages/core/theme.ts index 6d5e3184..56403d87 100644 --- a/packages/core/theme.ts +++ b/packages/core/theme.ts @@ -1,3 +1,5 @@ +import type { OptionalConfig } from 'tailwindcss/types/config'; + export const theme = { zIndex: { max: '1000', @@ -75,4 +77,4 @@ export const theme = { 'expand-vertical': 'max-height,visibility', }, }, -}; +} satisfies OptionalConfig['theme']; From 041c940ea12f5d3551959095c68743ba5d15134a Mon Sep 17 00:00:00 2001 From: DTCurrie Date: Mon, 28 Aug 2023 15:59:46 -0400 Subject: [PATCH 15/21] Additional cleanup --- packages/core/src/lib/input/numeric-input.svelte | 8 +------- packages/core/src/lib/pill.svelte | 13 +++++++------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/core/src/lib/input/numeric-input.svelte b/packages/core/src/lib/input/numeric-input.svelte index aa20cd6d..a9b55219 100644 --- a/packages/core/src/lib/input/numeric-input.svelte +++ b/packages/core/src/lib/input/numeric-input.svelte @@ -16,13 +16,7 @@ import type { NumericInputTypes } from './utils'; /** The input type */ export let type: NumericInputTypes | undefined = 'number'; -/** - * The value of the input, if any. - * - * TODO: Discuss disabling these rules for svelte components, otherwise - * these props are treatef as required and force users to add value={undefined} - * when no initial value is set. - */ +/** The value of the input, if any. */ export let value: number | undefined = undefined; /** The amount to increment/decrement when using the up/down arrows. */ diff --git a/packages/core/src/lib/pill.svelte b/packages/core/src/lib/pill.svelte index 6ed1df6e..70610c15 100644 --- a/packages/core/src/lib/pill.svelte +++ b/packages/core/src/lib/pill.svelte @@ -10,6 +10,7 @@ For displaying a list of items. -
    diff --git a/packages/core/src/vitest.setup.ts b/packages/core/src/vitest.setup.ts index b2e8e651..8dc2bfce 100644 --- a/packages/core/src/vitest.setup.ts +++ b/packages/core/src/vitest.setup.ts @@ -4,6 +4,14 @@ import matchers from '@testing-library/jest-dom/matchers'; expect.extend(matchers); +/** + * `Element.scrollIntoView` is not implemented/stubbed in `jsdom` so we stub it + * out here: + * + * https://github.com/jsdom/jsdom/issues/1695 + */ +Element.prototype.scrollIntoView = () => {}; + /** * `PointerEvent` does not exist in `jsdom` so this polyfill is based off this * comment on the PR to add it: From 5ff4228db5bad544143e89840ac7f38c3190718a Mon Sep 17 00:00:00 2001 From: DTCurrie Date: Tue, 29 Aug 2023 10:36:01 -0400 Subject: [PATCH 17/21] Add stories and cleanup --- packages/core/src/lib/index.ts | 8 +- .../src/lib/select/searchable-select.svelte | 7 +- .../select/labeled-searchable-select.svelte | 30 +++++ .../select/searchable-select.stories.mdx | 109 ++++++++++++++++++ .../src/stories/select/select.stories.mdx | 4 +- 5 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 packages/storybook/src/stories/select/labeled-searchable-select.svelte create mode 100644 packages/storybook/src/stories/select/searchable-select.stories.mdx diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index 646cc719..7843a315 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -11,6 +11,7 @@ export { default as Pill } from './pill.svelte'; export { default as Switch } from './switch.svelte'; export { default as Radio } from './radio.svelte'; export { default as Tabs } from './tabs.svelte'; +export { useUniqueId } from './unique-id'; export { default as Tooltip, @@ -34,12 +35,13 @@ export { type TextInputTypes, } from './input/text-input.svelte'; +export { default as Select, type SelectState } from './select/select.svelte'; +export { default as SearchableSelect } from './select/searchable-select.svelte'; +export { type SortOptions } from './select/search'; + export { default as Table, type TableVariant } from './table/table.svelte'; export { default as TableHeader } from './table/table-header.svelte'; export { default as TableHeaderCell } from './table/table-header-cell.svelte'; export { default as TableBody } from './table/table-body.svelte'; export { default as TableRow } from './table/table-row.svelte'; export { default as TableCell } from './table/table-cell.svelte'; -export { default as Select, type SelectState } from './select/select.svelte'; - -export * from './unique-id'; diff --git a/packages/core/src/lib/select/searchable-select.svelte b/packages/core/src/lib/select/searchable-select.svelte index f6036431..b8c75d8d 100644 --- a/packages/core/src/lib/select/searchable-select.svelte +++ b/packages/core/src/lib/select/searchable-select.svelte @@ -19,8 +19,8 @@ export let options: string[] = []; export let value: string | undefined = undefined; export let disabled = false; export let state: SelectState = 'none'; -export let button: { text: string; icon: string } | undefined = undefined; export let sort: SortOptions = 'default'; +export let button: { text: string; icon: string } | undefined = undefined; export let heading = ''; const dispatch = createEventDispatcher<{ @@ -31,8 +31,6 @@ const dispatch = createEventDispatcher<{ }>(); const menuId = useUniqueId('searchable-select'); - -let wrapper: Element; let menu: HTMLUListElement; let open = false; @@ -166,7 +164,6 @@ $: {
    @@ -175,7 +172,7 @@ $: { bind:value role="combobox" aria-controls={menuId} - aria-expanded={open ? true : undefined} + aria-expanded={open} readonly={disabled ? true : undefined} aria-disabled={disabled ? true : undefined} type="text" diff --git a/packages/storybook/src/stories/select/labeled-searchable-select.svelte b/packages/storybook/src/stories/select/labeled-searchable-select.svelte new file mode 100644 index 00000000..95cedc8b --- /dev/null +++ b/packages/storybook/src/stories/select/labeled-searchable-select.svelte @@ -0,0 +1,30 @@ + + + diff --git a/packages/storybook/src/stories/select/searchable-select.stories.mdx b/packages/storybook/src/stories/select/searchable-select.stories.mdx new file mode 100644 index 00000000..c2f82341 --- /dev/null +++ b/packages/storybook/src/stories/select/searchable-select.stories.mdx @@ -0,0 +1,109 @@ +import { Canvas, Meta, Story } from '@storybook/addon-docs'; +import { SearchableSelect } from '@viamrobotics/prime-core'; +import LabeledSelect from './labeled-searchable-select.svelte'; + + + +# Searchable Select + +A simple user input for selecting from a list of options. This is an implementation +of the native HTML `` with our styles applied. ## Labels For accessibility, consider wrapping your `