Repository: melt-ui/melt-ui (ts)

Issue:
🐛Bug: Key events in Select don't work in Safari
**Describe the bug**
Select does not respond to `esc` to close or arrow up and down to move through select options

**To Reproduce**
Steps to reproduce the behavior:
1. Open Select
2. Press `esc` to close it
3. Press `arrow up` to move up in options
4. Press `arrow down` to move down in options

**Expected behavior**
Should close on `esc` and Options should be traversed through when arrow up and down keys are used

**Desktop (please complete the following information):**
 - OS: macOS 14.1.1
 - Browser: Safari
 - Version 17.1

Pull request description:
Fix: Key events in Select don't work in Safari
closes #740

Patch:
diff --git a/.changeset/purple-pumpkins-sit.md b/.changeset/purple-pumpkins-sit.md
new file mode 100644
index 00000000..f6d301e1
--- /dev/null
+++ b/.changeset/purple-pumpkins-sit.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": patch
+---
+
+Fix: Key events in Select don't work in Safari
diff --git a/.changeset/rude-glasses-reflect.md b/.changeset/rude-glasses-reflect.md
new file mode 100644
index 00000000..8a31dd2d
--- /dev/null
+++ b/.changeset/rude-glasses-reflect.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": patch
+---
+
+fix: space to select select options
diff --git a/.changeset/tricky-readers-bow.md b/.changeset/tricky-readers-bow.md
new file mode 100644
index 00000000..827913e5
--- /dev/null
+++ b/.changeset/tricky-readers-bow.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": patch
+---
+
+fix: Select - toggle when elements within trigger are clicked
diff --git a/src/docs/content/builders/dialog.md b/src/docs/content/builders/dialog.md
index 9a4369ea..a05f4fd7 100644
--- a/src/docs/content/builders/dialog.md
+++ b/src/docs/content/builders/dialog.md
@@ -21,7 +21,7 @@ At a high level, the anatomy of a dialog looks like this:
 <script lang="ts">
 	import { createDialog, melt } from '@melt-ui/svelte'
 	const {
-		elements: { trigger, portalled, overlay, content, title, description, close, open }
+		elements: { trigger, portalled, overlay, content, title, description, close }
 	} = createDialog()
 </script>
 
@@ -143,7 +143,7 @@ Dialogs can be nested. For example, here's a dialog that opens another dialog.
 ## Accessibility
 
 Adheres to the
-[Dialog WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) &
+[Dialog WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) and
 [Alert Dialog WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/)
 
 <KbdTable {keyboard} />
diff --git a/src/docs/content/builders/dropdown-menu.md b/src/docs/content/builders/dropdown-menu.md
index 1365443b..56a9b899 100644
--- a/src/docs/content/builders/dropdown-menu.md
+++ b/src/docs/content/builders/dropdown-menu.md
@@ -90,7 +90,7 @@ occurring.
 
 ## Accessibility
 
-Adheres to the [Menu WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) &
+Adheres to the [Menu WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) and
 [Menu Button WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubutton/)
 
 <KbdTable {keyboard} />
diff --git a/src/docs/content/builders/menubar.md b/src/docs/content/builders/menubar.md
index 1eaecc24..b80286c3 100644
--- a/src/docs/content/builders/menubar.md
+++ b/src/docs/content/builders/menubar.md
@@ -121,7 +121,7 @@ to use it.
 
 ## Accessibility
 
-Adheres to the [Menu WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) &
+Adheres to the [Menu WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) and
 [Menu Button WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubutton/)
 
 <KbdTable {keyboard} />
diff --git a/src/docs/content/builders/pin-input.md b/src/docs/content/builders/pin-input.md
index b385ee66..4e667999 100644
--- a/src/docs/content/builders/pin-input.md
+++ b/src/docs/content/builders/pin-input.md
@@ -1,5 +1,5 @@
 ---
-title: Pin Input
+title: PIN Input
 description: A sequence of one-character alphanumeric inputs.
 ---
 
diff --git a/src/docs/content/controlled.md b/src/docs/content/controlled.md
index 3f088fbc..53ade692 100644
--- a/src/docs/content/controlled.md
+++ b/src/docs/content/controlled.md
@@ -10,14 +10,14 @@ description: Melt UI components are uncontrolled by default, but offer the abili
 <Callout type="warning">
 
 Controlled components are an optional advanced feature and should only be used when necessary. If
