Skip to content

Commit

Permalink
pkp/pkp-lib#10033 Add DropdownMenu component
Browse files Browse the repository at this point in the history
  • Loading branch information
blesildaramirez committed Jun 16, 2024
1 parent 5f6b1c7 commit 4203f00
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 0 deletions.
16 changes: 16 additions & 0 deletions src/components/DropdownMenu/DropdownMenu.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Primary, Controls, Stories, Meta, Story} from '@storybook/blocks';
import DropdownMenu from './DropdownMenu.vue';

import * as DropdownMenuStories from './DropdownMenu.stories.js';

<Meta of={DropdownMenuStories} />

# Dropdown Menu

## Usage

This component renders a dropdown menu that can display a list of actions. If the `name` prop is supplied, it is used as the button's label; otherwise, an ellipsis (`...`) is used.

<Primary />
<Controls />
<Stories />
124 changes: 124 additions & 0 deletions src/components/DropdownMenu/DropdownMenu.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import DropdownMenu from './DropdownMenu.vue';

export default {
title: 'Components/DropdownMenu',
component: DropdownMenu,
render: (args) => ({
components: {DropdownMenu},
setup() {
return {args};
},
template: '<DropdownMenu v-bind="args" />',
}),
argTypes: {
actions: {
description:
'An array of action objects. Each object should contain `label` (string), `url` (string), an optional `icon` (string) and `isWarnable` (boolean) if the button needs the "warning" button styling from `<Button>` component.',
table: {
type: {summary: 'Array'},
defaultValue: {summary: '[]'},
},
defaultValue: [],
},
name: {
control: {type: 'text'},
description:
'The name of the dropdown menu component (optional). If not supplied, then the dropdown menu will use an ellipsis menu (`...`)',
},
position: {
control: {type: 'select'},
options: ['left', 'right'],
description:
'Determines where to show the dropdown button. Options include `left` and `right`.',
},
},
};

const downloadActions = [
{
label: 'Author-Only Sections Displayed (PDF)',
url: '#',
},
{
label: 'Author-Only Sections Displayed (XML)',
url: '#',
},
{
label: 'Editor Forms Shows All Review Sections (PDF)',
url: '#',
},
{
label: 'Editor Forms Shows All Review Sections (XML)',
url: '#',
},
];

export const Default = {
args: {
actions: downloadActions,
name: 'Download Review Form',
},
decorators: [
() => ({
template: '<div style="height: 200px"><story/></div>',
}),
],
};

export const EllipsisMenu = {
args: {
actions: [
{
label: 'View',
url: '#',
icon: 'View',
},
{
label: 'Email',
url: '#',
icon: 'Email',
},
{
label: 'Login As',
url: '#',
icon: 'LoginAs',
},
{
label: 'Remove User',
url: '#',
icon: 'Cancel',
isWarnable: true,
},
{
label: 'Disable User',
url: '#',
icon: 'DisableUser',
isWarnable: true,
},
{
label: 'Merge User',
url: '#',
icon: 'MergeUser',
},
],
name: undefined,
},
decorators: [
() => ({
template: '<div style="height: 270px"><story/></div>',
}),
],
};

export const LeftPosition = {
args: {
actions: downloadActions,
name: 'Left Dropdown',
position: 'left',
},
decorators: [
() => ({
template: '<div style="height: 200px"><story/></div>',
}),
],
};
95 changes: 95 additions & 0 deletions src/components/DropdownMenu/DropdownMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<template>
<div class="relative flex items-start justify-between">
<Menu
as="div"
:class="
position === 'right'
? 'ltr:ml-auto rtl:mr-auto'
: 'ltr:mr-auto rtl:ml-auto'
"
>
<div>
<MenuButton
class="bg-white hover:bg-gray-50 inline-flex w-full justify-center gap-x-1.5 rounded px-3 py-2"
:class="[
name ? 'border border-light text-lg-normal' : 'text-3xl-normal',
]"
>
{{ name }}
<Icon
class="-mr-1 h-5 w-5 text-primary"
:icon="name ? 'Dropdown' : 'MoreOptions'"
aria-hidden="true"
/>
</MenuButton>
</div>

<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
class="bg-white dropdown-shadow absolute z-10 w-max border border-light focus:outline-none"
:class="
position === 'right'
? 'ltr:right-0 ltr:origin-top-right rtl:left-0 rtl:origin-top-right'
: 'ltr:left-0 ltr:origin-top-left rtl:right-0 rtl:origin-top-left'
"
>
<MenuItem v-for="(action, i) in actions" :key="i" v-slot="{active}">
<div class="w-auto">
<PkpButton
v-if="action.label"
element="a"
:href="action.url"
:icon="action.icon"
:is-active="active"
:is-warnable="action.isWarnable"
:class="i !== actions.length - 1 ? 'border-b' : ''"
size-variant="fullWidth"
class="border-light"
>
{{ action.label }}
</PkpButton>
</div>
</MenuItem>
</MenuItems>
</transition>
</Menu>
</div>
</template>

<script setup>
import {Menu, MenuButton, MenuItem, MenuItems} from '@headlessui/vue';
defineProps({
actions: {
type: Array,
required: true,
validator: (actions) => {
return actions.every(
(action) =>
typeof action.label === 'string' && action.label.trim() !== '',
);
},
},
name: {
type: String,
default: undefined,
},
position: {
type: String,
default: 'right',
},
});
</script>

<style scoped>
.dropdown-shadow {
box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.5);
}
</style>

0 comments on commit 4203f00

Please sign in to comment.