From ac86ee01b9fc9b5dc882b210d88b8fef73148e42 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Fri, 24 Jan 2025 18:35:03 +0100 Subject: [PATCH] feat(NavigationMenu): add `contentOrientation` prop --- docs/content/3.components/navigation-menu.md | 79 ++++++++++++++ .../app/pages/components/navigation-menu.vue | 6 +- src/runtime/components/NavigationMenu.vue | 8 ++ src/theme/navigation-menu.ts | 32 +++++- test/components/NavigationMenu.spec.ts | 6 +- .../NavigationMenu-vue.spec.ts.snap | 103 +++++++++++++++++- .../__snapshots__/NavigationMenu.spec.ts.snap | 103 +++++++++++++++++- 7 files changed, 318 insertions(+), 19 deletions(-) diff --git a/docs/content/3.components/navigation-menu.md b/docs/content/3.components/navigation-menu.md index 7f06b6f1..93c84229 100644 --- a/docs/content/3.components/navigation-menu.md +++ b/docs/content/3.components/navigation-menu.md @@ -613,6 +613,85 @@ props: The arrow is animated to follow the active item. :: +### Content Orientation + +Use the `content-orientation` prop to change the orientation of the content. + +::warning +This prop only works when `orientation` is `horizontal`. +:: + +::component-code +--- +collapse: true +ignore: + - items + - arrow + - class +external: + - items +props: + arrow: true + contentOrientation: 'vertical' + items: + - label: Guide + icon: i-lucide-book-open + to: /getting-started + children: + - label: Introduction + description: Fully styled and customizable components for Nuxt. + icon: i-lucide-house + - label: Installation + description: Learn how to install and configure Nuxt UI in your application. + icon: i-lucide-cloud-download + - label: 'Icons' + icon: 'i-lucide-smile' + description: 'You have nothing to do, @nuxt/icon will handle it automatically.' + - label: Composables + icon: i-lucide-database + to: /composables + children: + - label: defineShortcuts + icon: i-lucide-file-text + description: Define shortcuts for your application. + to: /composables/define-shortcuts + - label: useModal + icon: i-lucide-file-text + description: Display a modal within your application. + to: /composables/use-modal + - label: useSlideover + icon: i-lucide-file-text + description: Display a slideover within your application. + to: /composables/use-slideover + - label: useToast + icon: i-lucide-file-text + description: Display a toast within your application. + to: /composables/use-toast + - label: Components + icon: i-lucide-box + to: /components + active: true + children: + - label: Link + icon: i-lucide-file-text + description: Use NuxtLink with superpowers. + to: /components/link + - label: Modal + icon: i-lucide-file-text + description: Display a modal within your application. + to: /components/modal + - label: NavigationMenu + icon: i-lucide-file-text + description: Display a list of links. + to: /components/navigation-menu + - label: Pagination + icon: i-lucide-file-text + description: Display a list of pages. + to: /components/pagination + class: 'w-full justify-center' +--- +:: + ### Unmount Use the `unmount-on-hide` prop to control the content unmounting behavior. Defaults to `true`. diff --git a/playground/app/pages/components/navigation-menu.vue b/playground/app/pages/components/navigation-menu.vue index 8600042b..f1848179 100644 --- a/playground/app/pages/components/navigation-menu.vue +++ b/playground/app/pages/components/navigation-menu.vue @@ -4,11 +4,13 @@ import theme from '#build/ui/navigation-menu' const colors = Object.keys(theme.variants.color) const variants = Object.keys(theme.variants.variant) const orientations = Object.keys(theme.variants.orientation) +const contentOrientations = Object.keys(theme.variants.contentOrientation) const color = ref(theme.defaultVariants.color) const highlightColor = ref() const variant = ref(theme.defaultVariants.variant) -const orientation = ref('vertical' as const) +const orientation = ref('horizontal' as const) +const contentOrientation = ref('horizontal' as const) const highlight = ref(true) const collapsed = ref(false) @@ -93,6 +95,7 @@ const items = [ + @@ -105,6 +108,7 @@ const items = [ :color="color" :variant="variant" :orientation="orientation" + :viewport-orientation="contentOrientation" :highlight="highlight" :highlight-color="highlightColor" :class="highlight && 'data-[orientation=horizontal]:border-b border-[var(--ui-border)]'" diff --git a/src/runtime/components/NavigationMenu.vue b/src/runtime/components/NavigationMenu.vue index 93cd89b8..7253a7c8 100644 --- a/src/runtime/components/NavigationMenu.vue +++ b/src/runtime/components/NavigationMenu.vue @@ -72,6 +72,12 @@ export interface NavigationMenuProps extends Pick + /** + * The orientation of the content. + * Only works when `orientation` is `horizontal`. + * @defaultValue 'horizontal' + */ + contentOrientation?: NavigationMenuVariants['contentOrientation'] /** * Display an arrow alongside the menu. * @defaultValue false @@ -153,6 +159,7 @@ import UCollapsible from './Collapsible.vue' const props = withDefaults(defineProps>(), { orientation: 'horizontal', + contentOrientation: 'horizontal', delayDuration: 0, unmountOnHide: true, labelKey: 'label' @@ -180,6 +187,7 @@ const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: N const ui = computed(() => navigationMenu({ orientation: props.orientation, + contentOrientation: props.contentOrientation, collapsed: props.collapsed, color: props.color, variant: props.variant, diff --git a/src/theme/navigation-menu.ts b/src/theme/navigation-menu.ts index 2134ed70..ecf839cb 100644 --- a/src/theme/navigation-menu.ts +++ b/src/theme/navigation-menu.ts @@ -25,9 +25,9 @@ export default (options: Required) => ({ childLinkLabelExternalIcon: 'inline-block size-3 align-top text-[var(--ui-text-dimmed)]', childLinkDescription: 'text-sm text-[var(--ui-text-muted)]', separator: 'px-2 h-px bg-[var(--ui-border)]', - viewportWrapper: 'absolute top-full left-0 flex w-full justify-center', - viewport: 'relative overflow-hidden bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] h-[var(--reka-navigation-menu-viewport-height)] w-full transition-[width,height] origin-[top_center] data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]', - content: 'absolute top-0 left-0 w-full data-[motion=from-start]:animate-[enter-from-left_200ms_ease] data-[motion=from-end]:animate-[enter-from-right_200ms_ease] data-[motion=to-start]:animate-[exit-to-left_200ms_ease] data-[motion=to-end]:animate-[exit-to-right_200ms_ease]', + viewportWrapper: 'absolute top-full left-0 flex w-full', + viewport: 'relative overflow-hidden bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] h-[var(--reka-navigation-menu-viewport-height)] w-full transition-[width,height,left] duration-200 origin-[top_center] data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]', + content: 'absolute top-0 left-0 w-full', indicator: 'absolute data-[state=visible]:animate-[fade-in_100ms_ease-out] data-[state=hidden]:animate-[fade-out_100ms_ease-in] data-[state=hidden]:opacity-0 bottom-0 z-[1] w-[var(--reka-navigation-menu-indicator-size)] translate-x-[var(--reka-navigation-menu-indicator-position)] flex h-2.5 items-end justify-center overflow-hidden transition-[translate,width] duration-200', arrow: 'relative top-[50%] size-2.5 rotate-45 border border-[var(--ui-border)] bg-[var(--ui-bg)] z-[1] rounded-[calc(var(--ui-radius)/2)]' }, @@ -56,13 +56,24 @@ export default (options: Required) => ({ list: 'flex items-center', item: 'py-2', link: 'px-2.5 py-1.5 before:inset-x-px before:inset-y-0', - childList: 'grid grid-cols-2 gap-2 p-2' + childList: 'grid p-2' }, vertical: { root: 'flex-col', link: 'flex-row px-2.5 py-1.5 before:inset-y-px before:inset-x-0' } }, + contentOrientation: { + horizontal: { + viewport: '', + viewportWrapper: 'justify-center', + content: 'data-[motion=from-start]:animate-[enter-from-left_200ms_ease] data-[motion=from-end]:animate-[enter-from-right_200ms_ease] data-[motion=to-start]:animate-[exit-to-left_200ms_ease] data-[motion=to-end]:animate-[exit-to-right_200ms_ease]' + }, + vertical: { + viewport: 'sm:w-[var(--reka-navigation-menu-viewport-width)] left-[var(--reka-navigation-menu-viewport-left)]', + content: '' + } + }, active: { true: { childLink: 'bg-[var(--ui-bg-elevated)] text-[var(--ui-text-highlighted)]', @@ -91,6 +102,19 @@ export default (options: Required) => ({ } }, compoundVariants: [{ + orientation: 'horizontal', + contentOrientation: 'horizontal', + class: { + childList: 'grid-cols-2 gap-2' + } + }, { + orientation: 'horizontal', + contentOrientation: 'vertical', + class: { + childList: 'gap-1', + content: 'w-60' + } + }, { orientation: 'horizontal', highlight: true, class: { diff --git a/test/components/NavigationMenu.spec.ts b/test/components/NavigationMenu.spec.ts index 4c9c998c..db5688bd 100644 --- a/test/components/NavigationMenu.spec.ts +++ b/test/components/NavigationMenu.spec.ts @@ -84,9 +84,11 @@ describe('NavigationMenu', () => { it.each([ // Props ['with items', { props }], + ['with modelValue', { props: { ...props, modelValue: '0' } }], ['with labelKey', { props: { ...props, labelKey: 'icon' } }], - ['with arrow', { props: { ...props, arrow: true } }], - ['with orientation vertical', { props: { ...props, orientation: 'vertical' as const } }], + ['with arrow', { props: { ...props, arrow: true, modelValue: '0' } }], + ['with orientation vertical', { props: { ...props, orientation: 'vertical' as const, modelValue: '0' } }], + ['with content orientation vertical', { props: { ...props, contentOrientation: 'vertical' as const, modelValue: '0' } }], ...variants.map((variant: string) => [`with primary variant ${variant}`, { props: { ...props, variant } }]), ...variants.map((variant: string) => [`with neutral variant ${variant}`, { props: { ...props, variant, color: 'neutral' } }]), ...variants.map((variant: string) => [`with primary variant ${variant} highlight`, { props: { ...props, variant, highlight: true } }]), diff --git a/test/components/__snapshots__/NavigationMenu-vue.spec.ts.snap b/test/components/__snapshots__/NavigationMenu-vue.spec.ts.snap index 0dc57037..4c1ea147 100644 --- a/test/components/__snapshots__/NavigationMenu-vue.spec.ts.snap +++ b/test/components/__snapshots__/NavigationMenu-vue.spec.ts.snap @@ -10,13 +10,11 @@ exports[`NavigationMenu > renders with arrow correctly 1`] = ` -
  • -
  • @@ -41,7 +39,10 @@ exports[`NavigationMenu > renders with arrow correctly 1`] = `
    - +
    + + +
    " `; @@ -134,6 +135,51 @@ exports[`NavigationMenu > renders with class correctly 1`] = ` " `; +exports[`NavigationMenu > renders with content orientation vertical correctly 1`] = ` +"" +`; + exports[`NavigationMenu > renders with custom slot correctly 1`] = ` "" `; +exports[`NavigationMenu > renders with modelValue correctly 1`] = ` +"" +`; + exports[`NavigationMenu > renders with neutral variant link correctly 1`] = ` "" `; @@ -134,6 +135,51 @@ exports[`NavigationMenu > renders with class correctly 1`] = ` " `; +exports[`NavigationMenu > renders with content orientation vertical correctly 1`] = ` +"" +`; + exports[`NavigationMenu > renders with custom slot correctly 1`] = ` "" `; +exports[`NavigationMenu > renders with modelValue correctly 1`] = ` +"" +`; + exports[`NavigationMenu > renders with neutral variant link correctly 1`] = ` "