-you're unsure if you should be using the components in a controlled way, you likely don't.
+you're unsure if you should be using the components in a controlled way, you likely don't need to.
 
 </Callout>
 
 ## Controlled vs. Uncontrolled
 
 There are various definitions for controlled/uncontrolled components, but in the context of Melt UI,
-uncontrolled means that the state and stores of a component are created and managed by us.
+uncontrolled means that the state and stores of a component are created and managed automatically.
 Controlled means that the user (you) can create and manage the state and stores.
 
 All components are uncontrolled by default, and in most cases, this is more than enough. However,
@@ -32,7 +32,7 @@ available.
 
 </Callout>
 
-## Modify writable states
+## Modify Writable States
 
 By default, we provide an `open` store from the `Dialog` builder that will have its state updated
 whenever a `trigger` or `close` element is pressed.
diff --git a/src/docs/content/introduction.md b/src/docs/content/introduction.md
index de5fa85c..9659f4a7 100644
--- a/src/docs/content/introduction.md
+++ b/src/docs/content/introduction.md
@@ -1,7 +1,7 @@
 ---
 title: Introduction
 description:
-  An open-source collection of accessible & customizable component builders for creating user
+  An open-source collection of accessible and customizable component builders for creating user
   interfaces with Svelte.
 ---
 
