Skip to content

Commit

Permalink
feat: add NeDropdown component
Browse files Browse the repository at this point in the history
  • Loading branch information
andre8244 committed Jan 22, 2024
1 parent 793b8fe commit b688eed
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 0 deletions.
142 changes: 142 additions & 0 deletions src/components/NeDropdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<!--
Copyright (C) 2024 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->

<script lang="ts" setup>
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
import NeButton from './NeButton.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { faEllipsisVertical as fasEllipsisVertical } from '@fortawesome/free-solid-svg-icons'
import { ref, watch } from 'vue'

export interface Props {
items: NeDropdownItem[]
alignToRight: boolean
openMenuAriaLabel?: string
}

const props = withDefaults(defineProps<Props>(), {
items: () => [],
alignToRight: false,
openMenuAriaLabel: 'Open menu'
})

export interface NeDropdownItem {
id: string
label?: string
icon?: string
iconStyle?: string
danger?: boolean
action?: () => void
disabled?: boolean
}

library.add(fasEllipsisVertical)

function onItemClick(item: NeDropdownItem) {
if (!item.disabled && item.action) {
item.action()
}
}

function getMenuItemActiveClasses(item: NeDropdownItem) {
if (item.danger) {
return 'bg-rose-700 text-white dark:bg-rose-600 dark:text-white'
} else {
return 'bg-gray-100 dark:bg-gray-800'
}
}

const top = ref(0)
const left = ref(0)
const right = ref(0)
const buttonRef = ref<InstanceType<typeof MenuButton> | null>(null)

function calculatePosition() {
top.value = buttonRef.value?.$el.getBoundingClientRect().bottom + window.scrollY
left.value = buttonRef.value?.$el.getBoundingClientRect().left - window.scrollX
right.value =
document.documentElement.clientWidth -
buttonRef.value?.$el.getBoundingClientRect().right -
window.scrollX
}

watch(
() => props.alignToRight,
() => {
calculatePosition()
}
)
</script>

<template>
<Menu as="div" class="relative inline-block text-left">
<MenuButton
ref="buttonRef"
class="flex items-center text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-50"
@click="calculatePosition()"
>
<span class="sr-only">{{ openMenuAriaLabel }}</span>
<slot name="button">
<!-- default kebab button -->
<NeButton class="py-2" kind="tertiary">
<font-awesome-icon
:icon="['fas', 'ellipsis-vertical']"
aria-hidden="true"
class="h-5 w-5 shrink-0"
/>
</NeButton>
</slot>
</MenuButton>
<Teleport to="body">
<transition
enter-active-class="transition ease-out duration-100"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems
:style="[
{ top: top + 'px' },
alignToRight ? { right: right + 'px' } : { left: left + 'px' }
]"
class="absolute z-50 mt-2.5 min-w-[10rem] rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none dark:bg-gray-950 dark:ring-gray-500/50"
>
<template v-for="item in items" :key="item.id">
<!-- divider -->
<hr
v-if="item.id.includes('divider')"
class="my-1 border-gray-200 dark:border-gray-700"
/>
<!-- item -->
<MenuItem v-else v-slot="{ active }" :disabled="item.disabled">
<a
:class="[
active ? getMenuItemActiveClasses(item) : '',
'group flex items-center px-4 py-2 text-sm',
item.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
item.danger
? 'text-rose-700 dark:text-rose-500'
: 'text-gray-700 dark:text-gray-50'
]"
@click="onItemClick(item)"
>
<font-awesome-icon
v-if="item.icon"
:icon="[item.iconStyle || 'fas', item.icon]"
aria-hidden="true"
class="mr-2 h-5 w-5 shrink-0"
/>
{{ item.label }}
</a>
</MenuItem>
</template>
</MenuItems>
</transition>
</Teleport>
</Menu>
</template>
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { default as NeTableBody } from '@/components/NeTableBody.vue'
export { default as NeTableRow } from '@/components/NeTableRow.vue'
export { default as NeTableCell } from '@/components/NeTableCell.vue'
export { default as NeCombobox } from '@/components/NeCombobox.vue'
export { default as NeDropdown } from '@/components/NeDropdown.vue'

// types
export type { NeComboboxOption } from '@/components/NeCombobox.vue'
116 changes: 116 additions & 0 deletions stories/NeDropdown.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (C) 2024 Nethesis S.r.l.
// SPDX-License-Identifier: GPL-3.0-or-later

import type { Meta, StoryObj } from '@storybook/vue3'

import { NeDropdown, NeButton } from '../src/main'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faPenToSquare as fasPenToSquare } from '@fortawesome/free-solid-svg-icons'
import { faFloppyDisk as fasFloppyDisk } from '@fortawesome/free-solid-svg-icons'
import { faTrashCan as fasTrashCan } from '@fortawesome/free-solid-svg-icons'
import { faCopy as fasCopy } from '@fortawesome/free-solid-svg-icons'
import { faChevronDown as fasChevronDown } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'

library.add(fasPenToSquare)
library.add(fasFloppyDisk)
library.add(fasTrashCan)
library.add(fasCopy)
library.add(fasChevronDown)

const meta = {
title: 'Visual/NeDropdown',
component: NeDropdown,
tags: ['autodocs'],
args: {
items: [
{
id: 'edit',
label: 'Edit',
icon: 'pen-to-square',
iconStyle: 'fas',
action: () => {}
},
{
id: 'copy',
label: 'Copy',
icon: 'copy',
iconStyle: 'fas',
action: () => {}
},
{
id: 'save',
label: 'Save',
icon: 'floppy-disk',
iconStyle: 'fas',
action: () => {},
disabled: true
},
{
id: 'divider1'
},
{
id: 'delete',
label: 'Delete',
icon: 'trash-can',
iconStyle: 'fas',
danger: true,
action: () => {}
}
],
alignToRight: false,
openMenuAriaLabel: 'Open menu'
}
} satisfies Meta<typeof NeDropdown>

export default meta
type Story = StoryObj<typeof meta>

const template = '<NeDropdown v-bind="args" />'

export const Default: Story = {
render: (args) => ({
components: { NeDropdown },
setup() {
return { args }
},
template: template
}),
args: {}
}

const alignToRightTemplate = '<NeDropdown v-bind="args" class="ml-48" />'

export const AlignToRight: Story = {
render: (args) => ({
components: { NeDropdown },
setup() {
return { args }
},
template: alignToRightTemplate
}),
args: { alignToRight: true }
}

const withSlotTemplate =
'<NeDropdown v-bind="args">\
<template #button>\
<NeButton>\
<template #suffix>\
<font-awesome-icon :icon="[\'fas\', \'chevron-down\']" class="h-4 w-4" aria-hidden="true" />\
</template>\
Menu\
</NeButton>\
</template>\
</NeDropdown>'

export const WithSlot: Story = {
render: (args) => ({
components: { NeDropdown, NeButton, FontAwesomeIcon },
setup() {
return { args }
},
template: withSlotTemplate
}),
args: {}
}

0 comments on commit b688eed

Please sign in to comment.