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: