Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@melt-ui/svelte

Package Overview
Dependencies
Maintainers
1
Versions
195
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@melt-ui/svelte - npm Package Compare versions

Comparing version 0.54.1 to 0.55.0

dist/builders/listbox/create.d.ts

89

dist/builders/combobox/create.d.ts
/// <reference types="svelte" />
import type { MeltActionReturn } from '../../internal/types.js';
import { type Readable } from 'svelte/store';
import type { ComboboxEvents } from './events.js';
import type { ComboboxOption, ComboboxOptionProps, ComboboxSelected, CreateComboboxProps } from './types.js';
import type { ComboboxSelected, CreateComboboxProps } from './types.js';
export declare const INTERACTION_KEYS: string[];

@@ -10,18 +9,11 @@ /**

*
* @TODO expose a nice mechanism for clearing the input.
* @TODO would it be useful to have a callback for when an item is selected?
* @TODO multi-select using `tags-input` builder?
*/
export declare function createCombobox<Value, Multiple extends boolean = false, S extends ComboboxSelected<Multiple, Value> = ComboboxSelected<Multiple, Value>>(props?: CreateComboboxProps<Value, Multiple, S>): {
ids: {
input: string;
menu: string;
label: string;
};
elements: {
input: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[{
input: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[import("../../internal/helpers/index.js").ExplicitBuilderReturn<[{
update: (updater: import("svelte/store").Updater<boolean>, sideEffect?: ((newValue: boolean) => void) | undefined) => void;
set: (this: void, value: boolean) => void;
subscribe(this: void, run: import("svelte/store").Subscriber<boolean>, invalidate?: import("svelte/store").Invalidator<boolean> | undefined): import("svelte/store").Unsubscriber;
}, import("svelte/store").Writable<HTMLElement | null>, import("svelte/store").Writable<string>], (node: HTMLInputElement) => MeltActionReturn<ComboboxEvents['input']>, ([$open, $highlightedItem, $inputValue]: [boolean, HTMLElement | null, string]) => {
}, import("svelte/store").Writable<HTMLElement | null>, import("svelte/store").Writable<boolean>], (node: HTMLElement) => MeltActionReturn<"input" | "click" | "keydown">, ([$open, $highlightedItem, $disabled]: [boolean, HTMLElement | null, boolean]) => {
readonly 'aria-activedescendant': string | undefined;

@@ -33,7 +25,45 @@ readonly 'aria-autocomplete': "list";

readonly 'data-melt-id': string;
readonly autocomplete: "off";
readonly id: string;
readonly role: "combobox";
readonly disabled: true | undefined;
}, string>, import("svelte/store").Writable<string>], (node: HTMLInputElement) => MeltActionReturn<ComboboxEvents['input']>, ([$trigger, $inputValue]: [{
readonly 'aria-activedescendant': string | undefined;
readonly 'aria-autocomplete': "list";
readonly 'aria-controls': string;
readonly 'aria-expanded': boolean;
readonly 'aria-labelledby': string;
readonly 'data-melt-id': string;
readonly id: string;
readonly role: "combobox";
readonly disabled: true | undefined;
} & {
[x: `data-melt-${string}`]: "";
} & {
action: (node: HTMLElement) => MeltActionReturn<"input" | "click" | "keydown">;
}, string]) => {
readonly role: "combobox";
readonly value: string;
readonly disabled: true | undefined;
readonly 'aria-expanded': boolean;
readonly 'aria-controls': string;
readonly 'aria-activedescendant': string | undefined;
readonly 'aria-autocomplete': "list";
readonly 'aria-labelledby': string;
readonly id: string;
}, string>;
trigger: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[{
update: (updater: import("svelte/store").Updater<boolean>, sideEffect?: ((newValue: boolean) => void) | undefined) => void;
set: (this: void, value: boolean) => void;
subscribe(this: void, run: import("svelte/store").Subscriber<boolean>, invalidate?: import("svelte/store").Invalidator<boolean> | undefined): import("svelte/store").Unsubscriber;
}, import("svelte/store").Writable<HTMLElement | null>, import("svelte/store").Writable<boolean>], (node: HTMLElement) => MeltActionReturn<"input" | "click" | "keydown">, ([$open, $highlightedItem, $disabled]: [boolean, HTMLElement | null, boolean]) => {
readonly 'aria-activedescendant': string | undefined;
readonly 'aria-autocomplete': "list";
readonly 'aria-controls': string;
readonly 'aria-expanded': boolean;
readonly 'aria-labelledby': string;
readonly 'data-melt-id': string;
readonly id: string;
readonly role: "combobox";
readonly disabled: true | undefined;
}, string>;
option: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[{

@@ -43,3 +73,3 @@ update: (updater: import("svelte/store").Updater<S | undefined>, sideEffect?: ((newValue: S | undefined) => void) | undefined) => void;

subscribe(this: void, run: import("svelte/store").Subscriber<S | undefined>, invalidate?: import("svelte/store").Invalidator<S | undefined> | undefined): import("svelte/store").Unsubscriber;
}], (node: HTMLElement) => MeltActionReturn<ComboboxEvents['item']>, ([$selected]: [S | undefined]) => (props: ComboboxOptionProps<Value>) => {
}], (node: HTMLElement) => MeltActionReturn<"click" | "pointermove">, ([$selected]: [S | undefined]) => (props: import("./types.js").ComboboxOptionProps<unknown>) => {
readonly 'data-value': string;

@@ -55,3 +85,3 @@ readonly 'data-label': string | undefined;

}, string>;
menu: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[Readable<boolean>], (node: HTMLElement) => MeltActionReturn<ComboboxEvents['menu']>, ([$isVisible]: [boolean]) => {
menu: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[import("svelte/store").Readable<boolean>], (node: HTMLElement) => MeltActionReturn<"pointerleave">, ([$isVisible]: [boolean]) => {
readonly hidden: true | undefined;

@@ -70,4 +100,6 @@ readonly id: string;

subscribe(this: void, run: import("svelte/store").Subscriber<S | undefined>, invalidate?: import("svelte/store").Invalidator<S | undefined> | undefined): import("svelte/store").Unsubscriber;
}], import("svelte/action").Action<any, any, Record<never, any>>, ([$selected]: [S | undefined]) => {
value: Value | Value[] | undefined;
}, import("svelte/store").Writable<boolean>, import("svelte/store").Writable<string | undefined>], import("svelte/action").Action<any, any, Record<never, any>>, ([$selected, $required, $name]: [S | undefined, boolean, string | undefined]) => {
required: boolean | undefined;
value: unknown;
name: string | undefined;
type: string;

@@ -79,4 +111,10 @@ 'aria-hidden': boolean;

}, string>;
arrow: import("../../internal/helpers/index.js").ExplicitBuilderReturn<import("svelte/store").Writable<number | undefined>, import("svelte/action").Action<any, any, Record<never, any>>, ($arrowSize: number | undefined) => {
'data-arrow': boolean;
style: string;
}, string>;
};
states: {
touchedInput: import("svelte/store").Writable<boolean>;
inputValue: import("svelte/store").Writable<string>;
open: {

@@ -92,13 +130,20 @@ update: (updater: import("svelte/store").Updater<boolean>, sideEffect?: ((newValue: boolean) => void) | undefined) => void;

};
highlighted: Readable<ComboboxOption<Value> | undefined>;
touchedInput: import("svelte/store").Writable<boolean>;
inputValue: import("svelte/store").Writable<string>;
highlighted: import("svelte/store").Readable<import("./types.js").ComboboxOption<unknown> | undefined>;
};
ids: {
trigger: string;
menu: string;
label: string;
};
helpers: {
isSelected: Readable<(value: Value) => boolean>;
isHighlighted: Readable<(item: Value) => boolean>;
isSelected: import("svelte/store").Readable<(value: unknown) => boolean>;
isHighlighted: import("svelte/store").Readable<(item: unknown) => boolean>;
closeMenu: () => void;
};
options: {
multiple: import("svelte/store").Writable<Multiple>;
disabled: import("svelte/store").Writable<boolean>;
forceVisible: import("svelte/store").Writable<boolean>;
name: import("svelte/store").Writable<string | undefined>;
required: import("svelte/store").Writable<boolean>;
onOpenChange?: import("svelte/store").Writable<import("../../internal/helpers/index.js").ChangeFn<boolean> | undefined> | undefined;

@@ -111,2 +156,3 @@ preventScroll: import("svelte/store").Writable<boolean>;

}>;
arrowSize?: import("svelte/store").Writable<number | undefined> | undefined;
scrollAlignment: import("svelte/store").Writable<"center" | "nearest">;

