@melt-ui/svelte
Advanced tools
Comparing version 0.38.1 to 0.39.0
/// <reference types="svelte" /> | ||
import type { MeltActionReturn } from '../../internal/types.js'; | ||
import { type Writable } from 'svelte/store'; | ||
import type { ComboboxItemProps, CreateComboboxProps } from './types.js'; | ||
import type { MeltActionReturn } from '../../internal/types.js'; | ||
import type { ComboboxEvents } from './events.js'; | ||
import type { ComboboxItemProps, ComboboxOption, CreateComboboxProps } from './types.js'; | ||
export declare const INTERACTION_KEYS: string[]; | ||
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
export declare function createCombobox<ItemValue>(props?: CreateComboboxProps<ItemValue>): { | ||
export declare function createCombobox<Value>(props?: CreateComboboxProps<Value>): { | ||
elements: { | ||
@@ -47,7 +47,7 @@ input: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[{ | ||
}, string>; | ||
item: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[{ | ||
update: (updater: import("svelte/store").Updater<ItemValue | undefined>, sideEffect?: ((newValue: ItemValue | undefined) => void) | undefined) => void; | ||
set: (this: void, value: ItemValue | undefined) => void; | ||
subscribe(this: void, run: import("svelte/store").Subscriber<ItemValue | undefined>, invalidate?: import("svelte/store").Invalidator<ItemValue | undefined> | undefined): import("svelte/store").Unsubscriber; | ||
}, Writable<import("./types.js").ComboboxFilterFunction<ItemValue>>, { | ||
option: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[{ | ||
update: (updater: import("svelte/store").Updater<ComboboxOption<Value> | undefined>, sideEffect?: ((newValue: ComboboxOption<Value> | undefined) => void) | undefined) => void; | ||
set: (this: void, value: ComboboxOption<Value> | undefined) => void; | ||
subscribe(this: void, run: import("svelte/store").Subscriber<ComboboxOption<Value> | undefined>, invalidate?: import("svelte/store").Invalidator<ComboboxOption<Value> | undefined> | undefined): import("svelte/store").Unsubscriber; | ||
}, Writable<import("./types.js").ComboboxFilterFunction<Value>>, { | ||
set: (value: string) => void; | ||
@@ -76,3 +76,3 @@ update: (fn: (value: string) => string) => void; | ||
}> | undefined): import("svelte/store").Unsubscriber; | ||
}], (node: HTMLElement) => MeltActionReturn<ComboboxEvents['item']>, ([$value, $filterFunction, $inputValue, $touchedInput]: [ItemValue | undefined, import("./types.js").ComboboxFilterFunction<ItemValue>, { | ||
}], (node: HTMLElement) => MeltActionReturn<ComboboxEvents['item']>, ([$value, $filterFunction, $inputValue, $touchedInput]: [ComboboxOption<Value> | undefined, import("./types.js").ComboboxFilterFunction<Value>, { | ||
value: string; | ||
@@ -83,3 +83,3 @@ debounced: string; | ||
debounced: boolean; | ||
}]) => (props: ComboboxItemProps<ItemValue>) => { | ||
}]) => (props: ComboboxItemProps<Value>) => { | ||
readonly 'data-value': string; | ||
@@ -114,6 +114,6 @@ readonly 'data-label': string | undefined; | ||
}; | ||
value: { | ||
update: (updater: import("svelte/store").Updater<ItemValue | undefined>, sideEffect?: ((newValue: ItemValue | undefined) => void) | undefined) => void; | ||
set: (this: void, value: ItemValue | undefined) => void; | ||
subscribe(this: void, run: import("svelte/store").Subscriber<ItemValue | undefined>, invalidate?: import("svelte/store").Invalidator<ItemValue | undefined> | undefined): import("svelte/store").Unsubscriber; | ||
selected: { | ||
update: (updater: import("svelte/store").Updater<ComboboxOption<Value> | undefined>, sideEffect?: ((newValue: ComboboxOption<Value> | undefined) => void) | undefined) => void; | ||
set: (this: void, value: ComboboxOption<Value> | undefined) => void; | ||
subscribe(this: void, run: import("svelte/store").Subscriber<ComboboxOption<Value> | undefined>, invalidate?: import("svelte/store").Invalidator<ComboboxOption<Value> | undefined> | undefined): import("svelte/store").Unsubscriber; | ||
}; | ||
@@ -127,9 +127,6 @@ inputValue: import("svelte/store").Readable<{ | ||
helpers: { | ||
isSelected: import("svelte/store").Readable<(item: ItemValue) => boolean>; | ||
isSelected: import("svelte/store").Readable<(item: Value) => boolean>; | ||
}; | ||
options: { | ||
forceVisible: Writable<boolean>; | ||
value?: Writable<Writable<ItemValue | undefined> | undefined> | undefined; | ||
onValueChange?: Writable<import("../../internal/helpers/index.js").ChangeFn<ItemValue | undefined> | undefined> | undefined; | ||
defaultValue?: Writable<ItemValue | undefined> | undefined; | ||
onOpenChange?: Writable<import("../../internal/helpers/index.js").ChangeFn<boolean> | undefined> | undefined; | ||
@@ -143,5 +140,7 @@ preventScroll: Writable<boolean>; | ||
scrollAlignment: Writable<"center" | "nearest">; | ||
filterFunction: Writable<import("./types.js").ComboboxFilterFunction<ItemValue>>; | ||
filterFunction: Writable<import("./types.js").ComboboxFilterFunction<Value>>; | ||
loop: Writable<boolean>; | ||
defaultInputValue?: Writable<string | undefined> | undefined; | ||
defaultSelected?: Writable<ComboboxOption<Value> | undefined> | undefined; | ||
selected?: Writable<Writable<ComboboxOption<Value> | undefined> | undefined> | undefined; | ||
onSelectedChange?: Writable<import("../../internal/helpers/index.js").ChangeFn<ComboboxOption<Value> | undefined> | undefined> | undefined; | ||
closeOnOutsideClick: Writable<boolean>; | ||
@@ -148,0 +147,0 @@ closeOnEscape: Writable<boolean>; |
import { useEscapeKeydown, usePopper } from '../../internal/actions/index.js'; | ||
import { back, builder, createElHelpers, effect, executeCallbacks, FIRST_LAST_KEYS, forward, generateId, isBrowser, isElementDisabled, isHTMLElement, isHTMLInputElement, kbd, last, next, noop, overridable, prev, removeScroll, sleep, styleToString, toWritableStores, addHighlight, removeHighlight, omit, getOptions, derivedVisible, addMeltEventListener, getPortalDestination, } from '../../internal/helpers/index.js'; | ||
import { FIRST_LAST_KEYS, addHighlight, addMeltEventListener, back, builder, createElHelpers, derivedVisible, effect, executeCallbacks, forward, generateId, getOptions, getPortalDestination, isBrowser, isElementDisabled, isHTMLElement, isHTMLInputElement, kbd, last, next, noop, omit, overridable, prev, removeHighlight, removeScroll, sleep, styleToString, toWritableStores, } from '../../internal/helpers/index.js'; | ||
import { debounceable } from '../../internal/helpers/store/debounceable.js'; | ||
import deepEqual from 'deep-equal'; | ||
import { onMount, tick } from 'svelte'; | ||
import { derived, get, readonly, writable } from 'svelte/store'; | ||
import { createLabel } from '../label/create.js'; | ||
import { debounceable } from '../../internal/helpers/store/debounceable.js'; | ||
import deepEqual from 'deep-equal'; | ||
// prettier-ignore | ||
@@ -40,12 +40,11 @@ export const INTERACTION_KEYS = [kbd.ARROW_LEFT, kbd.ESCAPE, kbd.ARROW_RIGHT, kbd.SHIFT, kbd.CAPS_LOCK, kbd.CONTROL, kbd.ALT, kbd.META, kbd.ENTER, kbd.F1, kbd.F2, kbd.F3, kbd.F4, kbd.F5, kbd.F6, kbd.F7, kbd.F8, kbd.F9, kbd.F10, kbd.F11, kbd.F12]; | ||
const highlightedItem = writable(null); | ||
const valueWritable = withDefaults.value ?? writable(withDefaults.defaultValue); | ||
const value = overridable(valueWritable, withDefaults?.onValueChange); | ||
const selectedWritable = withDefaults.selected ?? | ||
writable(withDefaults.defaultSelected); | ||
const selected = overridable(selectedWritable, withDefaults?.onSelectedChange); | ||
// The current value of the input element. | ||
const inputValue = debounceable(withDefaults.defaultInputValue ?? '', withDefaults.debounce); | ||
const inputValue = debounceable(withDefaults.defaultSelected?.label ?? '', withDefaults.debounce); | ||
// Either the provided open store or a store with the default open value | ||
const openWritable = withDefaults.open ?? writable(true); | ||
const openWritable = withDefaults.open ?? writable(false); | ||
// The overridable open store which is the source of truth for the open state. | ||
const open = overridable(openWritable, withDefaults?.onOpenChange); | ||
// Open so we can register the optionsList items before mounted = true | ||
open.set(true); | ||
const isEmpty = writable(false); | ||
@@ -63,3 +62,3 @@ const options = toWritableStores(omit(withDefaults, 'open', 'defaultOpen', 'debounce')); | ||
/** ------- */ | ||
function getItemProps(el) { | ||
function getOptionProps(el) { | ||
const value = el.getAttribute('data-value'); | ||
@@ -74,21 +73,5 @@ const label = el.getAttribute('data-label'); | ||
} | ||
const cachedItemPropsArr = []; | ||
function getItemPropsArr() { | ||
if (!isBrowser) | ||
return cachedItemPropsArr; | ||
const menuElement = document.getElementById(ids.menu); | ||
if (!isHTMLElement(menuElement)) | ||
return cachedItemPropsArr; | ||
const items = getOptions(menuElement); | ||
return items.map(getItemProps); | ||
} | ||
function getSelectedLabel() { | ||
const $value = get(value); | ||
const itemPropsArr = getItemPropsArr(); | ||
const selectedItem = itemPropsArr.find((item) => deepEqual(item.value, $value)); | ||
return selectedItem?.label; | ||
} | ||
/** Resets the combobox inputValue and filteredItems back to the selectedItem */ | ||
function reset() { | ||
const $selectedItem = get(value); | ||
const $selectedItem = get(selected); | ||
// If no item is selected the input should be cleared and the filter reset. | ||
@@ -99,3 +82,3 @@ if (!$selectedItem) { | ||
else { | ||
inputValue.forceSet(getSelectedLabel() ?? ''); | ||
inputValue.forceSet(get(selected)?.label ?? ''); | ||
} | ||
@@ -109,5 +92,4 @@ touchedInput.forceSet(false); | ||
function selectItem(item) { | ||
if (!item.dataset.value) | ||
return; | ||
value.set(JSON.parse(item.dataset.value)); | ||
const props = getOptionProps(item); | ||
selected.set(props); | ||
const activeTrigger = document.getElementById(ids.input); | ||
@@ -182,3 +164,3 @@ if (activeTrigger) { | ||
*/ | ||
const isSelected = derived([value], ([$value]) => { | ||
const isSelected = derived([selected], ([$value]) => { | ||
return (item) => $value === item; | ||
@@ -235,3 +217,3 @@ }); | ||
tick().then(() => { | ||
const $selectedItem = get(value); | ||
const $selectedItem = get(selected); | ||
if ($selectedItem) | ||
@@ -454,6 +436,5 @@ return; | ||
}); | ||
const item = builder(name('item'), { | ||
stores: [value, filterFunction, inputValue, touchedInput], | ||
const option = builder(name('option'), { | ||
stores: [selected, filterFunction, inputValue, touchedInput], | ||
returned: ([$value, $filterFunction, $inputValue, $touchedInput]) => (props) => { | ||
cachedItemPropsArr.push(props); | ||
let hidden = false; | ||
@@ -523,10 +504,4 @@ if ($touchedInput.debounced && | ||
}); | ||
effect(value, function setInputValue($value) { | ||
if (!$value) { | ||
inputValue.set(''); | ||
return; | ||
} | ||
tick().then(() => { | ||
inputValue.set(getSelectedLabel() ?? ''); | ||
}); | ||
effect(selected, function setInputValue($selected) { | ||
inputValue.set($selected?.label ?? ''); | ||
}); | ||
@@ -561,3 +536,3 @@ /** | ||
input, | ||
item, | ||
option, | ||
menu, | ||
@@ -568,3 +543,3 @@ label, | ||
open, | ||
value, | ||
selected, | ||
inputValue: readonly(inputValue), | ||
@@ -571,0 +546,0 @@ isEmpty: readonly(isEmpty), |
@@ -7,3 +7,7 @@ import type { BuilderReturn } from '../../internal/types.js'; | ||
export type { ComboboxComponentEvents } from './events.js'; | ||
export type CreateComboboxProps<ItemValue> = { | ||
export type ComboboxOption<Value> = { | ||
value: Value; | ||
label?: string; | ||
}; | ||
export type CreateComboboxProps<Value> = { | ||
/** | ||
@@ -29,3 +33,3 @@ * Options for positioning the popover menu. | ||
*/ | ||
filterFunction?: ComboboxFilterFunction<ItemValue>; | ||
filterFunction?: ComboboxFilterFunction<Value>; | ||
/** | ||
@@ -58,26 +62,20 @@ * Whether or not the combobox should loop through the list when | ||
/** | ||
* The default value set on the select input. | ||
* The default selected option. | ||
* | ||
* This will be overridden if you also pass a `value` store prop. | ||
* This will be overridden if you also pass a `selected` store prop. | ||
* | ||
* @default undefined | ||
*/ | ||
defaultValue?: ItemValue; | ||
defaultSelected?: ComboboxOption<Value>; | ||
/** | ||
* An optional controlled store that manages the value state of the combobox. | ||
* An optional controlled store that manages the selected option of the combobox. | ||
*/ | ||
value?: Writable<ItemValue | undefined>; | ||
selected?: Writable<ComboboxOption<Value> | undefined>; | ||
/** | ||
* A change handler for the value store called when the value would normally change. | ||
* A change handler for the selected store called when the selected would normally change. | ||
* | ||
* @see https://melt-ui.com/docs/controlled#change-functions | ||
*/ | ||
onValueChange?: ChangeFn<ItemValue | undefined>; | ||
onSelectedChange?: ChangeFn<ComboboxOption<Value> | undefined>; | ||
/** | ||
* The default value for inputValue. | ||
* | ||
* @default undefined | ||
*/ | ||
defaultInputValue?: string; | ||
/** | ||
* Whether or not to close the combobox menu when the user clicks | ||
@@ -128,10 +126,4 @@ * outside of the combobox. | ||
export type ComboboxFilterFunction<T> = (args: ComboboxFilterFunctionArgs<T>) => boolean; | ||
export type ComboboxItemProps<T> = { | ||
value: T; | ||
export type ComboboxItemProps<Value> = ComboboxOption<Value> & { | ||
/** | ||
* By default, the textContent of the item will be used as the label. | ||
* Use the `label` prop to override this behavior. | ||
*/ | ||
label?: string; | ||
/** | ||
* Is the item disabled? | ||
@@ -138,0 +130,0 @@ */ |
/// <reference types="svelte" /> | ||
import type { MeltActionReturn } from '../../internal/types.js'; | ||
import type { SelectEvents } from './events.js'; | ||
import type { CreateSelectProps, SelectOptionProps } from './types.js'; | ||
export declare function createSelect<Value extends Multiple extends true ? Array<unknown> : unknown = any, Multiple extends boolean = false>(props?: CreateSelectProps<Value, Multiple>): { | ||
import type { CreateSelectProps, SelectOption, SelectOptionProps } from './types.js'; | ||
export declare function createSelect<Value = unknown, Multiple extends boolean = false, Selected extends Multiple extends true ? Array<SelectOption<Value>> : SelectOption<Value> = Multiple extends true ? Array<SelectOption<Value>> : SelectOption<Value>>(props?: CreateSelectProps<Value, Multiple, Selected>): { | ||
elements: { | ||
@@ -35,6 +35,6 @@ menu: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[import("svelte/store").Readable<boolean>, import("svelte/store").Writable<string | HTMLElement | null | undefined>], (node: HTMLElement) => MeltActionReturn<SelectEvents['menu']>, ([$isVisible, $portal]: [boolean, string | HTMLElement | null | undefined]) => { | ||
option: import("../../internal/helpers/index.js").ExplicitBuilderReturn<{ | ||
update: (updater: import("svelte/store").Updater<Value>, sideEffect?: ((newValue: Value) => void) | undefined) => void; | ||
set: (this: void, value: Value) => void; | ||
subscribe(this: void, run: import("svelte/store").Subscriber<Value>, invalidate?: import("svelte/store").Invalidator<Value> | undefined): import("svelte/store").Unsubscriber; | ||
}, (node: HTMLElement) => MeltActionReturn<SelectEvents['option']>, ($value: Value) => (props: SelectOptionProps<Value>) => { | ||
update: (updater: import("svelte/store").Updater<Selected | undefined>, sideEffect?: ((newValue: Selected | undefined) => void) | undefined) => void; | ||
set: (this: void, value: Selected | undefined) => void; | ||
subscribe(this: void, run: import("svelte/store").Subscriber<Selected | undefined>, invalidate?: import("svelte/store").Invalidator<Selected | undefined> | undefined): import("svelte/store").Unsubscriber; | ||
}, (node: HTMLElement) => MeltActionReturn<SelectEvents['option']>, ($selected: Selected | undefined) => (props: SelectOptionProps<Value>) => { | ||
readonly role: "option"; | ||
@@ -49,9 +49,9 @@ readonly 'aria-selected': boolean; | ||
input: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[{ | ||
update: (updater: import("svelte/store").Updater<Value>, sideEffect?: ((newValue: Value) => void) | undefined) => void; | ||
set: (this: void, value: Value) => void; | ||
subscribe(this: void, run: import("svelte/store").Subscriber<Value>, invalidate?: import("svelte/store").Invalidator<Value> | undefined): import("svelte/store").Unsubscriber; | ||
}, import("svelte/store").Writable<boolean>, import("svelte/store").Writable<boolean>, import("svelte/store").Writable<string | undefined>], import("svelte/action").Action<any, any, Record<never, any>>, ([$value, $required, $disabled, $nameStore]: [Value, boolean, boolean, string | undefined]) => { | ||
update: (updater: import("svelte/store").Updater<Selected | undefined>, sideEffect?: ((newValue: Selected | undefined) => void) | undefined) => void; | ||
set: (this: void, value: Selected | undefined) => void; | ||
subscribe(this: void, run: import("svelte/store").Subscriber<Selected | undefined>, invalidate?: import("svelte/store").Invalidator<Selected | undefined> | undefined): import("svelte/store").Unsubscriber; | ||
}, import("svelte/store").Writable<boolean>, import("svelte/store").Writable<boolean>, import("svelte/store").Writable<string | undefined>], import("svelte/action").Action<any, any, Record<never, any>>, ([$value, $required, $disabled, $nameStore]: [Selected | undefined, boolean, boolean, string | undefined]) => { | ||
type: string; | ||
name: string | undefined; | ||
value: Value; | ||
value: Selected | undefined; | ||
'aria-hidden': boolean; | ||
@@ -92,11 +92,11 @@ hidden: boolean; | ||
}; | ||
value: { | ||
update: (updater: import("svelte/store").Updater<Value>, sideEffect?: ((newValue: Value) => void) | undefined) => void; | ||
set: (this: void, value: Value) => void; | ||
subscribe(this: void, run: import("svelte/store").Subscriber<Value>, invalidate?: import("svelte/store").Invalidator<Value> | undefined): import("svelte/store").Unsubscriber; | ||
selected: { | ||
update: (updater: import("svelte/store").Updater<Selected | undefined>, sideEffect?: ((newValue: Selected | undefined) => void) | undefined) => void; | ||
set: (this: void, value: Selected | undefined) => void; | ||
subscribe(this: void, run: import("svelte/store").Subscriber<Selected | undefined>, invalidate?: import("svelte/store").Invalidator<Selected | undefined> | undefined): import("svelte/store").Unsubscriber; | ||
}; | ||
valueLabel: import("svelte/store").Readable<string | null>; | ||
selectedLabel: import("svelte/store").Readable<string>; | ||
}; | ||
helpers: { | ||
isSelected: import("svelte/store").Readable<(value: unknown) => boolean>; | ||
isSelected: import("svelte/store").Readable<(value: Value) => boolean>; | ||
}; | ||
@@ -108,3 +108,2 @@ options: { | ||
name: import("svelte/store").Writable<string | undefined>; | ||
defaultValue?: import("svelte/store").Writable<Value | undefined> | undefined; | ||
required: import("svelte/store").Writable<boolean>; | ||
@@ -111,0 +110,0 @@ preventScroll: import("svelte/store").Writable<boolean>; |
@@ -5,3 +5,3 @@ import { createLabel, createSeparator } from '../index.js'; | ||
import { onMount, tick } from 'svelte'; | ||
import { derived, get, readonly, writable } from 'svelte/store'; | ||
import { derived, get, writable } from 'svelte/store'; | ||
const defaults = { | ||
@@ -28,14 +28,10 @@ arrowSize: 8, | ||
const options = toWritableStores({ | ||
...omit(withDefaults, 'value', 'defaultValueLabel', 'onValueChange', 'onOpenChange', 'open', 'defaultOpen'), | ||
...omit(withDefaults, 'selected', 'defaultSelected', 'onSelectedChange', 'onOpenChange', 'open', 'defaultOpen'), | ||
multiple: withDefaults.multiple ?? false, | ||
}); | ||
const { positioning, arrowSize, required, disabled, loop, preventScroll, name: nameStore, portal, forceVisible, closeOnEscape, closeOnOutsideClick, multiple, } = options; | ||
let mounted = false; | ||
const openWritable = withDefaults.open ?? writable(true); | ||
const openWritable = withDefaults.open ?? writable(false); | ||
const open = overridable(openWritable, withDefaults?.onOpenChange); | ||
// Open so we can register the optionsList items before mounted = true | ||
open.set(true); | ||
const valueWritable = withDefaults.value ?? writable(withDefaults.defaultValue); | ||
const value = overridable(valueWritable, withDefaults?.onValueChange); | ||
const valueLabel = writable(withDefaults.defaultValueLabel ?? null); | ||
const selectedWritable = withDefaults.selected ?? writable(withDefaults.defaultSelected); | ||
const selected = overridable(selectedWritable, withDefaults?.onSelectedChange); | ||
const activeTrigger = writable(null); | ||
@@ -64,24 +60,109 @@ /** | ||
const { typed, handleTypeaheadSearch } = createTypeaheadSearch(); | ||
onMount(() => { | ||
// Run after all initial effects | ||
tick().then(() => { | ||
mounted = true; | ||
}); | ||
open.set(withDefaults.defaultOpen); | ||
if (!isBrowser) | ||
/* ------- */ | ||
/* Helpers */ | ||
/* ------- */ | ||
const isSelected = derived([selected], ([$selected]) => { | ||
return (value) => { | ||
if (Array.isArray($selected)) { | ||
return $selected.map((o) => o.value).includes(value); | ||
} | ||
return $selected === value; | ||
}; | ||
}); | ||
function isMouse(e) { | ||
return e.pointerType === 'mouse'; | ||
} | ||
function getSelectedOption(menuElement) { | ||
const selectedOption = menuElement.querySelector('[data-selected]'); | ||
return isHTMLElement(selectedOption) ? selectedOption : null; | ||
} | ||
function onOptionPointerMove(e) { | ||
if (!isMouse(e)) | ||
return; | ||
const menuEl = document.getElementById(ids.menu); | ||
if (!menuEl) | ||
const currentTarget = e.currentTarget; | ||
if (!isHTMLElement(currentTarget)) | ||
return; | ||
const triggerEl = document.getElementById(ids.trigger); | ||
if (triggerEl) { | ||
activeTrigger.set(triggerEl); | ||
handleRovingFocus(currentTarget); | ||
} | ||
function onOptionLeave() { | ||
const menuElement = document.getElementById(ids.menu); | ||
if (!isHTMLElement(menuElement)) | ||
return; | ||
handleRovingFocus(menuElement); | ||
} | ||
/** | ||
* Keyboard event handler for menu navigation | ||
* @param e The keyboard event | ||
*/ | ||
function handleMenuNavigation(e) { | ||
e.preventDefault(); | ||
// currently focused menu item | ||
const currentFocusedItem = document.activeElement; | ||
// menu element being navigated | ||
const currentTarget = e.currentTarget; | ||
if (!isHTMLElement(currentFocusedItem) || !isHTMLElement(currentTarget)) | ||
return; | ||
// menu items of the current menu | ||
const items = getOptions(currentTarget); | ||
if (!items.length) | ||
return; | ||
// Disabled items can't be highlighted. Skip them. | ||
const candidateNodes = items.filter((opt) => !isElementDisabled(opt)); | ||
// Get the index of the currently highlighted item. | ||
const currentIndex = candidateNodes.indexOf(currentFocusedItem); | ||
// Find the next menu item to highlight. | ||
let nextItem; | ||
const $loop = get(loop); | ||
switch (e.key) { | ||
case kbd.ARROW_DOWN: | ||
nextItem = next(candidateNodes, currentIndex, $loop); | ||
break; | ||
case kbd.PAGE_DOWN: | ||
nextItem = forward(candidateNodes, currentIndex, 10, $loop); | ||
break; | ||
case kbd.ARROW_UP: | ||
nextItem = prev(candidateNodes, currentIndex, $loop); | ||
break; | ||
case kbd.PAGE_UP: | ||
nextItem = back(candidateNodes, currentIndex, 10, $loop); | ||
break; | ||
case kbd.HOME: | ||
nextItem = candidateNodes[0]; | ||
break; | ||
case kbd.END: | ||
nextItem = last(candidateNodes); | ||
break; | ||
default: | ||
return; | ||
} | ||
const selectedEl = menuEl.querySelector('[data-selected]'); | ||
if (!isHTMLElement(selectedEl)) | ||
return; | ||
const dataLabel = selectedEl.getAttribute('data-label'); | ||
valueLabel.set(dataLabel ?? selectedEl.textContent ?? null); | ||
handleRovingFocus(nextItem); | ||
} | ||
function handleTabNavigation(e) { | ||
if (e.shiftKey) { | ||
const $prevFocusable = get(prevFocusable); | ||
if ($prevFocusable) { | ||
e.preventDefault(); | ||
$prevFocusable.focus(); | ||
prevFocusable.set(null); | ||
} | ||
} | ||
else { | ||
const $nextFocusable = get(nextFocusable); | ||
if ($nextFocusable) { | ||
e.preventDefault(); | ||
$nextFocusable.focus(); | ||
nextFocusable.set(null); | ||
} | ||
} | ||
} | ||
const isVisible = derivedVisible({ open, forceVisible, activeTrigger }); | ||
const selectedLabel = derived(selected, ($selected) => { | ||
if (Array.isArray($selected)) { | ||
return $selected.map((o) => o.label).join(', '); | ||
} | ||
return $selected?.label ?? ''; | ||
}); | ||
const isVisible = derivedVisible({ open, forceVisible, activeTrigger }); | ||
/* -------- */ | ||
/* Builders */ | ||
/* -------- */ | ||
const menu = builder(name('menu'), { | ||
@@ -320,32 +401,19 @@ stores: [isVisible, portal], | ||
}; | ||
const setValue = (newValue) => { | ||
value.update(($value) => { | ||
const setOption = (newOption) => { | ||
selected.update(($option) => { | ||
const $multiple = get(multiple); | ||
if (Array.isArray($value) || ($value === undefined && $multiple)) { | ||
return toggle(newValue, ($value ?? [])); | ||
if ($multiple) { | ||
const optionArr = Array.isArray($option) ? $option : []; | ||
return toggle(newOption, optionArr); | ||
} | ||
return newValue; | ||
return newOption; | ||
}); | ||
}; | ||
/** | ||
* List of options fetched from SSR. | ||
*/ | ||
const cachedOptionPropsArr = []; | ||
const getOptionPropsArr = () => { | ||
if (!isBrowser) | ||
return cachedOptionPropsArr; | ||
const menuEl = document.getElementById(ids.menu); | ||
if (!menuEl) | ||
return cachedOptionPropsArr; | ||
const options = getOptions(menuEl); | ||
return options.map(getOptionProps); | ||
}; | ||
const option = builder(name('option'), { | ||
stores: value, | ||
returned: ($value) => { | ||
stores: selected, | ||
returned: ($selected) => { | ||
return (props) => { | ||
cachedOptionPropsArr.push(props); | ||
const isSelected = Array.isArray($value) | ||
? $value.includes(props?.value) | ||
: $value === props?.value; | ||
const isSelected = Array.isArray($selected) | ||
? $selected.map((o) => o.value).includes(props.value) | ||
: $selected?.value === props?.value; | ||
return { | ||
@@ -373,3 +441,3 @@ role: 'option', | ||
handleRovingFocus(itemElement); | ||
setValue(props.value); | ||
setOption(props); | ||
const $multiple = get(multiple); | ||
@@ -389,3 +457,3 @@ if (!$multiple) | ||
node.setAttribute('data-selected', ''); | ||
setValue(props.value); | ||
setOption(props); | ||
const $multiple = get(multiple); | ||
@@ -431,16 +499,37 @@ if (!$multiple) | ||
}); | ||
effect(value, function updateValueLabel($value) { | ||
const optionPropsArr = getOptionPropsArr(); | ||
if (Array.isArray($value)) { | ||
const labels = optionPropsArr.reduce((result, current) => { | ||
if ($value.includes(current.value) && current.label) { | ||
result.add(current.label); | ||
} | ||
return result; | ||
}, new Set()); | ||
valueLabel.set(Array.from(labels).join(', ')); | ||
const input = builder(name('input'), { | ||
stores: [selected, required, disabled, nameStore], | ||
returned: ([$value, $required, $disabled, $nameStore]) => { | ||
return { | ||
type: 'hidden', | ||
name: $nameStore, | ||
value: $value, | ||
'aria-hidden': true, | ||
hidden: true, | ||
tabIndex: -1, | ||
required: $required, | ||
disabled: $disabled, | ||
style: styleToString({ | ||
position: 'absolute', | ||
opacity: 0, | ||
'pointer-events': 'none', | ||
margin: 0, | ||
transform: 'translateX(-100%)', | ||
}), | ||
}; | ||
}, | ||
}); | ||
/* ------------------- */ | ||
/* Lifecycle & Effects */ | ||
/* ------------------- */ | ||
onMount(() => { | ||
const triggerEl = document.getElementById(ids.trigger); | ||
if (triggerEl) { | ||
activeTrigger.set(triggerEl); | ||
} | ||
else { | ||
const newLabel = optionPropsArr.find((opt) => opt.value === $value)?.label; | ||
valueLabel.set(newLabel ?? null); | ||
}); | ||
let hasOpened = false; | ||
effect(open, ($open) => { | ||
if ($open) { | ||
hasOpened = true; | ||
} | ||
@@ -455,3 +544,2 @@ }); | ||
} | ||
const constantMounted = mounted; | ||
sleep(1).then(() => { | ||
@@ -476,3 +564,3 @@ const menuEl = document.getElementById(ids.menu); | ||
} | ||
else if ($activeTrigger && constantMounted && isUsingKeyboard) { | ||
else if ($activeTrigger && hasOpened) { | ||
// Hacky way to prevent the keydown event from triggering on the trigger | ||
@@ -486,10 +574,2 @@ handleRovingFocus($activeTrigger); | ||
}); | ||
const isSelected = derived([value], ([$value]) => { | ||
return (value) => { | ||
if (Array.isArray($value)) { | ||
return $value.includes(value); | ||
} | ||
return $value === value; | ||
}; | ||
}); | ||
effect([open, activeTrigger], ([$open, $activeTrigger]) => { | ||
@@ -510,110 +590,2 @@ if (!isBrowser) | ||
}); | ||
const input = builder(name('input'), { | ||
stores: [value, required, disabled, nameStore], | ||
returned: ([$value, $required, $disabled, $nameStore]) => { | ||
return { | ||
type: 'hidden', | ||
name: $nameStore, | ||
value: $value, | ||
'aria-hidden': true, | ||
hidden: true, | ||
tabIndex: -1, | ||
required: $required, | ||
disabled: $disabled, | ||
style: styleToString({ | ||
position: 'absolute', | ||
opacity: 0, | ||
'pointer-events': 'none', | ||
margin: 0, | ||
transform: 'translateX(-100%)', | ||
}), | ||
}; | ||
}, | ||
}); | ||
function isMouse(e) { | ||
return e.pointerType === 'mouse'; | ||
} | ||
function getSelectedOption(menuElement) { | ||
const selectedOption = menuElement.querySelector('[data-selected]'); | ||
return isHTMLElement(selectedOption) ? selectedOption : null; | ||
} | ||
function onOptionPointerMove(e) { | ||
if (!isMouse(e)) | ||
return; | ||
const currentTarget = e.currentTarget; | ||
if (!isHTMLElement(currentTarget)) | ||
return; | ||
handleRovingFocus(currentTarget); | ||
} | ||
function onOptionLeave() { | ||
const menuElement = document.getElementById(ids.menu); | ||
if (!isHTMLElement(menuElement)) | ||
return; | ||
handleRovingFocus(menuElement); | ||
} | ||
/** | ||
* Keyboard event handler for menu navigation | ||
* @param e The keyboard event | ||
*/ | ||
function handleMenuNavigation(e) { | ||
e.preventDefault(); | ||
// currently focused menu item | ||
const currentFocusedItem = document.activeElement; | ||
// menu element being navigated | ||
const currentTarget = e.currentTarget; | ||
if (!isHTMLElement(currentFocusedItem) || !isHTMLElement(currentTarget)) | ||
return; | ||
// menu items of the current menu | ||
const items = getOptions(currentTarget); | ||
if (!items.length) | ||
return; | ||
// Disabled items can't be highlighted. Skip them. | ||
const candidateNodes = items.filter((opt) => !isElementDisabled(opt)); | ||
// Get the index of the currently highlighted item. | ||
const currentIndex = candidateNodes.indexOf(currentFocusedItem); | ||
// Find the next menu item to highlight. | ||
let nextItem; | ||
const $loop = get(loop); | ||
switch (e.key) { | ||
case kbd.ARROW_DOWN: | ||
nextItem = next(candidateNodes, currentIndex, $loop); | ||
break; | ||
case kbd.PAGE_DOWN: | ||
nextItem = forward(candidateNodes, currentIndex, 10, $loop); | ||
break; | ||
case kbd.ARROW_UP: | ||
nextItem = prev(candidateNodes, currentIndex, $loop); | ||
break; | ||
case kbd.PAGE_UP: | ||
nextItem = back(candidateNodes, currentIndex, 10, $loop); | ||
break; | ||
case kbd.HOME: | ||
nextItem = candidateNodes[0]; | ||
break; | ||
case kbd.END: | ||
nextItem = last(candidateNodes); | ||
break; | ||
default: | ||
return; | ||
} | ||
handleRovingFocus(nextItem); | ||
} | ||
function handleTabNavigation(e) { | ||
if (e.shiftKey) { | ||
const $prevFocusable = get(prevFocusable); | ||
if ($prevFocusable) { | ||
e.preventDefault(); | ||
$prevFocusable.focus(); | ||
prevFocusable.set(null); | ||
} | ||
} | ||
else { | ||
const $nextFocusable = get(nextFocusable); | ||
if ($nextFocusable) { | ||
e.preventDefault(); | ||
$nextFocusable.focus(); | ||
nextFocusable.set(null); | ||
} | ||
} | ||
} | ||
return { | ||
@@ -633,4 +605,4 @@ elements: { | ||
open, | ||
value, | ||
valueLabel: readonly(valueLabel), | ||
selected, | ||
selectedLabel, | ||
}, | ||
@@ -637,0 +609,0 @@ helpers: { |
@@ -7,3 +7,7 @@ import type { FloatingConfig } from '../../internal/actions/index.js'; | ||
export type { SelectComponentEvents } from './events.js'; | ||
export type CreateSelectProps<Item extends Multiple extends true ? Array<unknown> : unknown = any, Multiple extends boolean = false> = { | ||
export type SelectOption<Value> = { | ||
value: Value; | ||
label?: string; | ||
}; | ||
export type CreateSelectProps<Value = unknown, Multiple extends boolean = false, Selected extends Multiple extends true ? Array<SelectOption<Value>> : SelectOption<Value> = Multiple extends true ? Array<SelectOption<Value>> : SelectOption<Value>> = { | ||
/** | ||
@@ -50,8 +54,2 @@ * Options for positioning the popover menu. | ||
/** | ||
* The label for the select input. | ||
* | ||
* @default undefined | ||
*/ | ||
defaultValueLabel?: string; | ||
/** | ||
* The name for the select input. | ||
@@ -108,7 +106,7 @@ * | ||
*/ | ||
defaultValue?: Item; | ||
defaultSelected?: Selected; | ||
/** | ||
* An optional controlled store that manages the value state of the combobox. | ||
*/ | ||
value?: Writable<Item>; | ||
selected?: Writable<Selected>; | ||
/** | ||
@@ -119,8 +117,6 @@ * A change handler for the value store called when the value would normally change. | ||
*/ | ||
onValueChange?: ChangeFn<Item>; | ||
onSelectedChange?: ChangeFn<Selected | undefined>; | ||
multiple?: Multiple; | ||
}; | ||
export type SelectOptionProps<Item = unknown> = { | ||
value: Item; | ||
label?: string; | ||
export type SelectOptionProps<Value = unknown> = SelectOption<Value> & { | ||
disabled?: boolean; | ||
@@ -127,0 +123,0 @@ }; |
@@ -70,6 +70,6 @@ /// <reference types="svelte" /> | ||
update: import("svelte/store").Writable<import("./types.js").UpdateTag | undefined>; | ||
selected?: import("svelte/store").Writable<Tag | undefined> | undefined; | ||
placeholder: import("svelte/store").Writable<string>; | ||
add: import("svelte/store").Writable<import("./types.js").AddTag | undefined>; | ||
editable: import("svelte/store").Writable<boolean>; | ||
selected?: import("svelte/store").Writable<Tag | undefined> | undefined; | ||
defaultTags: import("svelte/store").Writable<string[] | Tag[]>; | ||
@@ -76,0 +76,0 @@ onTagsChange?: import("svelte/store").Writable<import("../../internal/helpers/index.js").ChangeFn<Tag[]> | undefined> | undefined; |
@@ -30,3 +30,2 @@ import { addEventListener, addMeltEventListener, builder, createElHelpers, derivedVisible, effect, executeCallbacks, generateId, getPortalDestination, isBrowser, isTouch, kbd, makeHullFromElements, noop, omit, overridable, pointInPolygon, styleToString, toWritableStores, } from '../../internal/helpers/index.js'; | ||
}; | ||
let timeout = null; | ||
let clickedTrigger = false; | ||
@@ -38,23 +37,31 @@ onMount(() => { | ||
}); | ||
let openTimeout = null; | ||
let closeTimeout = null; | ||
function openTooltip() { | ||
if (timeout) { | ||
window.clearTimeout(timeout); | ||
timeout = null; | ||
if (closeTimeout) { | ||
window.clearTimeout(closeTimeout); | ||
closeTimeout = null; | ||
} | ||
timeout = window.setTimeout(() => { | ||
open.set(true); | ||
}, get(openDelay)); | ||
if (!openTimeout) { | ||
openTimeout = window.setTimeout(() => { | ||
open.set(true); | ||
openTimeout = null; | ||
}, get(openDelay)); | ||
} | ||
} | ||
function closeTooltip(isBlur) { | ||
if (timeout) { | ||
window.clearTimeout(timeout); | ||
timeout = null; | ||
if (openTimeout) { | ||
window.clearTimeout(openTimeout); | ||
openTimeout = null; | ||
} | ||
if (isBlur && isMouseInTooltipArea) | ||
return; | ||
timeout = window.setTimeout(() => { | ||
open.set(false); | ||
if (isBlur) | ||
clickedTrigger = false; | ||
}, get(closeDelay)); | ||
if (!closeTimeout) { | ||
closeTimeout = window.setTimeout(() => { | ||
open.set(false); | ||
if (isBlur) | ||
clickedTrigger = false; | ||
closeTimeout = null; | ||
}, get(closeDelay)); | ||
} | ||
} | ||
@@ -74,5 +81,5 @@ const trigger = builder(name('trigger'), { | ||
clickedTrigger = true; | ||
if (timeout) { | ||
window.clearTimeout(timeout); | ||
timeout = null; | ||
if (openTimeout) { | ||
window.clearTimeout(openTimeout); | ||
openTimeout = null; | ||
} | ||
@@ -86,5 +93,5 @@ }), addMeltEventListener(node, 'pointerenter', (e) => { | ||
return; | ||
if (timeout) { | ||
window.clearTimeout(timeout); | ||
timeout = null; | ||
if (openTimeout) { | ||
window.clearTimeout(openTimeout); | ||
openTimeout = null; | ||
} | ||
@@ -97,5 +104,5 @@ }), addMeltEventListener(node, 'focus', () => { | ||
if (get(closeOnEscape) && e.key === kbd.ESCAPE) { | ||
if (timeout) { | ||
window.clearTimeout(timeout); | ||
timeout = null; | ||
if (openTimeout) { | ||
window.clearTimeout(openTimeout); | ||
openTimeout = null; | ||
} | ||
@@ -102,0 +109,0 @@ open.set(false); |
@@ -0,1 +1,2 @@ | ||
import deepEqual from 'deep-equal'; | ||
/** | ||
@@ -76,7 +77,10 @@ * Returns the element some number before the given index. If the target index is out of bounds: | ||
export function toggle(item, array) { | ||
if (array.includes(item)) { | ||
return array.filter((i) => i !== item); | ||
const itemIdx = array.findIndex((i) => deepEqual(i, item)); | ||
if (itemIdx !== -1) { | ||
array.splice(itemIdx, 1); | ||
} | ||
array.push(item); | ||
else { | ||
array.push(item); | ||
} | ||
return array; | ||
} |
{ | ||
"name": "@melt-ui/svelte", | ||
"version": "0.38.1", | ||
"version": "0.39.0", | ||
"license": "MIT", | ||
@@ -5,0 +5,0 @@ "exports": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
591411
13688