From 65a3b0a2d0f8a4e32b9c0ce4707f22268b32e3dc Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Fri, 26 Apr 2024 14:53:50 +0200 Subject: [PATCH] feat(ContextMenu): new component Resolves #18 --- playground/app.vue | 1 + playground/pages/context-menu.vue | 78 +++++++++++ src/runtime/components/ContextMenu.vue | 91 ++++++++++++ src/runtime/components/ContextMenuContent.vue | 117 ++++++++++++++++ src/runtime/types/index.d.ts | 3 +- src/theme/context-menu.ts | 28 ++++ src/theme/index.ts | 1 + test/components/ContextMenu.spec.ts | 94 +++++++++++++ .../__snapshots__/ContextMenu.spec.ts.snap | 131 ++++++++++++++++++ 9 files changed, 543 insertions(+), 1 deletion(-) create mode 100644 playground/pages/context-menu.vue create mode 100644 src/runtime/components/ContextMenu.vue create mode 100644 src/runtime/components/ContextMenuContent.vue create mode 100644 src/theme/context-menu.ts create mode 100644 test/components/ContextMenu.spec.ts create mode 100644 test/components/__snapshots__/ContextMenu.spec.ts.snap diff --git a/playground/app.vue b/playground/app.vue index 8da7677b..0c606a8a 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -20,6 +20,7 @@ const components = [ 'checkbox', 'chip', 'collapsible', + 'context-menu', 'drawer', 'dropdown-menu', 'form', diff --git a/playground/pages/context-menu.vue b/playground/pages/context-menu.vue new file mode 100644 index 00000000..de240517 --- /dev/null +++ b/playground/pages/context-menu.vue @@ -0,0 +1,78 @@ + + + diff --git a/src/runtime/components/ContextMenu.vue b/src/runtime/components/ContextMenu.vue new file mode 100644 index 00000000..8494683a --- /dev/null +++ b/src/runtime/components/ContextMenu.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/runtime/components/ContextMenuContent.vue b/src/runtime/components/ContextMenuContent.vue new file mode 100644 index 00000000..870f20a2 --- /dev/null +++ b/src/runtime/components/ContextMenuContent.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/src/runtime/types/index.d.ts b/src/runtime/types/index.d.ts index a96aaef7..3d9e9906 100644 --- a/src/runtime/types/index.d.ts +++ b/src/runtime/types/index.d.ts @@ -1,6 +1,6 @@ -export * from '../components/App.vue' export * from '../components/Accordion.vue' export * from '../components/Alert.vue' +export * from '../components/App.vue' export * from '../components/Avatar.vue' export * from '../components/Badge.vue' export * from '../components/Breadcrumb.vue' @@ -10,6 +10,7 @@ export * from '../components/Checkbox.vue' export * from '../components/Chip.vue' export * from '../components/Collapsible.vue' export * from '../components/Container.vue' +export * from '../components/ContextMenu.vue' export * from '../components/Drawer.vue' export * from '../components/DropdownMenu.vue' export * from '../components/Form.vue' diff --git a/src/theme/context-menu.ts b/src/theme/context-menu.ts new file mode 100644 index 00000000..71147ac3 --- /dev/null +++ b/src/theme/context-menu.ts @@ -0,0 +1,28 @@ +export default { + slots: { + content: 'min-w-32 bg-white dark:bg-gray-900 shadow-lg rounded-md ring ring-gray-200 dark:ring-gray-800 divide-y divide-gray-200 dark:divide-gray-800 overflow-y-auto scroll-py-1 data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]', + arrow: 'fill-gray-200 dark:fill-gray-800', + group: 'p-1 isolate', + label: 'w-full flex items-center gap-1.5 p-1.5 text-sm font-medium select-none', + separator: '-mx-1 my-1 h-px bg-gray-200 dark:bg-gray-800', + link: 'group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75', + linkLeadingIcon: 'shrink-0 size-5', + linkLeadingAvatar: 'shrink-0', + linkTrailing: 'ms-auto inline-flex', + linkTrailingIcon: 'shrink-0 size-5', + linkTrailingKbds: 'hidden lg:inline-flex items-center shrink-0 gap-0.5', + linkLabel: 'truncate' + }, + variants: { + active: { + true: { + link: 'text-gray-900 dark:text-white before:bg-gray-100 dark:before:bg-gray-800', + linkLeadingIcon: 'text-gray-700 dark:text-gray-200' + }, + false: { + link: 'text-gray-700 dark:text-gray-200 data-highlighted:text-gray-900 dark:data-highlighted:text-white data-[state=open]:text-gray-900 dark:data-[state=open]:text-white data-highlighted:before:bg-gray-50 dark:data-highlighted:before:bg-gray-800/50 data-[state=open]:before:bg-gray-50 dark:data-[state=open]:before:bg-gray-800/50', + linkLeadingIcon: 'text-gray-400 dark:text-gray-500 group-data-highlighted:text-gray-700 dark:group-data-highlighted:text-gray-200 group-data-[state=open]:text-gray-700 dark:group-data-[state=open]:text-gray-200' + } + } + } +} diff --git a/src/theme/index.ts b/src/theme/index.ts index fe744570..066a256e 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -9,6 +9,7 @@ export { default as checkbox } from './checkbox' export { default as chip } from './chip' export { default as collapsible } from './collapsible' export { default as container } from './container' +export { default as contextMenu } from './context-menu' export { default as drawer } from './drawer' export { default as dropdownMenu } from './dropdown-menu' export { default as form } from './form' diff --git a/test/components/ContextMenu.spec.ts b/test/components/ContextMenu.spec.ts new file mode 100644 index 00000000..ca440a5c --- /dev/null +++ b/test/components/ContextMenu.spec.ts @@ -0,0 +1,94 @@ +import { describe, it, expect } from 'vitest' +import ContextMenu, { type ContextMenuProps, type ContextMenuSlots } from '../../src/runtime/components/ContextMenu.vue' +import ComponentRender from '../component-render' + +// FIXME: Can't force open state +describe('ContextMenu', () => { + const items = [ + [{ + label: 'Appearance', + children: [{ + label: 'System', + icon: 'i-heroicons-computer-desktop' + }, { + label: 'Light', + icon: 'i-heroicons-sun' + }, { + label: 'Dark', + icon: 'i-heroicons-moon' + }] + }], + [{ + label: 'Show Sidebar', + kbds: ['meta', 'S'], + select() { + console.log('Show Sidebar clicked') + } + }, { + label: 'Show Toolbar', + kbds: ['shift', 'meta', 'D'], + select() { + console.log('Show Toolbar clicked') + } + }, { + label: 'Collapse Pinned Tabs', + disabled: true + }], [{ + label: 'Refresh the Page' + }, { + label: 'Clear Cookies and Refresh' + }, { + label: 'Clear Cache and Refresh' + }, { + type: 'separator' as const + }, { + label: 'Developer', + children: [[{ + label: 'View Source', + kbds: ['option', 'meta', 'U'], + select() { + console.log('View Source clicked') + } + }, { + label: 'Developer Tools', + kbds: ['option', 'meta', 'I'], + select() { + console.log('Developer Tools clicked') + } + }], [{ + label: 'Inspect Elements', + kbds: ['option', 'meta', 'C'], + select() { + console.log('Inspect Elements clicked') + } + }], [{ + label: 'JavaScript Console', + kbds: ['option', 'meta', 'J'], + slot: 'custom', + select() { + console.log('JavaScript Console clicked') + } + }]] + }] + ] + + const props = { portal: false, items } + + it.each([ + // Props + ['with items', { props }], + ['with disabled', { props: { ...props, disabled: true } }], + ['with class', { props: { ...props, class: 'min-w-96' } }], + ['with ui', { props: { ...props, ui: { linkLeadingIcon: 'size-4' } } }], + // Slots + ['with default slot', { props, slots: { default: () => 'Default slot' } }], + ['with leading slot', { props, slots: { leading: () => 'Leading slot' } }], + ['with label slot', { props, slots: { label: () => 'Label slot' } }], + ['with trailing slot', { props, slots: { trailing: () => 'Trailing slot' } }], + ['with item slot', { props, slots: { item: () => 'Item slot' } }], + ['with custom slot', { props, slots: { custom: () => 'Custom slot' } }] + ])('renders %s correctly', async (nameOrHtml: string, options: { props?: ContextMenuProps, slots?: Partial> }) => { + const html = await ComponentRender(nameOrHtml, options, ContextMenu) + expect(html).toMatchSnapshot() + }) +}) diff --git a/test/components/__snapshots__/ContextMenu.spec.ts.snap b/test/components/__snapshots__/ContextMenu.spec.ts.snap new file mode 100644 index 00000000..6ea46cc6 --- /dev/null +++ b/test/components/__snapshots__/ContextMenu.spec.ts.snap @@ -0,0 +1,131 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ContextMenu > renders with class correctly 1`] = ` +" + + + + + + + + +" +`; + +exports[`ContextMenu > renders with custom slot correctly 1`] = ` +" + + + + + + + + +" +`; + +exports[`ContextMenu > renders with default slot correctly 1`] = ` +"Default slot + + + + + + + + +" +`; + +exports[`ContextMenu > renders with disabled correctly 1`] = ` +" + + + + + + + + +" +`; + +exports[`ContextMenu > renders with item slot correctly 1`] = ` +" + + + + + + + + +" +`; + +exports[`ContextMenu > renders with items correctly 1`] = ` +" + + + + + + + + +" +`; + +exports[`ContextMenu > renders with label slot correctly 1`] = ` +" + + + + + + + + +" +`; + +exports[`ContextMenu > renders with leading slot correctly 1`] = ` +" + + + + + + + + +" +`; + +exports[`ContextMenu > renders with trailing slot correctly 1`] = ` +" + + + + + + + + +" +`; + +exports[`ContextMenu > renders with ui correctly 1`] = ` +" + + + + + + + + +" +`;