@@ -49,7 +49,7 @@ Melt UI puts accessibility front and center, making sure your UI components are
 user-friendly. We follow [WAI-ARIA design patterns](https://www.w3.org/WAI/ARIA/apg/) and take care
 of all the nitty-gritty details like aria attributes, role management, focus handling, and keyboard
 navigation. Each builder's page includes a section on accessibility, with references to relevant
-WAI-ARIA guidelines & instructions for using the builder in an accessible manner.
+WAI-ARIA guidelines and instructions for using the builder in an accessible manner.
 
 ### Bring Your Own Styles
 
@@ -57,7 +57,7 @@ The builders ship with zero predefined styles, allowing you to customize them to
 integrate with your application's design system. Whether you prefer vanilla CSS, CSS preprocessors,
 or CSS-in-JS libraries, our components work harmoniously with your preferred styling solution.
 
-### Open & Extensible
+### Open and Extensible
 
 Melt UI embraces your desire for customization. The builder architecture is open and flexible,
 allowing you to tinker with every aspect of the components. Wrap them, extend their functionality,
@@ -70,9 +70,9 @@ freeing you from the burden of managing local states. However, if you prefer gre
 can switch to controlled components. We handle the behind-the-scenes complexity, ensuring a smooth
 developer experience.
 
-### TypeScript & SvelteKit Support
+### TypeScript and SvelteKit Support
 
-Melt UI offers a fully-typed API, safeguarding your code while providing autocompletion superpowers
+Melt UI offers a fully typed API, safeguarding your code while providing autocompletion superpowers
 in your preferred code editor. All our components share a consistent API, promoting familiarity and
 minimizing surprises. The components are also build with SSR in mind, making them a perfect fit for
 SvelteKit.
@@ -84,13 +84,13 @@ if it wasn't for the amazing work of some amazing people.
 
 <Contributors />
 
-## Inspiration & Credits
+## Inspiration and Credits
 
-We're big fans of open source and love working collaboratively. Over time, we've taken cues &
+We're big fans of open source and love working collaboratively. Over time, we've taken cues and
 inspiration from some really amazing projects and people that have motivated us to continuously
 improve our ideas.
 
-Some of the project's we've been inspired by in no particular order:
+Some of the projects we've been inspired by in no particular order:
 
 - Zag - [https://zagjs.com](https://zagjs.com)
 - Radix UI - [https://radix-ui.com](https://radix-ui.com)
diff --git a/src/docs/content/preprocessor.md b/src/docs/content/preprocessor.md
index 9816b495..5d8833dd 100644
--- a/src/docs/content/preprocessor.md
+++ b/src/docs/content/preprocessor.md
@@ -7,12 +7,12 @@ description: Simplifying the syntax of Melt UI using a custom preprocessor.
 
 We provide a custom preprocessor that aims to enhance the DX of Melt UI just a bit further. It
 introduces a new action, `melt`, that accepts our builders as values. This helps trim down on the
-boilerplate just enough to keep the markup nice and tidy. The PP will then transform our special
+boilerplate just enough to keep the markup nice and tidy. The preprocessor will then transform our special
 attribute into the proper Svelte syntax.
 
 ## How it works
 
-In the most simplest form, the PP will take the following code:
+In the simplest form, the preprocessor will take the following code:
 
 ```svelte
 <script>
diff --git a/src/docs/content/transitions.md b/src/docs/content/transitions.md
index 5caa38b7..792e530b 100644
--- a/src/docs/content/transitions.md
+++ b/src/docs/content/transitions.md
@@ -1,9 +1,9 @@
 ---
 title: Transitions
-description: Learn how to use Svelte & other custom transitions and animations with Melt.
+description: Learn how to use Svelte and other custom transitions and animations with Melt.
 ---
 
-By default, the visibility of most elements you'd want to apply transitions to are controlled by
+By default, the visibility of most elements you'd want to apply transitions to is controlled by
 Melt internally. We set the element's `hidden` attribute to `true`, and the display to `none` when
 the open state is false. We do this to prevent flickering when the component is first rendered.
 
diff --git a/src/docs/content/usage.md b/src/docs/content/usage.md
index 30b6faf1..c300d0e0 100644
--- a/src/docs/content/usage.md
+++ b/src/docs/content/usage.md
@@ -82,8 +82,8 @@ Melt's props, it's all good.
 
 ### Animations
 
-By passing the builder returned props to an element, certain data and aria attributes will
-automatically be added/and or altered for you. These changes can then be used for animation
+By passing the builder's returned props to an element, certain data and aria attributes will
+automatically be added and/or altered for you. These changes can then be used for animation
 purposes, should you desire to do so.
 
 ```svelte
diff --git a/src/docs/data/builders/dialog.ts b/src/docs/data/builders/dialog.ts
index a77d66d8..800eabfd 100644
--- a/src/docs/data/builders/dialog.ts
+++ b/src/docs/data/builders/dialog.ts
@@ -35,6 +35,10 @@ const builder = builderSchema(BUILDER_NAME, {
 			name: 'trigger',
 			description: 'The builder store used to create the dialog trigger.',
 		},
+		{
+			name: 'portalled',
+			description: 'The builder store used to create the portalled dialog container.',
+		},
 		{
 			name: 'overlay',
 			description: 'The builder store used to create the dialog overlay.',
diff --git a/src/docs/data/builders/index.ts b/src/docs/data/builders/index.ts
index 7e5d39fb..fdc8af7d 100644
--- a/src/docs/data/builders/index.ts
+++ b/src/docs/data/builders/index.ts
@@ -53,7 +53,7 @@ export const builderList = [
 	'link-preview',
 	'menubar',
 	'pagination',
-	'pin-input',
+	'PIN-input',
 	'popover',
 	'progress',
 	'radio-group',
@@ -105,7 +105,7 @@ export const data: Builders = {
 	'link-preview': linkPreviewData,
 	menubar: menubarData,
 	pagination: paginationData,
-	'pin-input': pinInputData,
+	'PIN-input': pinInputData,
 	popover: popoverData,
 	progress: progressData,
 	'radio-group': radioGroupData,
diff --git a/src/lib/builders/listbox/create.ts b/src/lib/builders/listbox/create.ts
index f7af9d9d..7a927a69 100644
--- a/src/lib/builders/listbox/create.ts
+++ b/src/lib/builders/listbox/create.ts
@@ -18,7 +18,9 @@ import {
 	getPortalDestination,
 	hiddenInputAttrs,
 	isBrowser,
+	isElement,
 	isElementDisabled,
+	isHTMLButtonElement,
 	isHTMLElement,
 	isHTMLInputElement,
 	kbd,
@@ -271,6 +273,7 @@ export function createListbox<
 
 			const unsubscribe = executeCallbacks(
 				addMeltEventListener(node, 'click', () => {
+					node.focus(); // Fix for safari not adding focus on trigger
 					const $open = get(open);
 					if ($open) {
 						closeMenu();
@@ -302,7 +305,7 @@ export function createListbox<
 
 						// Clicking space on a button triggers a click event. We don't want to
 						// open the menu in this case, and we let the click handler handle it.
-						if (e.key === kbd.SPACE && node instanceof HTMLButtonElement) {
+						if (e.key === kbd.SPACE && isHTMLButtonElement(node)) {
 							return;
 						}
 
@@ -342,7 +345,7 @@ export function createListbox<
 						return;
 					}
 					// Pressing enter with a highlighted item should select it.
-					if (e.key === kbd.ENTER) {
+					if (e.key === kbd.ENTER || (e.key === kbd.SPACE && isHTMLButtonElement(node))) {
 						e.preventDefault();
 						const $highlightedItem = get(highlightedItem);
 						if ($highlightedItem) {
@@ -489,7 +492,10 @@ export function createListbox<
 									? {
 											handler: (e) => {
 												const target = e.target;
-												if (target === $activeTrigger) return;
+												if (!isElement(target)) return;
+												if (target === $activeTrigger || $activeTrigger.contains(target)) {
+													return;
+												}
 												closeMenu();
 											},
 											ignore: ignoreHandler,
diff --git a/src/lib/internal/helpers/is.ts b/src/lib/internal/helpers/is.ts
index 23d99f4a..43fb27d5 100644
--- a/src/lib/internal/helpers/is.ts
+++ b/src/lib/internal/helpers/is.ts
@@ -20,6 +20,10 @@ export function isHTMLLabelElement(element: unknown): element is HTMLLabelElemen
 	return element instanceof HTMLLabelElement;
 }
 
+export function isHTMLButtonElement(element: unknown): element is HTMLButtonElement {
+	return element instanceof HTMLButtonElement;
+}
+
 export function isElementDisabled(element: HTMLElement): boolean {
 	const ariaDisabled = element.getAttribute('aria-disabled');
 	const disabled = element.getAttribute('disabled');
diff --git a/vite.config.ts b/vite.config.ts
index b17ece0b..c21f1a16 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -20,5 +20,6 @@ export default defineConfig({
 		deps: {
 			inline: ['clsx'],
 		},
+		retry: 3,
 	},
 });


Test patch:
diff --git a/src/tests/select/Select.spec.ts b/src/tests/select/Select.spec.ts
index 43c0f9e8..6291177b 100644
--- a/src/tests/select/Select.spec.ts
+++ b/src/tests/select/Select.spec.ts
@@ -39,6 +39,34 @@ describe('Select', () => {
 		expect(menu).toBeVisible();
 	});
 
+	test('Toggles when trigger is clicked', async () => {
+		const { getByTestId } = render(SelectTest);
+		const trigger = getByTestId('trigger');
+		const menu = getByTestId('menu');
+		const user = userEvent.setup();
+
+		expect(menu).not.toBeVisible();
+		await user.click(trigger);
+		expect(menu).toBeVisible();
+
+		await user.click(trigger);
+		expect(menu).not.toBeVisible();
+	});
+
+	test('Toggles when icon within trigger is clicked', async () => {
+		const { getByTestId } = render(SelectTest);
+		const icon = getByTestId('icon');
+		const menu = getByTestId('menu');
+		const user = userEvent.setup();
+
+		expect(menu).not.toBeVisible();
+		await user.click(icon);
+		expect(menu).toBeVisible();
+
+		await user.click(icon);
+		expect(menu).not.toBeVisible();
+	});
+
 	test('Selects item when clicked', async () => {
 		const { getByTestId } = render(SelectTest);
 		const trigger = getByTestId('trigger');
diff --git a/src/tests/select/SelectTest.svelte b/src/tests/select/SelectTest.svelte
index a9843802..cb89a6ac 100644
--- a/src/tests/select/SelectTest.svelte
+++ b/src/tests/select/SelectTest.svelte
@@ -58,7 +58,21 @@
 
 	<button use:melt={$trigger} aria-label="Food" data-testid="trigger">
 		{$selectedLabel || 'Select an option'}
-		<ChevronDown />
+		<svg
+			xmlns="http://www.w3.org/2000/svg"
+			width="1em"
+			height="1em"
+			viewBox="0 0 24 24"
+			data-testid="icon"
+			><path
+				fill="none"
+				stroke="currentColor"
+				stroke-linecap="round"
+				stroke-linejoin="round"
+				stroke-width="2"
+				d="m6 9l6 6l6-6"
+			/></svg
+		>
 	</button>
 
 	<div use:melt={$menu} data-testid="menu">