diff --git a/docs/src/pages/components/AutoComplete.svx b/docs/src/pages/components/AutoComplete.svx index 07ec7a33..d8d356b8 100644 --- a/docs/src/pages/components/AutoComplete.svx +++ b/docs/src/pages/components/AutoComplete.svx @@ -5,6 +5,31 @@ components: ["AutoComplete", "AutoCompleteSkeleton"] `AutoComplete` is keyed for performance reasons. @@ -15,10 +40,7 @@ components: ["AutoComplete", "AutoCompleteSkeleton"] ### Default - + ### Custom slot @@ -28,74 +50,38 @@ Override the default slot to customize the display of each item. Access the item ### Hidden label - - -### Format item display text - -Use the `itemToString` prop to format the display of individual items. - - { - return item.text + ' (' + item.id +')' -}} titleText="Contact" selectedId="0" items="{[{id: "0", text: "Slack"}, - {id: "1", text: "Email"}, - {id: "2", text: "Fax"}]}" /> - -### Multiple dropdowns - - + ### Top direction Set `direction` to `"top"` for the dropdown menu to appear above the input. - + ### Light variant - - -### Inline variant - - + ### Extra-large size - + ### Small size - + ### Invalid state - + ### Warning state - + ### Disabled state - + ### Skeleton - \ No newline at end of file + diff --git a/docs/src/pages/components/ComboBox.svx b/docs/src/pages/components/ComboBox.svx index 4b4e20f1..bc8f51a0 100644 --- a/docs/src/pages/components/ComboBox.svx +++ b/docs/src/pages/components/ComboBox.svx @@ -18,6 +18,15 @@ items={[ {id: "2", text: "Fax"} ]} /> +### `shouldFilterItem` + + item.text.startsWith(value)} titleText="Contact" placeholder="Select contact method" +items={[ + {id: "0", text: "Slack"}, + {id: "1", text: "Email"}, + {id: "2", text: "Fax"} + ]} /> + ### Custom slot Override the default slot to customize the display of each item. Access the item and index through the `let:` directive. diff --git a/docs/src/pages/components/Editor.svx b/docs/src/pages/components/Editor.svx new file mode 100644 index 00000000..c82e3c88 --- /dev/null +++ b/docs/src/pages/components/Editor.svx @@ -0,0 +1,42 @@ +--- +components: ["Editor", "EditorSkeleton"] +--- + + + +`Editor` is keyed for performance reasons. + + +
html attribute contains pure html.
+
+ +### Default + + + +### Extra-large size + + + +### Small size + + + +### Invalid state + + + +### Warning state + + + +### Disabled state + + + +### Skeleton + + \ No newline at end of file diff --git a/docs/src/pages/framed/AutoComplete/AutoCompleteSlot.svelte b/docs/src/pages/framed/AutoComplete/AutoCompleteSlot.svelte index fa4edf0f..e7b92570 100644 --- a/docs/src/pages/framed/AutoComplete/AutoCompleteSlot.svelte +++ b/docs/src/pages/framed/AutoComplete/AutoCompleteSlot.svelte @@ -1,15 +1,38 @@ diff --git a/docs/src/pages/framed/AutoComplete/MultipleAutoComplete.svelte b/docs/src/pages/framed/AutoComplete/MultipleAutoComplete.svelte deleted file mode 100644 index 8a7473e5..00000000 --- a/docs/src/pages/framed/AutoComplete/MultipleAutoComplete.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - - - -
Primary: {primary}
- - - -
Secondary: {secondary}
- - diff --git a/src/AutoComplete/AutoComplete.svelte b/src/AutoComplete/AutoComplete.svelte index 1b07701b..ae578655 100644 --- a/src/AutoComplete/AutoComplete.svelte +++ b/src/AutoComplete/AutoComplete.svelte @@ -7,12 +7,6 @@ * @slot {{ item: AutoCompleteItem; index: number; }} */ - /** - * Set the full list of items - * @type {AutoCompleteItem[]} - */ - export let items = []; - /** * Override the display of a dropdown item * @type {(item: AutoCompleteItem) => string} @@ -32,10 +26,10 @@ export let selectedItem = undefined; /** - * Specify the type of dropdown - * @type {"default" | "inline"} + * Determine if an item should be filtered given the current combobox value + * @type {(value: string) => {}} */ - export let type = "default"; + export let shouldFilterItem = () => {}; /** * Specify the direction of the dropdown menu @@ -52,9 +46,6 @@ /** Set to `true` to open the dropdown */ export let open = false; - /** Set to `true` to use the inline variant */ - export let inline = false; - /** Set to `true` to enable the light variant */ export let light = false; @@ -103,80 +94,107 @@ /** Specify the placeholder text */ export let placeholder = null; - import { createEventDispatcher } from "svelte"; + /** + * Obtain a reference to the list HTML element + * @type {null | HTMLDivElement} + */ + export let listRef = null; + + import { createEventDispatcher, afterUpdate, tick } from "svelte"; + import Checkmark from "../icons/Checkmark.svelte"; import WarningFilled from "../icons/WarningFilled.svelte"; import WarningAltFilled from "../icons/WarningAltFilled.svelte"; - import { ListBox, ListBoxMenu, ListBoxMenuItem } from "../ListBox"; + import ListBox from "../ListBox/ListBox.svelte"; + import ListBoxField from "../ListBox/ListBoxField.svelte"; + import ListBoxMenu from "../ListBox/ListBoxMenu.svelte"; + import ListBoxMenuIcon from "../ListBox/ListBoxMenuIcon.svelte"; + import ListBoxMenuItem from "../ListBox/ListBoxMenuItem.svelte"; + import ListBoxSelection from "../ListBox/ListBoxSelection.svelte"; const dispatch = createEventDispatcher(); - let filteredItems = []; - + export let filteredItems = []; + let inputValue = ""; //value; + let prevSelectedId = null; let highlightedIndex = -1; - let innerValue = undefined; - function change(dir) { let index = highlightedIndex + dir; - if (index < 0) { index = filteredItems.length - 1; } else if (index >= filteredItems.length) { index = 0; } - highlightedIndex = index; } - function onKeydown(event) { - let key = event.key; - - if (["Enter", "ArrowDown", "ArrowUp"].includes(key)) { - event.preventDefault(); - } - - if (key === "Enter") { - open = !open; - if ( - highlightedIndex > -1 && - filteredItems[highlightedIndex].id !== selectedId - ) { - selectedItem = filteredItems[highlightedIndex]; - selectedId = selectedItem.id; - innerValue = selectedItem.text; - open = false; - } - } else if (key === "Backspace") { - selectedItem = undefined; - selectedId = undefined; - open = innerValue.length > 0 && filteredItems.length > 0; - } else if (key === "Tab") { - open = false; - ref.blur(); - } else if (key === "ArrowDown") { - change(1); - } else if (key === "ArrowUp") { - change(-1); - } else if (key === "Escape") { - innerValue = ""; - dispatch("clear"); - open = false; - } else { - if (!open) open = filteredItems.length > 0; - } + /** + * Clear the combo box programmatically + * @type {(options?: { focus?: boolean; }) => void} + */ + export function clear(options = {}) { + prevSelectedId = null; + highlightedIndex = -1; + highlightedId = undefined; + selectedId = undefined; + selectedItem = undefined; + open = false; + inputValue = ""; + if (options?.focus !== false) ref?.focus(); } + afterUpdate(() => { + if (open) { + ref.focus(); + //filteredItems = shouldFilterItem(value); + } else { + highlightedIndex = -1; + filteredItems = []; + if (!selectedItem) { + selectedId = undefined; + inputValue = ""; + highlightedIndex = -1; + highlightedId = undefined; + } else { + // programmatically set inputValue + inputValue = selectedItem.text; + } + } + }); + $: if (selectedId !== undefined) { - dispatch("select", { selectedId, selectedItem }); + if (prevSelectedId !== selectedId) { + prevSelectedId = selectedId; + if (filteredItems?.length === 1 && open) { + selectedId = filteredItems[0].id; + selectedItem = filteredItems[0]; + highlightedIndex = -1; + highlightedId = undefined; + } else { + selectedItem = + filteredItems?.length > 0 + ? filteredItems.find((item) => item.id === selectedId) + : null; + } + dispatch("select", { selectedId, selectedItem }); + } + } else { + prevSelectedId = selectedId; + selectedItem = undefined; } - $: filteredItems = items.filter( - (item) => innerValue?.length > 0 && item.text.startsWith(innerValue) - ); - $: inline = type === "inline"; - $: if (!open) { - highlightedIndex = -1; + $: ariaLabel = $$props["aria-label"] || "Choose an item"; + $: menuId = `menu-${id}`; + $: comboId = `combo-${id}`; + $: highlightedId = filteredItems[highlightedIndex] + ? filteredItems[highlightedIndex].id + : 0; + $: if (inputValue) { + shouldFilterItem(inputValue); + } else { + filteredItems = []; } + //$: value = inputValue; -
- {#if titleText} +
+ {#if titleText && !hideLabel} {/if} - {#if invalid} - - {/if} - {#if !invalid && warn} - - {/if} - + > + + {#if invalid} + + {/if} + {#if !invalid && warn} + + {/if} + {#if inputValue} + + {/if} + + {#if open} - + {#each filteredItems as item, i (item.id)} {itemToString(item)} + {#if selectedItem && selectedItem.id === item.id} + + {/if} {/each} {/if} - {#if !inline && !invalid && !warn && helperText} + {#if !invalid && helperText && !warn}
{/if}
- - diff --git a/src/Editor/Editor.svelte b/src/Editor/Editor.svelte new file mode 100644 index 00000000..1ab22c10 --- /dev/null +++ b/src/Editor/Editor.svelte @@ -0,0 +1,114 @@ + + +{#if titleText} + +{/if} +{#if disabled} + {@html html} +{:else} +
+{/if} +{#if invalid} +
{invalidText}
+{/if} +{#if !invalid && warn} +
{warnText}
+{/if} +{#if !invalid && !warn && helperText} +
+ {helperText} +
+{/if} +
{invalidText}
+ + diff --git a/src/Editor/EditorSkeleton.svelte b/src/Editor/EditorSkeleton.svelte new file mode 100644 index 00000000..76588c07 --- /dev/null +++ b/src/Editor/EditorSkeleton.svelte @@ -0,0 +1,19 @@ + + + +
+ {#if !hideLabel} + + {/if} +
+
diff --git a/src/Editor/index.js b/src/Editor/index.js new file mode 100644 index 00000000..4b448254 --- /dev/null +++ b/src/Editor/index.js @@ -0,0 +1,3 @@ +export { default as Editor } from "./Editor.svelte"; +export { default as EditorSkeleton } from "./EditorSkeleton.svelte"; +export { quill } from "./quill"; diff --git a/src/Editor/quill.js b/src/Editor/quill.js new file mode 100644 index 00000000..140a84c6 --- /dev/null +++ b/src/Editor/quill.js @@ -0,0 +1,32 @@ +// https://github.com/lagden/svelte-editor-quill + +import Quill from "quill"; + +export function quill(node, params) { + const quill = new Quill(node, { + ...params.options, + }); + + if (params.html) { + const delta = quill.clipboard.convert(params.html); + quill.setContents(delta, "silent"); + } + + const onTextChange = () => { + const customEvent = new CustomEvent("text-change", { + detail: { + html: quill.root.innerHTML, + text: quill.getText(), + }, + }); + node.dispatchEvent(customEvent); + }; + + quill.on("text-change", onTextChange); + + return { + destroy() { + quill.off("text-change", onTextChange); + }, + }; +} diff --git a/types/AutoComplete/AutoComplete.svelte.d.ts b/types/AutoComplete/AutoComplete.svelte.d.ts index c1a147fc..48e70934 100644 --- a/types/AutoComplete/AutoComplete.svelte.d.ts +++ b/types/AutoComplete/AutoComplete.svelte.d.ts @@ -11,13 +11,7 @@ export interface AutoCompleteItem { } export interface AutoCompleteProps - extends svelte.JSX.HTMLAttributes { - /** - * Set the full list of items - * @default [] - */ - items?: AutoCompleteItem[]; - + extends svelte.JSX.HTMLAttributes { /** * Override the display of a dropdown item * @default (item) => item.text || item.id @@ -37,10 +31,10 @@ export interface AutoCompleteProps selectedItem?: AutoCompleteItem; /** - * Specify the type of dropdown - * @default "default" + * Determine if an item should be filtered given the current combobox value + * @default () => [] */ - type?: "default" | "inline"; + shouldFilterItem?: (value: string) => AutoCompleteItem[]; /** * Specify the direction of the dropdown menu @@ -60,12 +54,6 @@ export interface AutoCompleteProps */ open?: boolean; - /** - * Set to `true` to use the inline variant - * @default false - */ - inline?: boolean; - /** * Set to `true` to enable the light variant * @default false @@ -149,6 +137,12 @@ export interface AutoCompleteProps * @default null */ placeholder?: undefined; + + /** + * Obtain a reference to the list HTML element + * @default null + */ + listRef?: null | HTMLDivElement; } export default class AutoComplete extends SvelteComponentTyped< @@ -158,11 +152,17 @@ export default class AutoComplete extends SvelteComponentTyped< selectedId: AutoCompleteItemId; selectedItem: AutoCompleteItem; }>; - change: WindowEventMap["change"]; + keydown: WindowEventMap["keydown"]; + keyup: WindowEventMap["keyup"]; focus: WindowEventMap["focus"]; blur: WindowEventMap["blur"]; - input: WindowEventMap["input"]; - clear: CustomEvent; + clear: WindowEventMap["clear"]; + scroll: WindowEventMap["scroll"]; }, { default: { item: AutoCompleteItem; index: number } } -> {} +> { + /** + * Clear the combo box programmatically + */ + clear: (options?: { focus?: boolean }) => void; +} diff --git a/types/Editor/Editor.svelte.d.ts b/types/Editor/Editor.svelte.d.ts new file mode 100644 index 00000000..3852fd79 --- /dev/null +++ b/types/Editor/Editor.svelte.d.ts @@ -0,0 +1,83 @@ +/// +import type { SvelteComponentTyped } from "svelte"; + +export interface EditorProps { + /** + * Set to `true` to disable the dropdown + * @default false + */ + disabled?: boolean; + + /** + * Specify the title text + * @default "" + */ + titleText?: string; + + /** + * Set to `true` to indicate an invalid state + * @default false + */ + invalid?: boolean; + + /** + * Specify the invalid state text + * @default "" + */ + invalidText?: string; + + /** + * Set to `true` to indicate an warning state + * @default false + */ + warn?: boolean; + + /** + * Specify the warning state text + * @default "" + */ + warnText?: string; + + /** + * Specify the helper text + * @default "" + */ + helperText?: string; + + /** + * Set to `true` to visually hide the label text + * @default false + */ + hideLabel?: boolean; + + /** + * Set an id for the list box component + * @default "ccs-" + Math.random().toString(36) + */ + id?: string; + + /** + * Specify the placeholder text + * @default null + */ + placeholder?: undefined; + + /** + * Specify the html + * @default undefined + */ + html?: string; + + /** + * Specify the text + * @default undefined + */ + text?: string; + + /** + * @default { modules: { toolbar }, theme: 'snow', placeholder } + */ + options?: { modules: { toolbar }; theme: "snow"; placeholder }; +} + +export default class Editor extends SvelteComponentTyped {} diff --git a/types/Editor/EditorSkeleton.svelte.d.ts b/types/Editor/EditorSkeleton.svelte.d.ts new file mode 100644 index 00000000..692f83ff --- /dev/null +++ b/types/Editor/EditorSkeleton.svelte.d.ts @@ -0,0 +1,22 @@ +/// +import type { SvelteComponentTyped } from "svelte"; + +export interface EditorSkeletonProps + extends svelte.JSX.HTMLAttributes { + /** + * Set to `true` to hide the label text + * @default false + */ + hideLabel?: boolean; +} + +export default class EditorSkeleton extends SvelteComponentTyped< + EditorSkeletonProps, + { + click: WindowEventMap["click"]; + mouseover: WindowEventMap["mouseover"]; + mouseenter: WindowEventMap["mouseenter"]; + mouseleave: WindowEventMap["mouseleave"]; + }, + {} +> {}