@@ -119,3 +165,4 @@ loop: import("svelte/store").Writable<boolean>;

closeOnEscape: import("svelte/store").Writable<boolean>;
typeahead: import("svelte/store").Writable<boolean>;
};
};

@@ -1,153 +0,17 @@

import { useEscapeKeydown, usePopper } from '../../internal/actions/index.js';
import { FIRST_LAST_KEYS, addEventListener, addHighlight, addMeltEventListener, back, builder, createClickOutsideIgnore, createElHelpers, derivedVisible, disabledAttr, effect, executeCallbacks, forward, generateId, getElementByMeltId, getOptions, getPortalDestination, hiddenInputAttrs, isBrowser, isElementDisabled, isHTMLElement, isHTMLInputElement, kbd, last, next, noop, omit, overridable, prev, removeHighlight, removeScroll, styleToString, toWritableStores, toggle, } from '../../internal/helpers/index.js';
import { dequal as deepEqual } from 'dequal';
import { onMount, tick } from 'svelte';
import { derived, get, writable } from 'svelte/store';
import { createLabel } from '../label/create.js';
import { useEscapeKeydown } from '../../internal/actions/index.js';
import { addEventListener, addMeltEventListener, builder, createElHelpers, effect, executeCallbacks, isHTMLInputElement, kbd, noop, omit, } from '../../internal/helpers/index.js';
import { writable } from 'svelte/store';
import { createListbox } from '../listbox/create.js';
// prettier-ignore
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 defaults = {
positioning: {
placement: 'bottom',
sameWidth: true,
},
scrollAlignment: 'nearest',
loop: true,
defaultOpen: false,
closeOnOutsideClick: true,
preventScroll: true,
closeOnEscape: true,
forceVisible: false,
portal: undefined,
};
const { name, selector } = createElHelpers('combobox');
const { name } = createElHelpers('combobox');
/**
* Creates an ARIA-1.2-compliant combobox.
*
* @TODO expose a nice mechanism for clearing the input.
* @TODO would it be useful to have a callback for when an item is selected?
* @TODO multi-select using `tags-input` builder?
*/
export function createCombobox(props) {
const withDefaults = { ...defaults, ...props };
// Trigger element for the popper portal. This will be our input element.
const activeTrigger = writable(null);
// The currently highlighted menu item.
const highlightedItem = writable(null);
const selectedWritable = withDefaults.selected ?? writable(withDefaults.defaultSelected);
const selected = overridable(selectedWritable, withDefaults?.onSelectedChange);
const highlighted = derived(highlightedItem, ($highlightedItem) => $highlightedItem ? getOptionProps($highlightedItem) : undefined);
// Either the provided open store or a store with the default open value
const openWritable = withDefaults.open ?? writable(withDefaults.defaultOpen);
// The overridable open store which is the source of truth for the open state.
const open = overridable(openWritable, withDefaults?.onOpenChange);
const listbox = createListbox({ ...props, builder: 'combobox', typeahead: false });
const inputValue = writable('');
const touchedInput = writable(false);
const options = toWritableStores({
...omit(withDefaults, 'open', 'defaultOpen'),
multiple: withDefaults.multiple ?? false,
});
const { scrollAlignment, loop, closeOnOutsideClick, closeOnEscape, preventScroll, portal, forceVisible, positioning, multiple, } = options;
const ids = {
input: generateId(),
menu: generateId(),
label: generateId(),
};
/** ------- */
/** HELPERS */
/** ------- */
function getOptionProps(el) {
const value = el.getAttribute('data-value');
const label = el.getAttribute('data-label');
const disabled = el.hasAttribute('data-disabled');
return {
value: value ? JSON.parse(value) : value,
label: label ?? el.textContent ?? undefined,
disabled: disabled ? true : false,
};
}
const setOption = (newOption) => {
selected.update(($option) => {
const $multiple = get(multiple);
if ($multiple) {
const optionArr = Array.isArray($option) ? $option : [];
return toggle(newOption, optionArr, (itemA, itemB) => deepEqual(itemA.value, itemB.value));
}
return newOption;
});
};
/**
* Selects an item from the menu
* @param index array index of the item to select.
*/
function selectItem(item) {
const props = getOptionProps(item);
setOption(props);
}
/**
* Opens the menu, sets the active trigger, and highlights
* the selected item (if one exists). It also optionally accepts the current
* open state to prevent unnecessary updates if we know the menu is already open.
*/
async function openMenu(currentOpenState = false) {
/**
* We're checking the open state here because the menu may have
* been programatically opened by the user using a controlled store.
* In that case we don't want to update the open state, but we do
* want to update the active trigger and highlighted item as normal.
*/
if (!currentOpenState) {
open.set(true);
}
const triggerEl = getElementByMeltId(ids.input);
if (!triggerEl)
return;
// The active trigger is used to anchor the menu to the input element.
activeTrigger.set(triggerEl);
// Wait a tick for the menu to open then highlight the selected item.
await tick();
const menuElement = document.getElementById(ids.menu);
if (!isHTMLElement(menuElement))
return;
const selectedItem = menuElement.querySelector('[aria-selected=true]');
if (!isHTMLElement(selectedItem))
return;
highlightedItem.set(selectedItem);
}
/** Closes the menu & clears the active trigger */
function closeMenu() {
open.set(false);
touchedInput.set(false);
highlightedItem.set(null);
}
/**
* To properly anchor the popper to the input/trigger, we need to ensure both
* the open state is true and the activeTrigger is not null. This helper store's
* value is true when both of these conditions are met and keeps the code tidy.
*/
const isVisible = derivedVisible({ open, forceVisible, activeTrigger });
/* ------ */
/* STATES */
/* ------ */
/**
* Determines if a given item is selected.
* This is useful for displaying additional markup on the selected item.
*/
const isSelected = derived([selected], ([$selected]) => {
return (value) => {
if (Array.isArray($selected)) {
return $selected.some((o) => deepEqual(o.value, value));
}
return deepEqual($selected?.value, value);
};
});
/**
* Determines if a given item is highlighted.
* This is useful for displaying additional markup on the highlighted item.
*/
const isHighlighted = derived([highlighted], ([$value]) => {
return (item) => {
return deepEqual($value?.value, item);
};
});
/* -------- */

@@ -158,13 +22,6 @@ /* ELEMENTS */

const input = builder(name('input'), {
stores: [open, highlightedItem, inputValue],
returned: ([$open, $highlightedItem, $inputValue]) => {
stores: [listbox.elements.trigger, inputValue],
returned: ([$trigger, $inputValue]) => {
return {
'aria-activedescendant': $highlightedItem?.id,
'aria-autocomplete': 'list',
'aria-controls': ids.menu,
'aria-expanded': $open,
'aria-labelledby': ids.label,
'data-melt-id': ids.input,
autocomplete: 'off',
id: ids.input,
...omit($trigger, 'action'),
role: 'combobox',

@@ -175,117 +32,3 @@ value: $inputValue,

action: (node) => {
const unsubscribe = executeCallbacks(addMeltEventListener(node, 'click', () => {
const $open = get(open);
if ($open) {
return;
}
openMenu($open);
}),
// Handle all input key events including typing, meta, and navigation.
addMeltEventListener(node, 'keydown', (e) => {
const $open = get(open);
/**
* When the menu is closed...
*/
if (!$open) {
// Pressing one of the interaction keys shouldn't open the menu.
if (INTERACTION_KEYS.includes(e.key)) {
return;
}
// Tab should not open the menu.
if (e.key === kbd.TAB) {
return;
}
// Pressing backspace when the input is blank shouldn't open the menu.
if (e.key === kbd.BACKSPACE && node.value === '') {
return;
}
// All other events should open the menu.
openMenu($open);
tick().then(() => {
const $selectedItem = get(selected);
if ($selectedItem)
return;
const menuEl = document.getElementById(ids.menu);
if (!isHTMLElement(menuEl))
return;
const enabledItems = Array.from(menuEl.querySelectorAll(`${selector('item')}:not([data-disabled]):not([data-hidden])`)).filter((item) => isHTMLElement(item));
if (!enabledItems.length)
return;
if (e.key === kbd.ARROW_DOWN) {
highlightedItem.set(enabledItems[0]);
enabledItems[0].scrollIntoView({ block: get(scrollAlignment) });
}
else if (e.key === kbd.ARROW_UP) {
highlightedItem.set(last(enabledItems));
last(enabledItems).scrollIntoView({ block: get(scrollAlignment) });
}
});
}
/**
* When the menu is open...
*/
// Pressing `esc` should close the menu.
if (e.key === kbd.TAB || e.key === kbd.ESCAPE) {
closeMenu();
return;
}
// Pressing enter with a highlighted item should select it.
if (e.key === kbd.ENTER) {
e.preventDefault();
const $highlightedItem = get(highlightedItem);
if ($highlightedItem) {
selectItem($highlightedItem);
}
closeMenu();
}
// Pressing Alt + Up should close the menu.
if (e.key === kbd.ARROW_UP && e.altKey) {
closeMenu();
}
// Navigation (up, down, etc.) should change the highlighted item.
if (FIRST_LAST_KEYS.includes(e.key)) {
e.preventDefault();
// Get all the menu items.
const menuElement = document.getElementById(ids.menu);
if (!isHTMLElement(menuElement))
return;
const itemElements = getOptions(menuElement);
if (!itemElements.length)
return;
// Disabled items can't be highlighted. Skip them.
const candidateNodes = itemElements.filter((opt) => !isElementDisabled(opt) && opt.dataset.hidden === undefined);
// Get the index of the currently highlighted item.
const $currentItem = get(highlightedItem);
const currentIndex = $currentItem ? candidateNodes.indexOf($currentItem) : -1;
// Find the next menu item to highlight.
const $loop = get(loop);
const $scrollAlignment = get(scrollAlignment);
let nextItem;
switch (e.key) {
case kbd.ARROW_DOWN:
nextItem = next(candidateNodes, currentIndex, $loop);
break;
case kbd.ARROW_UP:
nextItem = prev(candidateNodes, currentIndex, $loop);
break;
case kbd.PAGE_DOWN:
nextItem = forward(candidateNodes, currentIndex, 10, $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;
}
// Highlight the new item and scroll it into view.
highlightedItem.set(nextItem);
nextItem.scrollIntoView({ block: $scrollAlignment });
}
}), addMeltEventListener(node, 'input', (e) => {
const unsubscribe = executeCallbacks(addMeltEventListener(node, 'input', (e) => {
if (!isHTMLInputElement(e.target))

@@ -304,3 +47,3 @@ return;

handler: () => {
closeMenu();
listbox.helpers.closeMenu();
},

@@ -311,4 +54,6 @@ });

}
const { destroy } = listbox.elements.trigger(node);
return {
destroy() {
destroy?.();
unsubscribe();

@@ -320,189 +65,19 @@ unsubEscapeKeydown();

});
/**
* Action and attributes for the menu element.
*/
const menu = builder(name('menu'), {
stores: [isVisible],
returned: ([$isVisible]) => {
return {
hidden: $isVisible ? undefined : true,
id: ids.menu,
role: 'listbox',
style: styleToString({ display: $isVisible ? undefined : 'none' }),
};
},
action: (node) => {
let unsubPopper = noop;
let unsubScroll = noop;
const unsubscribe = executeCallbacks(
// Bind the popper portal to the input element.
effect([
isVisible,
preventScroll,
closeOnEscape,
portal,
closeOnOutsideClick,
positioning,
activeTrigger,
], ([$isVisible, $preventScroll, $closeOnEscape, $portal, $closeOnOutsideClick, $positioning, $activeTrigger,]) => {
unsubPopper();
unsubScroll();
if (!$isVisible || !$activeTrigger)
return;
if ($preventScroll) {
unsubScroll = removeScroll();
}
const ignoreHandler = createClickOutsideIgnore(ids.input);
const popper = usePopper(node, {
anchorElement: $activeTrigger,
open,
options: {
floating: $positioning,
focusTrap: null,
clickOutside: $closeOnOutsideClick
? {
handler: (e) => {
const target = e.target;
if (target === $activeTrigger)
return;
closeMenu();
},
ignore: ignoreHandler,
}
: null,
escapeKeydown: $closeOnEscape
? {
handler: () => {
closeMenu();
},
}
: null,
portal: getPortalDestination(node, $portal),
},
});
if (popper && popper.destroy) {
unsubPopper = popper.destroy;
}
}));
return {
destroy: () => {
unsubscribe();
unsubPopper();
unsubScroll();
},
};
},
});
// Use our existing label builder to create a label for the combobox input.
const { elements: { root: labelBuilder }, } = createLabel();
const { action: labelAction } = get(labelBuilder);
const label = builder(name('label'), {
returned: () => {
return {
id: ids.label,
for: ids.input,
};
},
action: labelAction,
});
const option = builder(name('option'), {
stores: [selected],
returned: ([$selected]) => (props) => {
const selected = Array.isArray($selected)
? $selected.some((o) => deepEqual(o.value, props.value))
: deepEqual($selected?.value, props.value);
return {
'data-value': JSON.stringify(props.value),
'data-label': props.label,
'data-disabled': disabledAttr(props.disabled),
'aria-disabled': props.disabled ? true : undefined,
'aria-selected': selected,
'data-selected': selected ? '' : undefined,
id: generateId(),
role: 'option',
style: styleToString({ cursor: props.disabled ? 'default' : 'pointer' }),
};
},
action: (node) => {
const unsubscribe = executeCallbacks(addMeltEventListener(node, 'click', (e) => {
// If the item is disabled, `preventDefault` to stop the input losing focus.
if (isElementDisabled(node)) {
e.preventDefault();
return;
}
// Otherwise, select the item and close the menu.
selectItem(node);
closeMenu();
}));
return { destroy: unsubscribe };
},
});
const hiddenInput = builder(name('hidden-input'), {
stores: [selected],
returned: ([$selected]) => {
const value = Array.isArray($selected) ? $selected.map((o) => o.value) : $selected?.value;
return {
...hiddenInputAttrs,
value,
};
},
});
/* ------------------- */
/* LIFECYCLE & EFFECTS */
/* ------------------- */
onMount(() => {
if (!isBrowser)
return;
const menuEl = document.getElementById(ids.menu);
if (!menuEl)
return;
const triggerEl = getElementByMeltId(ids.input);
if (triggerEl) {
activeTrigger.set(triggerEl);
effect(listbox.states.open, ($open) => {
if (!$open) {
touchedInput.set(false);
}
const selectedEl = menuEl.querySelector('[data-selected]');
if (!isHTMLElement(selectedEl))
return;
});
/**
* Handles moving the `data-highlighted` attribute between items when
* the user moves their pointer or navigates with their keyboard.
*/
effect([highlightedItem], ([$highlightedItem]) => {
if (!isBrowser)
return;
const menuElement = document.getElementById(ids.menu);
if (!isHTMLElement(menuElement))
return;
getOptions(menuElement).forEach((node) => {
if (node === $highlightedItem) {
addHighlight(node);
}
else {
removeHighlight(node);
}
});
});
return {
ids,
...listbox,
elements: {
...listbox.elements,
input,
option,
menu,
label,
hiddenInput,
},
states: {
open,
selected,
highlighted,
...listbox.states,
touchedInput,
inputValue,
},
helpers: {
isSelected,
isHighlighted,
},
options,
};
}
import type { GroupedEvents, MeltComponentEvents } from '../../internal/types.js';
export declare const comboboxEvents: {
input: readonly ["click", "keydown", "input"];
trigger: readonly ["click", "keydown", "input"];
menu: readonly ["pointerleave"];

@@ -5,0 +6,0 @@ item: readonly ["pointermove", "click"];

@@ -0,5 +1,5 @@

import { listboxEvents } from '../listbox/events';
export const comboboxEvents = {
...listboxEvents,
input: ['click', 'keydown', 'input'],
menu: ['pointerleave'],
item: ['pointermove', 'click'],
};

@@ -1,116 +0,11 @@

import type { BuilderReturn, WhenTrue } from '../../internal/types.js';
import type { ChangeFn } from '../../internal/helpers/index.js';
import type { Writable } from 'svelte/store';
import type { BuilderReturn } from '../../internal/types.js';
import type { CreateListboxProps, ListboxSelected } from '../listbox/types.js';
import type { createCombobox } from './create.js';
import type { FloatingConfig } from '../../internal/actions/index.js';
export type { ComboboxComponentEvents } from './events.js';
export type ComboboxOption<Value> = {
value: Value;
label?: string;
};
export type ComboboxSelected<Multiple extends boolean, Value> = WhenTrue<Multiple, ComboboxOption<Value>[], ComboboxOption<Value>>;
export type CreateComboboxProps<Value, Multiple extends boolean = false, S extends ComboboxSelected<Multiple, Value> = ComboboxSelected<Multiple, Value>> = {
/**
* Options for positioning the popover menu.
*
* @default placement: 'bottom'
*/
positioning?: FloatingConfig;
/**
* Determines behavior when scrolling items into view.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#block
*/
scrollAlignment?: 'nearest' | 'center';
/**
* Whether or not the combobox should loop through the list when
* the end or beginning is reached.
*
* @default true
*/
loop?: boolean;
/**
* Whether or not the combobox should be open by default
* when the component is rendered.
*
* This should only be used when you are not passing a controlled `open` store.
*
* @default false
*/
defaultOpen?: boolean;
/**
* An optional controlled store that manages the open state of the combobox.
*/
open?: Writable<boolean>;
/**
* Change function that is called when the combobox's `open` state changes.
*
* @see https://melt-ui.com/docs/controlled#change-functions
*/
onOpenChange?: ChangeFn<boolean>;
/**
* The default selected option.
*
* This will be overridden if you also pass a `selected` store prop.
*
* @default undefined
*/
defaultSelected?: S;
/**
* An optional controlled store that manages the selected option of the combobox.
*/
selected?: Writable<S>;
/**
* A change handler for the selected store called when the selected would normally change.
*
* @see https://melt-ui.com/docs/controlled#change-functions
*/
onSelectedChange?: ChangeFn<S | undefined>;
/**
* Whether or not to close the combobox menu when the user clicks
* outside of the combobox.
*
* @default true
*/
closeOnOutsideClick?: boolean;
/**
* Whether or not to close the combobox menu when the user presses
* the escape key.
*
* @default true
*/
closeOnEscape?: boolean;
/**
* Whether or not to prevent scrolling the page when the
* combobox menu is open.
*
* @default true
*/
preventScroll?: boolean;
/**
* If not undefined, the combobox menu will be rendered within the provided element or selector.
*
* @default 'body'
*/
portal?: HTMLElement | string | null;
/**
* Whether the menu content should be displayed even if it is not open.
* This is useful for animating the content in and out using transitions.
*
* @see https://melt-ui.com/docs/transitions
*
* @default false
*/
forceVisible?: boolean;
multiple?: Multiple;
};
export type ComboboxOptionProps<Value> = ComboboxOption<Value> & {
/**
* Is the item disabled?
*/
disabled?: boolean;
};
export type Combobox<Value = unknown, Multiple extends boolean = false, S extends ComboboxSelected<Multiple, Value> = ComboboxSelected<Multiple, Value>> = BuilderReturn<typeof createCombobox<Value, Multiple, S>>;
export type ComboboxElements<Value = unknown, Multiple extends boolean = false, S extends ComboboxSelected<Multiple, Value> = ComboboxSelected<Multiple, Value>> = Combobox<Value, Multiple, S>['elements'];
export type ComboboxOptions<Value = unknown, Multiple extends boolean = false, S extends ComboboxSelected<Multiple, Value> = ComboboxSelected<Multiple, Value>> = Combobox<Value, Multiple, S>['options'];
export type ComboboxStates<Value = unknown, Multiple extends boolean = false, S extends ComboboxSelected<Multiple, Value> = ComboboxSelected<Multiple, Value>> = Combobox<Value, Multiple, S>['states'];
export type ComboboxHelpers<Value = unknown, Multiple extends boolean = false, S extends ComboboxSelected<Multiple, Value> = ComboboxSelected<Multiple, Value>> = Combobox<Value, Multiple, S>['helpers'];
export type { ListboxOption as ComboboxOption, ListboxSelected as ComboboxSelected, ListboxOptionProps as ComboboxOptionProps, } from '../listbox/types.js';
export type CreateComboboxProps<Value, Multiple extends boolean = false, S extends ListboxSelected<Multiple, Value> = ListboxSelected<Multiple, Value>> = Omit<CreateListboxProps<Value, Multiple, S>, 'builder' | 'typeahead'>;
export type Combobox<Value = unknown, Multiple extends boolean = false, S extends ListboxSelected<Multiple, Value> = ListboxSelected<Multiple, Value>> = BuilderReturn<typeof createCombobox<Value, Multiple, S>>;
export type ComboboxElements<Value = unknown, Multiple extends boolean = false, S extends ListboxSelected<Multiple, Value> = ListboxSelected<Multiple, Value>> = Combobox<Value, Multiple, S>['elements'];
export type ComboboxOptions<Value = unknown, Multiple extends boolean = false, S extends ListboxSelected<Multiple, Value> = ListboxSelected<Multiple, Value>> = Combobox<Value, Multiple, S>['options'];
export type ComboboxStates<Value = unknown, Multiple extends boolean = false, S extends ListboxSelected<Multiple, Value> = ListboxSelected<Multiple, Value>> = Combobox<Value, Multiple, S>['states'];
export type ComboboxHelpers<Value = unknown, Multiple extends boolean = false, S extends ListboxSelected<Multiple, Value> = ListboxSelected<Multiple, Value>> = Combobox<Value, Multiple, S>['helpers'];

@@ -1,2 +0,2 @@

import { addMeltEventListener, builder, createElHelpers, disabledAttr, executeCallbacks, generateId, hiddenInputAttrs, isBrowser, isHTMLElement, isHTMLInputElement, last, next, omit, overridable, prev, styleToString, toWritableStores, } from '../../internal/helpers/index.js';
import { addMeltEventListener, builder, createElHelpers, disabledAttr, executeCallbacks, generateId, hiddenInputAttrs, isBrowser, isHTMLElement, isHTMLInputElement, last, next, omit, overridable, prev, toWritableStores, } from '../../internal/helpers/index.js';
import { tick } from 'svelte';

@@ -3,0 +3,0 @@ import { derived, get, readonly, writable } from 'svelte/store';

@@ -57,7 +57,7 @@ /// <reference types="svelte" />

}>;
arrowSize: import("svelte/store").Writable<number>;
closeOnOutsideClick: import("svelte/store").Writable<boolean>;
closeOnEscape: import("svelte/store").Writable<boolean>;
arrowSize: import("svelte/store").Writable<number>;
disableFocusTrap: import("svelte/store").Writable<boolean>;
};
};

@@ -1,2 +0,2 @@

import { addEventListener, addMeltEventListener, builder, createElHelpers, disabledAttr, effect, executeCallbacks, getDirectionalKeys, getElemDirection, hiddenInputAttrs, isHTMLElement, kbd, omit, overridable, styleToString, toWritableStores, } from '../../internal/helpers/index.js';
import { addEventListener, addMeltEventListener, builder, createElHelpers, disabledAttr, effect, executeCallbacks, getDirectionalKeys, getElemDirection, hiddenInputAttrs, isHTMLElement, kbd, omit, overridable, toWritableStores, } from '../../internal/helpers/index.js';
import { onMount } from 'svelte';

@@ -3,0 +3,0 @@ import { derived, get, writable } from 'svelte/store';

/// <reference types="svelte" />
import type { MeltActionReturn } from '../../internal/types.js';
import type { SelectEvents } from './events.js';
import type { CreateSelectProps, SelectOptionProps, SelectSelected } from './types.js';
import type { CreateSelectProps, SelectSelected } from './types.js';
export declare function createSelect<Value = unknown, Multiple extends boolean = false, S extends SelectSelected<Multiple, Value> = SelectSelected<Multiple, Value>>(props?: CreateSelectProps<Value, Multiple, S>): {
ids: {
menu: string;
trigger: string;
label: string;
};
elements: {
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]) => {
style: string;
id: string;
group: import("../../internal/helpers/index.js").ExplicitBuilderReturn<import("svelte/store").Stores | undefined, import("svelte/action").Action<any, any, Record<never, any>>, () => (groupId: string) => {
role: string;
'aria-labelledby': string;
role: string;
'data-portal': string | undefined;
}, string>;
groupLabel: import("../../internal/helpers/index.js").ExplicitBuilderReturn<import("svelte/store").Stores | undefined, import("svelte/action").Action<any, any, Record<never, any>>, () => (groupId: string) => {
id: string;
}, string>;
trigger: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[{

@@ -23,31 +16,38 @@ update: (updater: import("svelte/store").Updater<boolean>, sideEffect?: ((newValue: boolean) => void) | undefined) => void;

subscribe(this: void, run: import("svelte/store").Subscriber<boolean>, invalidate?: import("svelte/store").Invalidator<boolean> | undefined): import("svelte/store").Unsubscriber;
}, import("svelte/store").Writable<boolean>, import("svelte/store").Writable<boolean>], (node: HTMLElement) => MeltActionReturn<SelectEvents['trigger']>, ([$open, $disabled, $required]: [boolean, boolean, boolean]) => {
readonly role: "combobox";
readonly type: "button";
readonly 'aria-autocomplete': "none";
readonly 'aria-haspopup': "listbox";
}, import("svelte/store").Writable<HTMLElement | null>, import("svelte/store").Writable<boolean>], (node: HTMLElement) => import("../../internal/types.js").MeltActionReturn<"input" | "click" | "keydown">, ([$open, $highlightedItem, $disabled]: [boolean, HTMLElement | null, boolean]) => {
readonly 'aria-activedescendant': string | undefined;
readonly 'aria-autocomplete': "list";
readonly 'aria-controls': string;
readonly 'aria-expanded': boolean;
readonly 'aria-required': boolean;
readonly 'data-state': "open" | "closed";
readonly 'data-disabled': true | undefined;
readonly 'aria-labelledby': string;
readonly disabled: true | undefined;
readonly 'data-melt-id': string;
readonly id: string;
readonly tabindex: 0;
readonly role: "combobox";
readonly disabled: true | undefined;
}, string>;
option: import("../../internal/helpers/index.js").ExplicitBuilderReturn<{
option: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[{
update: (updater: import("svelte/store").Updater<S | undefined>, sideEffect?: ((newValue: S | undefined) => void) | undefined) => void;
set: (this: void, value: S | undefined) => void;
subscribe(this: void, run: import("svelte/store").Subscriber<S | undefined>, invalidate?: import("svelte/store").Invalidator<S | undefined> | undefined): import("svelte/store").Unsubscriber;
}, (node: HTMLElement) => MeltActionReturn<SelectEvents['option']>, ($selected: S | undefined) => (props: SelectOptionProps<Value>) => {
readonly role: "option";
readonly 'aria-selected': boolean;
readonly 'data-selected': "" | undefined;
}], (node: HTMLElement) => import("../../internal/types.js").MeltActionReturn<"click" | "pointermove">, ([$selected]: [S | undefined]) => (props: import("./types.js").SelectOptionProps<unknown>) => {
readonly 'data-value': string;
readonly 'data-label': string | undefined;
readonly 'data-disabled': true | undefined;
readonly tabindex: -1;
readonly 'aria-disabled': true | undefined;
readonly 'aria-selected': boolean;
readonly 'data-selected': "" | undefined;
readonly id: string;
readonly role: "option";
readonly style: string;
}, string>;
menu: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[import("svelte/store").Readable<boolean>], (node: HTMLElement) => import("../../internal/types.js").MeltActionReturn<"pointerleave">, ([$isVisible]: [boolean]) => {
readonly hidden: true | undefined;
readonly id: string;
readonly role: "listbox";
readonly style: string;
}, string>;
label: import("../../internal/helpers/index.js").ExplicitBuilderReturn<import("svelte/store").Stores | undefined, (node: HTMLElement) => import("../../internal/types.js").MeltActionReturn<"mousedown">, () => {
id: string;
for: string;
}, string>;
hiddenInput: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[{

@@ -57,7 +57,6 @@ update: (updater: import("svelte/store").Updater<S | undefined>, sideEffect?: ((newValue: S | undefined) => void) | undefined) => void;

subscribe(this: void, run: import("svelte/store").Subscriber<S | undefined>, invalidate?: import("svelte/store").Invalidator<S | 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>>, ([$selected, $required, $disabled, $nameStore]: [S | undefined, boolean, boolean, string | undefined]) => {
}, import("svelte/store").Writable<boolean>, import("svelte/store").Writable<string | undefined>], import("svelte/action").Action<any, any, Record<never, any>>, ([$selected, $required, $name]: [S | undefined, boolean, string | undefined]) => {
required: boolean | undefined;
value: unknown;
name: string | undefined;
value: Value | Value[] | undefined;
required: boolean;
disabled: boolean | undefined;
type: string;

@@ -69,25 +68,9 @@ 'aria-hidden': boolean;

}, string>;
group: import("../../internal/helpers/index.js").ExplicitBuilderReturn<import("svelte/store").Stores | undefined, import("svelte/action").Action<any, any, Record<never, any>>, () => (groupId: string) => {
role: string;
'aria-labelledby': string;
}, string>;
groupLabel: import("../../internal/helpers/index.js").ExplicitBuilderReturn<import("svelte/store").Stores | undefined, import("svelte/action").Action<any, any, Record<never, any>>, () => (groupId: string) => {
id: string;
}, string>;
arrow: import("../../internal/helpers/index.js").ExplicitBuilderReturn<import("svelte/store").Writable<number>, import("svelte/action").Action<any, any, Record<never, any>>, ($arrowSize: number) => {
arrow: import("../../internal/helpers/index.js").ExplicitBuilderReturn<import("svelte/store").Writable<number | undefined>, import("svelte/action").Action<any, any, Record<never, any>>, ($arrowSize: number | undefined) => {
'data-arrow': boolean;
style: string;
}, string>;
separator: import("../../internal/helpers/index.js").ExplicitBuilderReturn<[import("svelte/store").Writable<import("../../internal/types.js").Orientation>, import("svelte/store").Writable<boolean>], import("svelte/action").Action<any, any, Record<never, any>>, ([$orientation, $decorative]: [import("../../internal/types.js").Orientation, boolean]) => {
role: string;
'aria-orientation': "vertical" | undefined;
'aria-hidden': boolean;
'data-orientation': import("../../internal/types.js").Orientation;
}, "separator">;
label: import("../../internal/helpers/index.js").ExplicitBuilderReturn<import("svelte/store").Stores | undefined, <Node_1 extends any>(node: Node_1) => MeltActionReturn<SelectEvents['label']>, () => {
id: string;
for: string;
}, string>;
};
states: {
selectedLabel: import("svelte/store").Readable<string>;
open: {

@@ -103,6 +86,13 @@ update: (updater: import("svelte/store").Updater<boolean>, sideEffect?: ((newValue: boolean) => void) | undefined) => void;

};
selectedLabel: import("svelte/store").Readable<string>;
highlighted: import("svelte/store").Readable<import("./types.js").SelectOption<unknown> | undefined>;
};
ids: {
trigger: string;
menu: string;
label: string;
};
helpers: {
isSelected: import("svelte/store").Readable<(value: Value) => boolean>;
isSelected: import("svelte/store").Readable<(value: unknown) => boolean>;
isHighlighted: import("svelte/store").Readable<(item: unknown) => boolean>;
closeMenu: () => void;
};

@@ -115,2 +105,3 @@ options: {

required: import("svelte/store").Writable<boolean>;
onOpenChange?: import("svelte/store").Writable<import("../../internal/helpers/index.js").ChangeFn<boolean> | undefined> | undefined;
preventScroll: import("svelte/store").Writable<boolean>;

@@ -122,7 +113,12 @@ portal: import("svelte/store").Writable<string | HTMLElement | null | undefined>;

}>;
arrowSize?: import("svelte/store").Writable<number | undefined> | undefined;
scrollAlignment: import("svelte/store").Writable<"center" | "nearest">;
loop: import("svelte/store").Writable<boolean>;
defaultSelected?: import("svelte/store").Writable<S | undefined> | undefined;
selected?: import("svelte/store").Writable<import("svelte/store").Writable<S> | undefined> | undefined;
onSelectedChange?: import("svelte/store").Writable<import("../../internal/helpers/index.js").ChangeFn<S | undefined> | undefined> | undefined;
closeOnOutsideClick: import("svelte/store").Writable<boolean>;
closeOnEscape: import("svelte/store").Writable<boolean>;
arrowSize: import("svelte/store").Writable<number>;
typeahead: import("svelte/store").Writable<boolean>;
};
};

@@ -1,367 +0,7 @@

import { createLabel, createSeparator } from '../index.js';
import { usePopper } from '../../internal/actions/index.js';
import { FIRST_LAST_KEYS, SELECTION_KEYS, addEventListener, addHighlight, addMeltEventListener, back, builder, createClickOutsideIgnore, createElHelpers, createTypeaheadSearch, derivedVisible, disabledAttr, effect, executeCallbacks, forward, generateId, getFirstOption, getNextFocusable, getOptions, getPortalDestination, getPreviousFocusable, handleRovingFocus, hiddenInputAttrs, isBrowser, isElementDisabled, isHTMLElement, kbd, last, next, noop, omit, overridable, prev, removeHighlight, removeScroll, sleep, styleToString, toWritableStores, toggle, } from '../../internal/helpers/index.js';
import { dequal as deepEqual } from 'dequal';
import { onMount, tick } from 'svelte';
import { derived, get, writable } from 'svelte/store';
import { getElementByMeltId } from '../../internal/helpers/builder';
const defaults = {
arrowSize: 8,
required: false,
disabled: false,
positioning: {
placement: 'bottom',
sameWidth: true,
},
preventScroll: true,
loop: false,
name: undefined,
defaultOpen: false,
forceVisible: false,
portal: undefined,
closeOnEscape: true,
closeOnOutsideClick: true,
};
import { builder, createElHelpers } from '../../internal/helpers/index.js';
import { derived } from 'svelte/store';
import { createListbox } from '../listbox/create.js';
const { name } = createElHelpers('select');
export function createSelect(props) {
const withDefaults = { ...defaults, ...props };
const options = toWritableStores({
...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;
const openWritable = withDefaults.open ?? writable(withDefaults.defaultOpen);
const open = overridable(openWritable, withDefaults?.onOpenChange);
const selectedWritable = withDefaults.selected ?? writable(withDefaults.defaultSelected);
const selected = overridable(selectedWritable, withDefaults?.onSelectedChange);
const activeTrigger = writable(null);
/**
* Keeps track of the next/previous focusable element when the menu closes.
* This is because we are portaling the menu to the body and we need
* to be able to focus the next element in the DOM when the menu closes.
*
* Without keeping track of this, the focus would be reset to the top of
* the page (or the first focusable element in the body).
*/
const nextFocusable = writable(null);
const prevFocusable = writable(null);
/**
* Keeps track of if the user is using the keyboard to navigate the menu.
* This is used to determine how we handle focus on open behavior differently
* than when the user is using the mouse.
*/
let isUsingKeyboard = false;
const ids = {
menu: generateId(),
trigger: generateId(),
label: generateId(),
};
const { typed, handleTypeaheadSearch } = createTypeaheadSearch();
/* ------- */
/* Helpers */
/* ------- */
const isSelected = derived([selected], ([$selected]) => {
return (value) => {
if (Array.isArray($selected)) {
return $selected.some((o) => deepEqual(o.value, value));
}
return deepEqual($selected?.value, 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 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);
}
}
}
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 ?? '';
});
/* -------- */
/* Builders */
/* -------- */
const menu = builder(name('menu'), {
stores: [isVisible, portal],
returned: ([$isVisible, $portal]) => {
return {
style: styleToString({
display: $isVisible ? undefined : 'none',
}),
id: ids.menu,
'aria-labelledby': ids.trigger,
role: 'listbox',
'data-portal': $portal ? '' : undefined,
};
},
action: (node) => {
let unsubPopper = noop;
let unsubScroll = noop;
const unsubDerived = effect([isVisible, preventScroll, positioning, portal, closeOnEscape, closeOnOutsideClick], ([$isVisible, $preventScroll, $positioning, $portal, $closeOnEscape, $closeOnOutsideClick,]) => {
unsubPopper();
unsubScroll();
const $activeTrigger = get(activeTrigger);
if (!($isVisible && $activeTrigger))
return;
if ($preventScroll) {
unsubScroll = removeScroll();
}
const ignoreHandler = createClickOutsideIgnore(ids.trigger);
tick().then(() => {
const popper = usePopper(node, {
anchorElement: $activeTrigger,
open,
options: {
floating: $positioning,
clickOutside: $closeOnOutsideClick
? {
ignore: ignoreHandler,
}
: null,
escapeKeydown: $closeOnEscape
? {
handler: () => {
open.set(false);
},
}
: null,
portal: getPortalDestination(node, $portal),
},
});
if (popper && popper.destroy) {
unsubPopper = popper.destroy;
}
});
});
const unsubEventListeners = executeCallbacks(addMeltEventListener(node, 'keydown', (e) => {
const menuEl = e.currentTarget;
const target = e.target;
if (!isHTMLElement(menuEl) || !isHTMLElement(target))
return;
const isModifierKey = e.ctrlKey || e.altKey || e.metaKey;
const isCharacterKey = e.key.length === 1;
if (e.key === kbd.TAB) {
e.preventDefault();
open.set(false);
handleTabNavigation(e);
}
if (FIRST_LAST_KEYS.includes(e.key)) {
e.preventDefault();
if (menuEl === target) {
const selectedOption = getSelectedOption(menuEl);
if (selectedOption) {
handleRovingFocus(selectedOption);
return;
}
}
handleMenuNavigation(e);
}
if (!isModifierKey && isCharacterKey) {
handleTypeaheadSearch(e.key, getOptions(node));
}
}));
return {
destroy() {
unsubDerived();
unsubPopper();
unsubScroll();
unsubEventListeners();
},
};
},
});
const trigger = builder(name('trigger'), {
stores: [open, disabled, required],
returned: ([$open, $disabled, $required]) => {
return {
role: 'combobox',
type: 'button',
'aria-autocomplete': 'none',
'aria-haspopup': 'listbox',
'aria-controls': ids.menu,
'aria-expanded': $open,
'aria-required': $required,
'data-state': $open ? 'open' : 'closed',
'data-disabled': disabledAttr($disabled),
'aria-labelledby': ids.label,
disabled: disabledAttr($disabled),
'data-melt-id': ids.trigger,
id: ids.trigger,
tabindex: 0,
};
},
action: (node) => {
const unsub = executeCallbacks(addMeltEventListener(node, 'click', (e) => {
if (get(disabled)) {
e.preventDefault();
return;
}
const $open = get(open);
const triggerEl = e.currentTarget;
if (!isHTMLElement(triggerEl))
return;
open.update((prev) => {
const isOpen = !prev;
if (isOpen) {
nextFocusable.set(getNextFocusable(triggerEl));
prevFocusable.set(getPreviousFocusable(triggerEl));
activeTrigger.set(triggerEl);
}
return isOpen;
});
if (!$open)
e.preventDefault();
}), addMeltEventListener(node, 'keydown', (e) => {
const triggerEl = e.currentTarget;
if (!isHTMLElement(triggerEl))
return;
if (SELECTION_KEYS.includes(e.key) ||
e.key === kbd.ARROW_DOWN ||
e.key === kbd.ARROW_UP) {
if (e.key === kbd.ARROW_DOWN || e.key === kbd.ARROW_UP) {
/**
* We don't want to scroll the page when the user presses the
* down arrow when focused on the trigger, so we prevent that
* default behavior.
*/
e.preventDefault();
}
open.update((prev) => {
const isOpen = !prev;
if (isOpen) {
e.preventDefault();
nextFocusable.set(getNextFocusable(triggerEl));
prevFocusable.set(getPreviousFocusable(triggerEl));
activeTrigger.set(triggerEl);
}
return isOpen;
});
const menu = document.getElementById(ids.menu);
if (!menu)
return;
const selectedOption = menu.querySelector('[data-selected]');
if (isHTMLElement(selectedOption)) {
handleRovingFocus(selectedOption);
return;
}
const options = getOptions(menu);
if (!options.length)
return;
handleRovingFocus(options[0]);
}
}));
return {
destroy: unsub,
};
},
});
// Use our existing label builder to create a label for the select trigger.
const { elements: { root: labelBuilder }, } = createLabel();
const { action: labelAction } = get(labelBuilder);
const label = builder(name('label'), {
returned: () => {
return {
id: ids.label,
for: ids.trigger,
};
},
action: (node) => {
const destroy = executeCallbacks(labelAction(node).destroy ?? noop, addMeltEventListener(node, 'click', (e) => {
e.preventDefault();
const triggerEl = document.getElementById(ids.trigger);
if (!isHTMLElement(triggerEl))
return;
triggerEl.focus();
}));
return {
destroy,
};
},
});
const { elements: { root: separator }, } = createSeparator({
decorative: true,
});
const listbox = createListbox({ ...props, builder: 'select' });
const group = builder(name('group'), {

@@ -382,219 +22,20 @@ returned: () => {

});
const arrow = builder(name('arrow'), {
stores: arrowSize,
returned: ($arrowSize) => ({
'data-arrow': true,
style: styleToString({
position: 'absolute',
width: `var(--arrow-size, ${$arrowSize}px)`,
height: `var(--arrow-size, ${$arrowSize}px)`,
}),
}),
});
const getOptionProps = (el) => {
const value = el.getAttribute('data-value');
const label = el.getAttribute('data-label');
const disabled = el.hasAttribute('data-disabled');
return {
value: value ? JSON.parse(value) : value,
label: label ?? el.textContent ?? undefined,
disabled: disabled ? true : false,
};
};
const setOption = (newOption) => {
selected.update(($option) => {
const $multiple = get(multiple);
if ($multiple) {
const optionArr = Array.isArray($option) ? $option : [];
return toggle(newOption, optionArr, (itemA, itemB) => deepEqual(itemA.value, itemB.value));
}
return newOption;
});
};
const option = builder(name('option'), {
stores: selected,
returned: ($selected) => {
return (props) => {
const isSelected = Array.isArray($selected)
? $selected.some((o) => deepEqual(o.value, props.value))
: deepEqual($selected?.value, props?.value);
return {
role: 'option',
'aria-selected': isSelected,
'data-selected': isSelected ? '' : undefined,
'data-value': JSON.stringify(props.value),
'data-label': props.label ?? undefined,
'data-disabled': disabledAttr(props.disabled),
tabindex: -1,
};
};
},
action: (node) => {
const unsub = executeCallbacks(addMeltEventListener(node, 'click', (e) => {
const itemElement = e.currentTarget;
if (!isHTMLElement(itemElement))
return;
const props = getOptionProps(node);
if (props.disabled) {
e.preventDefault();
return;
}
handleRovingFocus(itemElement);
setOption(props);
const $multiple = get(multiple);
if (!$multiple)
open.set(false);
}), addMeltEventListener(node, 'keydown', (e) => {
const $typed = get(typed);
const isTypingAhead = $typed.length > 0;
if (isTypingAhead && e.key === kbd.SPACE) {
e.preventDefault();
return;
}
if (e.key === kbd.ENTER || e.key === kbd.SPACE) {
e.preventDefault();
const props = getOptionProps(node);
node.setAttribute('data-selected', '');
setOption(props);
const $multiple = get(multiple);
if (!$multiple)
open.set(false);
}
}), addMeltEventListener(node, 'pointermove', (e) => {
const props = getOptionProps(node);
if (props.disabled) {
e.preventDefault();
return;
}
const itemEl = e.currentTarget;
if (!isHTMLElement(itemEl))
return;
if (props.disabled) {
const menuElement = document.getElementById(ids.menu);
if (!menuElement)
return;
handleRovingFocus(menuElement);
}
onOptionPointerMove(e);
}), addMeltEventListener(node, 'pointerleave', (e) => {
if (!isMouse(e))
return;
onOptionLeave();
}), addMeltEventListener(node, 'focusin', (e) => {
const itemEl = e.currentTarget;
if (!isHTMLElement(itemEl))
return;
addHighlight(itemEl);
}), addMeltEventListener(node, 'focusout', (e) => {
const itemEl = e.currentTarget;
if (!isHTMLElement(itemEl))
return;
removeHighlight(itemEl);
}));
return {
destroy: unsub,
};
},
});
const hiddenInput = builder(name('hidden-input'), {
stores: [selected, required, disabled, nameStore],
returned: ([$selected, $required, $disabled, $nameStore]) => {
const value = Array.isArray($selected) ? $selected.map((o) => o.value) : $selected?.value;
return {
...hiddenInputAttrs,
name: $nameStore,
value,
required: $required,
disabled: disabledAttr($disabled),
};
},
});
/* ------------------- */
/* Lifecycle & Effects */
/* ------------------- */
onMount(() => {
const triggerEl = getElementByMeltId(ids.trigger);
if (triggerEl) {
activeTrigger.set(triggerEl);
const selectedLabel = derived(listbox.states.selected, ($selected) => {
if (Array.isArray($selected)) {
return $selected.map((o) => o.label).join(', ');
}
return $selected?.label ?? '';
});
let hasOpened = false;
effect(open, ($open) => {
if ($open) {
hasOpened = true;
}
});
effect([open, activeTrigger], function handleFocus([$open, $activeTrigger]) {
const unsubs = [];
if (!isBrowser)
return;
if ($open && get(preventScroll)) {
unsubs.push(removeScroll());
}
sleep(1).then(() => {
const menuEl = document.getElementById(ids.menu);
if (menuEl && $open && isUsingKeyboard) {
// Focus on selected option or first option
const selectedOption = getSelectedOption(menuEl);
if (!selectedOption) {
const firstOption = getFirstOption(menuEl);
if (!firstOption)
return;
handleRovingFocus(firstOption);
}
else {
handleRovingFocus(selectedOption);
}
}
else if (menuEl && $open) {
// focus on the menu element
handleRovingFocus(menuEl);
}
else if ($activeTrigger && hasOpened) {
// Hacky way to prevent the keydown event from triggering on the trigger
handleRovingFocus($activeTrigger);
}
});
return () => {
unsubs.forEach((unsub) => unsub());
};
});
effect([open, activeTrigger], ([$open, $activeTrigger]) => {
if (!isBrowser)
return;
const handlePointer = () => (isUsingKeyboard = false);
const handleKeyDown = (e) => {
isUsingKeyboard = true;
if (e.key === kbd.ESCAPE && $open) {
open.set(false);
if (!$activeTrigger)
return;
handleRovingFocus($activeTrigger);
}
};
return executeCallbacks(addEventListener(document, 'keydown', handleKeyDown, { capture: true }), addEventListener(document, 'pointerdown', handlePointer, { capture: true, once: true }), addEventListener(document, 'pointermove', handlePointer, { capture: true, once: true }));
});
return {
ids,
...listbox,
elements: {
menu,
trigger,
option,
hiddenInput,
...listbox.elements,
group,
groupLabel,
arrow,
separator,
label,
},
states: {
open,
selected,
...listbox.states,
selectedLabel,
},
helpers: {
isSelected,
},
options,
};
}

@@ -1,125 +0,11 @@

import type { FloatingConfig } from '../../internal/actions/index.js';
import type { BuilderReturn, WhenTrue } from '../../internal/types.js';
import type { Writable } from 'svelte/store';
import type { BuilderReturn } from '../../internal/types.js';
import type { CreateListboxProps, ListboxSelected } from '../listbox/types.js';
import type { createSelect } from './create.js';
import type { ChangeFn } from '../../internal/helpers/index.js';
export type { SelectComponentEvents } from './events.js';
export type SelectOption<Value> = {
value: Value;
label?: string;
};
export type SelectSelected<Multiple extends boolean, Value> = WhenTrue<Multiple, SelectOption<Value>[], SelectOption<Value>>;
export type CreateSelectProps<Value = unknown, Multiple extends boolean = false, S extends SelectSelected<Multiple, Value> = SelectSelected<Multiple, Value>> = {
/**
* Options for positioning the popover menu.
*
* @default placement: 'bottom'
*/
positioning?: FloatingConfig;
/**
* The size of the arrow in pixels.
* @default 8
*/
arrowSize?: number;
/**
* Whether or not the select is required.
*
* @default false
*/
required?: boolean;
/**
* Whether or not the select input is disabled.
*
* @default false
*/
disabled?: boolean;
/**
* An optional controlled store that manages the open state of the select menu.
*/
defaultOpen?: boolean;
/**
* A controlled open state store for the menu. If provided, the
* value of this store will override the `defaultOpen` prop.
*
* @default Writable<false>
*/
open?: Writable<boolean>;
/**
* A callback for when the open state changes.
*
* @see https://melt-ui.com/docs/controlled#change-functions
*/
onOpenChange?: ChangeFn<boolean>;
/**
* The name for the select input.
*
* @default undefined
*/
name?: string;
/**
* Whether or not to prevent scrolling the body when the menu is open.
*
* @default true
*/
preventScroll?: boolean;
/**
* Whether or not to loop through the menu options once the end or beginning is reached.
*/
loop?: boolean;
/**
* Whether or not to close the select menu when the user presses
* the escape key.
*
* @default true
*/
closeOnEscape?: boolean;
/**
* Whether or not to close the select menu when the user clicks
* outside of the menu.
*
* @default true
*/
closeOnOutsideClick?: boolean;
/**
* If not undefined, the select menu will be rendered within the provided element or selector.
*
* @default 'body'
*/
portal?: HTMLElement | string | null;
/**
* Whether the menu content should be displayed even if it is not open.
* This is useful for animating the content in and out using transitions.
*
* @see https://melt-ui.com/docs/transitions
*
* @default false
*/
forceVisible?: boolean;
/**
* The default value set on the select input.
*
* This will be overridden if you also pass a `value` store prop.
*
* @default undefined
*/
defaultSelected?: S;
/**
* An optional controlled store that manages the value state of the combobox.
*/
selected?: Writable<S | undefined>;
/**
* A change handler for the value store called when the value would normally change.
*
* @see https://melt-ui.com/docs/controlled#change-functions
*/
onSelectedChange?: ChangeFn<S | undefined>;
multiple?: Multiple;
};
export type SelectOptionProps<Value = unknown> = SelectOption<Value> & {
disabled?: boolean;
};
export type Select<Value = unknown, Multiple extends boolean = false, S extends SelectSelected<Multiple, Value> = SelectSelected<Multiple, Value>> = BuilderReturn<typeof createSelect<Value, Multiple, S>>;
export type SelectElements<Value = unknown, Multiple extends boolean = false, S extends SelectSelected<Multiple, Value> = SelectSelected<Multiple, Value>> = BuilderReturn<typeof createSelect<Value, Multiple, S>>['elements'];
export type SelectOptions<Value = unknown, Multiple extends boolean = false, S extends SelectSelected<Multiple, Value> = SelectSelected<Multiple, Value>> = BuilderReturn<typeof createSelect<Value, Multiple, S>>['options'];
export type SelectStates<Value = unknown, Multiple extends boolean = false, S extends SelectSelected<Multiple, Value> = SelectSelected<Multiple, Value>> = BuilderReturn<typeof createSelect<Value, Multiple, S>>['states'];
export type SelectHelpers<Value = unknown, Multiple extends boolean = false, S extends SelectSelected<Multiple, Value> = SelectSelected<Multiple, Value>> = BuilderReturn<typeof createSelect<Value, Multiple, S>>['helpers'];
export type { ListboxOption as SelectOption, ListboxSelected as SelectSelected, ListboxOptionProps as SelectOptionProps, } from '../listbox/types.js';
export type CreateSelectProps<Value = unknown, Multiple extends boolean = false, S extends ListboxSelected<Multiple, Value> = ListboxSelected<Multiple, Value>> = Omit<CreateListboxProps<Value, Multiple, S>, 'builder'>;
export type Select<Value = unknown, Multiple extends boolean = false, S extends ListboxSelected<Multiple, Value> = ListboxSelected<Multiple, Value>> = BuilderReturn<typeof createSelect<Value, Multiple, S>>;
export type SelectElements<Value = unknown, Multiple extends boolean = false, S extends ListboxSelected<Multiple, Value> = ListboxSelected<Multiple, Value>> = Select<Value, Multiple, S>['elements'];
export type SelectOptions<Value = unknown, Multiple extends boolean = false, S extends ListboxSelected<Multiple, Value> = ListboxSelected<Multiple, Value>> = Select<Value, Multiple, S>['options'];
export type SelectStates<Value = unknown, Multiple extends boolean = false, S extends ListboxSelected<Multiple, Value> = ListboxSelected<Multiple, Value>> = Select<Value, Multiple, S>['states'];
export type SelectHelpers<Value = unknown, Multiple extends boolean = false, S extends ListboxSelected<Multiple, Value> = ListboxSelected<Multiple, Value>> = Select<Value, Multiple, S>['helpers'];

@@ -44,4 +44,4 @@ /// <reference types="svelte" />

}>;
arrowSize: Writable<number>;
closeOnEscape: Writable<boolean>;
arrowSize: Writable<number>;
group: Writable<string | boolean | undefined>;

@@ -48,0 +48,0 @@ openDelay: Writable<number>;

@@ -8,2 +8,8 @@ import { type Writable } from 'svelte/store';

onMatch?: (element: HTMLElement) => void;
/**
* Get the current item, usually the active element.
* @returns The current item
* @default () => document.activeElement
*/
getCurrentItem?: () => Element | null | undefined;
};

@@ -10,0 +16,0 @@ export type HandleTypeaheadSearch = {};

@@ -13,2 +13,3 @@ import { get, writable } from 'svelte/store';

onMatch: handleRovingFocus,
getCurrentItem: () => document.activeElement,
};

@@ -22,5 +23,3 @@ export function createTypeaheadSearch(args = {}) {

const handleTypeaheadSearch = (key, items) => {
const currentItem = document.activeElement;
if (!isHTMLElement(currentItem))
return;
const currentItem = withDefaults.getCurrentItem();
const $typed = get(typed);

@@ -31,3 +30,3 @@ if (!Array.isArray($typed)) {

$typed.push(key.toLowerCase());
typed.update(() => $typed);
typed.set($typed);
const candidateItems = items.filter((item) => {

@@ -43,3 +42,3 @@ if (item.getAttribute('disabled') === 'true' ||

const normalizeSearch = isRepeated ? $typed[0] : $typed.join('');
const currentItemIndex = currentItem ? candidateItems.indexOf(currentItem) : -1;
const currentItemIndex = isHTMLElement(currentItem) ? candidateItems.indexOf(currentItem) : -1;
let wrappedItems = wrapArray(candidateItems, Math.max(currentItemIndex, 0));

@@ -50,3 +49,3 @@ const excludeCurrentItem = normalizeSearch.length === 1;

}
const nextItem = wrappedItems.find((item) => item.innerText.toLowerCase().startsWith(normalizeSearch.toLowerCase()));
const nextItem = wrappedItems.find((item) => item?.innerText && item.innerText.toLowerCase().startsWith(normalizeSearch.toLowerCase()));
if (isHTMLElement(nextItem) && nextItem !== currentItem) {

@@ -53,0 +52,0 @@ withDefaults.onMatch(nextItem);

{
"name": "@melt-ui/svelte",
"version": "0.54.1",
"version": "0.55.0",
"license": "MIT",

@@ -5,0 +5,0 @@ "repository": "github:melt-ui/melt-ui",

@@ -1,4 +0,2 @@

<h1 align="center">
<img align="center" src="https://raw.githubusercontent.com/melt-ui/melt-ui/main/static/banner.png" />
</h1>
![](static/banner.png)

@@ -63,2 +61,7 @@ [Melt UI](https://www.melt-ui.com/) is a set of headless, accessible component builders for Svelte.

Melt UI is an open-source project built by the community for the community. It wouldn't be possible
if it wasn't for the work of some amazing people.
[![Contributors](https://contrib.rocks/image?repo=melt-ui/melt-ui)](<[https://github.com/codemaniac-sahil/news-webapp-api](https://github.com/melt-ui/melt-ui)https://github.com/melt-ui/melt-ui/graphs/contributors>)
## Community

@@ -65,0 +68,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc