diff --git a/packages/core/src/lib/__tests__/code-snippet.spec.ts b/packages/core/src/lib/__tests__/code-snippet.spec.ts
index 102f1d67..9b5a11b3 100644
--- a/packages/core/src/lib/__tests__/code-snippet.spec.ts
+++ b/packages/core/src/lib/__tests__/code-snippet.spec.ts
@@ -67,7 +67,6 @@ describe('CodeSnippet', () => {
const initialCode = screen.getByText(common.code);
expect(initialCode).toBeInTheDocument();
-
const newCode = '{ their: "json" }';
await rerender({
...common,
diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts
index 78eb130f..ee55ad02 100644
--- a/packages/core/src/lib/index.ts
+++ b/packages/core/src/lib/index.ts
@@ -55,6 +55,7 @@ export { preventHandler, preventKeyboardHandler } from './prevent-handler';
export * from './select';
+export { default as NavDropdown } from './nav-dropdown/nav-dropdown.svelte';
export { default as Progress } from './progress/progress.svelte';
export { default as Switch } from './switch.svelte';
export { default as Radio } from './radio.svelte';
diff --git a/packages/core/src/lib/nav-dropdown/__tests__/nav-dropdown.spec.ts b/packages/core/src/lib/nav-dropdown/__tests__/nav-dropdown.spec.ts
new file mode 100644
index 00000000..34608975
--- /dev/null
+++ b/packages/core/src/lib/nav-dropdown/__tests__/nav-dropdown.spec.ts
@@ -0,0 +1,91 @@
+import { describe, expect, it } from 'vitest';
+import { render, screen, within } from '@testing-library/svelte';
+import userEvent from '@testing-library/user-event';
+import { NavDropdown as Subject } from '$lib';
+
+const versionOptions = [
+ {
+ label: 'Version 1',
+ detail: '1 day ago',
+ description: 'stable',
+ href: '/v1',
+ },
+ {
+ label: 'Version 2',
+ detail: '5 hours ago',
+ description: 'latest',
+ href: '/v2',
+ },
+];
+
+describe('NavDropdown', () => {
+ it('renders a button that controls a menu', () => {
+ render(Subject, {
+ props: { options: versionOptions, selectedHref: '/v1' },
+ });
+
+ const button = screen.getByRole('button');
+
+ expect(button).toHaveAttribute('aria-haspopup', 'menu');
+ expect(button).toHaveAttribute('aria-expanded', 'false');
+ });
+
+ it('expands the menu on click', async () => {
+ const user = userEvent.setup();
+ render(Subject, {
+ props: { options: versionOptions, selectedHref: '/v1' },
+ });
+
+ const button = screen.getByRole('button');
+ await user.click(button);
+
+ const menu = screen.getByRole('menu');
+
+ expect(menu).toBeInTheDocument();
+ expect(button).toHaveAttribute('aria-expanded', 'true');
+ });
+
+ it('displays option details', async () => {
+ const user = userEvent.setup();
+ render(Subject, {
+ props: { options: versionOptions, selectedHref: '/v1' },
+ });
+
+ const button = screen.getByRole('button');
+ await user.click(button);
+
+ const menuitem = screen.getByRole('menuitem', { name: /Version 1/u });
+ expect(within(menuitem).getByText(/1 day ago/u)).toBeInTheDocument();
+ expect(within(menuitem).getByText('stable')).toBeInTheDocument();
+ });
+
+ it('opens menu with Space when button is focused', async () => {
+ const user = userEvent.setup();
+ render(Subject, {
+ props: { options: versionOptions, selectedHref: '/v1' },
+ });
+
+ const button = screen.getByRole('button');
+ button.focus();
+ await user.keyboard(' ');
+
+ expect(screen.getByRole('menu')).toBeInTheDocument();
+ expect(button).toHaveAttribute('aria-expanded', 'true');
+ });
+
+ it('closes menu with Escape', async () => {
+ const user = userEvent.setup();
+ render(Subject, {
+ props: { options: versionOptions, selectedHref: '/v1' },
+ });
+
+ const button = screen.getByRole('button');
+ await user.click(button);
+ expect(screen.getByRole('menu')).toBeInTheDocument();
+
+ await user.keyboard('{Escape}');
+
+ expect(screen.queryByRole('menu')).not.toBeInTheDocument();
+ expect(button).toHaveAttribute('aria-expanded', 'false');
+ });
+});
diff --git a/packages/core/src/lib/nav-dropdown/nav-dropdown.svelte b/packages/core/src/lib/nav-dropdown/nav-dropdown.svelte
new file mode 100644
index 00000000..e19881cc
--- /dev/null
+++ b/packages/core/src/lib/nav-dropdown/nav-dropdown.svelte
@@ -0,0 +1,116 @@
+
+
+
+
+
+ {#if isOpen}
+
+
+
+ {/if}
+
diff --git a/packages/core/src/routes/+page.svelte b/packages/core/src/routes/+page.svelte
index 1501ce31..081c6e7a 100644
--- a/packages/core/src/routes/+page.svelte
+++ b/packages/core/src/routes/+page.svelte
@@ -41,6 +41,7 @@ import {
CodeSnippet,
RangeInput,
Progress,
+ NavDropdown,
} from '$lib';
import { uniqueId } from 'lodash-es';
@@ -1107,6 +1108,44 @@ const onHoverDelayMsInput = (event: Event) => {
+
+ NAV Dropdown
+
+
+
+
Notify
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d5edeb53..9f497e5c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1385,6 +1385,10 @@ packages:
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
engines: {node: '>=6.0.0'}
+ '@jridgewell/gen-mapping@0.3.8':
+ resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
+ engines: {node: '>=6.0.0'}
+
'@jridgewell/resolve-uri@3.1.1':
resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
engines: {node: '>=6.0.0'}
@@ -1393,12 +1397,15 @@ packages:
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
engines: {node: '>=6.0.0'}
- '@jridgewell/source-map@0.3.5':
- resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==}
+ '@jridgewell/source-map@0.3.6':
+ resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
'@jridgewell/sourcemap-codec@1.4.15':
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
+ '@jridgewell/sourcemap-codec@1.5.0':
+ resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
+
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
@@ -2028,6 +2035,11 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
+ acorn@8.14.0:
+ resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
agent-base@7.1.0:
resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==}
engines: {node: '>= 14'}
@@ -4527,8 +4539,8 @@ packages:
sander@0.5.1:
resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==}
- sax@1.3.0:
- resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
+ sax@1.4.1:
+ resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
@@ -4977,6 +4989,9 @@ packages:
tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
tweakpane@3.1.10:
resolution: {integrity: sha512-rqwnl/pUa7+inhI2E9ayGTqqP0EPOOn/wVvSWjZsRbZUItzNShny7pzwL3hVlaN4m9t/aZhsP0aFQ9U5VVR2VQ==}
@@ -6198,18 +6213,28 @@ snapshots:
'@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping': 0.3.25
+ '@jridgewell/gen-mapping@0.3.8':
+ dependencies:
+ '@jridgewell/set-array': 1.2.1
+ '@jridgewell/sourcemap-codec': 1.5.0
+ '@jridgewell/trace-mapping': 0.3.25
+ optional: true
+
'@jridgewell/resolve-uri@3.1.1': {}
'@jridgewell/set-array@1.2.1': {}
- '@jridgewell/source-map@0.3.5':
+ '@jridgewell/source-map@0.3.6':
dependencies:
- '@jridgewell/gen-mapping': 0.3.5
+ '@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
optional: true
'@jridgewell/sourcemap-codec@1.4.15': {}
+ '@jridgewell/sourcemap-codec@1.5.0':
+ optional: true
+
'@jridgewell/trace-mapping@0.3.25':
dependencies:
'@jridgewell/resolve-uri': 3.1.1
@@ -6968,6 +6993,9 @@ snapshots:
acorn@8.11.3: {}
+ acorn@8.14.0:
+ optional: true
+
agent-base@7.1.0:
dependencies:
debug: 4.3.4
@@ -8719,7 +8747,7 @@ snapshots:
dependencies:
copy-anything: 2.0.6
parse-node-version: 1.0.1
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
errno: 0.1.8
graceful-fs: 4.2.11
@@ -9649,7 +9677,7 @@ snapshots:
needle@3.3.1:
dependencies:
iconv-lite: 0.6.3
- sax: 1.3.0
+ sax: 1.4.1
optional: true
nlcst-to-string@3.1.1:
@@ -10308,7 +10336,7 @@ snapshots:
mkdirp: 0.5.6
rimraf: 2.7.1
- sax@1.3.0:
+ sax@1.4.1:
optional: true
saxes@6.0.0:
@@ -10686,8 +10714,8 @@ snapshots:
terser@5.26.0:
dependencies:
- '@jridgewell/source-map': 0.3.5
- acorn: 8.11.3
+ '@jridgewell/source-map': 0.3.6
+ acorn: 8.14.0
commander: 2.20.3
source-map-support: 0.5.21
optional: true
@@ -10804,6 +10832,9 @@ snapshots:
tslib@2.6.2: {}
+ tslib@2.8.1:
+ optional: true
+
tweakpane@3.1.10: {}
type-check@0.4.0: