-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
pkp/pkp-lib#10033 Add new dropdown menu component (#363)
* pkp/pkp-lib#10033 Add MoreOptions icon in Icon Gallery/components * pkp/pkp-lib#10033 Add DropdownMenu component * pkp/pkp-lib#10033 Change classes used for white bg and set min width * pkp/pkp-lib#10033 Only use white bg for dropdown button if not using ellipsis menu * pkp/pkp-lib#10033 Rename newly added component from DropdownMenu to DropdownActions * pkp/pkp-lib#10033 Add click user event when rendering stories for DropdownActions component * pkp/pkp-lib#10033 Add ariaLabel prop when using ellipsis menu * pkp/pkp-lib#i10033 Implement code review suggestions * pkp/pkp#10033 Update documentation and validations for the DropdownActions component * pkp/pkp-lib#10033 Add validator for direction prop * pkp/pkp-lib#10033 Add select options for Dropdown controls in Storybook * pkp/pkp-lib#10033 Update documentation for the DropdownActions component * pkp/pkp-lib#10033 Add Accessibility section in DropdownActions documentation
- Loading branch information
1 parent
50e3204
commit 67733de
Showing
6 changed files
with
264 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import {Primary, Controls, Stories, Meta, Description} from '@storybook/blocks'; | ||
import DropdownActions from './DropdownActions.vue'; | ||
|
||
import * as DropdownActionsStories from './DropdownActions.stories.js'; | ||
|
||
<Meta of={DropdownActionsStories} /> | ||
|
||
# Dropdown Actions | ||
|
||
## Usage | ||
|
||
This component renders a dropdown menu that displays a list of actions. To use an ellipsis menu, set `displayAsEllipsis` to `true`. Otherwise, the `label` prop will be used as the dropdown button text. | ||
|
||
## Accesibility | ||
|
||
When the ellipsis menu is enabled by setting `displayAsEllipsis` to `true`, the `label` prop provides descriptive text for the "More Options" icon. In this case, the `ariaLabel` prop can be used to further specify the function of the button for screen readers. | ||
|
||
For dropdowns with text, you can provide an `ariaLabel` prop to set the `aria-label` attribute on the dropdown button, ensuring that it is fully described for screen readers. This enhances accessibility without relying solely on the surrounding visual context. | ||
|
||
<Primary /> | ||
<Controls /> | ||
<Stories /> |
125 changes: 125 additions & 0 deletions
125
src/components/DropdownActions/DropdownActions.stories.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import DropdownActions from './DropdownActions.vue'; | ||
import {within, userEvent} from '@storybook/test'; | ||
|
||
export default { | ||
title: 'Components/DropdownActions', | ||
component: DropdownActions, | ||
render: (args) => ({ | ||
components: {DropdownActions}, | ||
setup() { | ||
return {args}; | ||
}, | ||
template: '<DropdownActions v-bind="args" />', | ||
}), | ||
argTypes: { | ||
direction: { | ||
control: {type: 'select'}, | ||
options: ['left', 'right'], | ||
}, | ||
}, | ||
decorators: [ | ||
() => ({ | ||
template: | ||
'<div style="height: 300px; padding: 10px;" class="text-center"><story/></div>', | ||
}), | ||
], | ||
}; | ||
|
||
const downloadActions = [ | ||
{ | ||
label: 'Author-Only Sections Displayed (PDF)', | ||
name: 'authorPdf', | ||
}, | ||
{ | ||
label: 'Author-Only Sections Displayed (XML)', | ||
name: 'authorXml', | ||
}, | ||
{ | ||
label: 'Editor Forms Shows All Review Sections (PDF)', | ||
name: 'editorPdf', | ||
}, | ||
{ | ||
label: 'Editor Forms Shows All Review Sections (XML)', | ||
name: 'editorXml', | ||
}, | ||
]; | ||
|
||
export const Default = { | ||
args: { | ||
actions: downloadActions, | ||
label: 'Download Review Form', | ||
ariaLabel: 'Click to download content in the available formats', | ||
direction: 'left', | ||
}, | ||
play: async ({canvasElement}) => { | ||
// Assigns canvas to the component root element | ||
const canvas = within(canvasElement); | ||
const user = userEvent.setup(); | ||
|
||
await user.click(canvas.getByText('Download Review Form')); | ||
}, | ||
}; | ||
|
||
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', | ||
}, | ||
], | ||
label: 'User management options', | ||
displayAsEllipsis: true, | ||
direction: 'left', | ||
}, | ||
play: async ({canvasElement}) => { | ||
// Assigns canvas to the component root element | ||
const canvas = within(canvasElement); | ||
const user = userEvent.setup(); | ||
|
||
await user.click(canvas.getByText('User management options')); | ||
}, | ||
}; | ||
|
||
export const RightAlignedMenu = { | ||
args: { | ||
actions: downloadActions, | ||
label: 'Right Aligned Menu', | ||
direction: 'right', | ||
}, | ||
play: async ({canvasElement}) => { | ||
// Assigns canvas to the component root element | ||
const canvas = within(canvasElement); | ||
const user = userEvent.setup(); | ||
|
||
await user.click(canvas.getByText('Right Aligned Menu')); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
<template> | ||
<div class="relative inline-block items-start justify-between"> | ||
<Menu as="div"> | ||
<div> | ||
<MenuButton | ||
class="hover:bg-gray-50 inline-flex w-full justify-center gap-x-1.5 rounded px-3 py-2" | ||
:class=" | ||
displayAsEllipsis | ||
? 'text-3xl-normal' | ||
: 'border border-light bg-secondary text-lg-normal' | ||
" | ||
:aria-label="ariaLabel || null" | ||
> | ||
<span :class="displayAsEllipsis ? 'sr-only' : ''">{{ label }}</span> | ||
<Icon | ||
class="-mr-1 h-5 w-5 text-primary" | ||
:icon="displayAsEllipsis ? 'MoreOptions' : 'Dropdown'" | ||
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="absolute z-10 w-max border border-light bg-secondary shadow focus:outline-none" | ||
:class=" | ||
direction === 'right' | ||
? 'ltr:left-0 ltr:origin-top-left rtl:right-0 rtl:origin-top-left' | ||
: 'ltr:right-0 ltr:origin-top-right rtl:left-0 rtl:origin-top-right' | ||
" | ||
> | ||
<MenuItem v-for="(action, i) in actions" :key="i" v-slot="{active}"> | ||
<div class="min-w-[96px]"> | ||
<PkpButton | ||
v-if="isValidAction(action)" | ||
:element="action.url ? 'a' : 'button'" | ||
: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" | ||
@click="action.name" | ||
> | ||
{{ action.label }} | ||
</PkpButton> | ||
</div> | ||
</MenuItem> | ||
</MenuItems> | ||
</transition> | ||
</Menu> | ||
</div> | ||
</template> | ||
|
||
<script setup> | ||
import {Menu, MenuButton, MenuItem, MenuItems} from '@headlessui/vue'; | ||
defineProps({ | ||
/** An array of action objects. Each object should contain `label` (string), `url` (string) to navigate to if the action involves a link, or `name` (string) to perform the action when clicked, an optional `icon` (string) and `isWarnable` (boolean) if the button needs the "warning" button styling from `<Button>` component. */ | ||
actions: { | ||
type: Array, | ||
required: true, | ||
validator: (actions) => { | ||
return actions.every((action) => { | ||
const hasLabel = | ||
typeof action.label === 'string' && action.label.trim() !== ''; | ||
const hasAction = action.url || action.name; | ||
return hasLabel && hasAction; | ||
}); | ||
}, | ||
}, | ||
/** The text label for the button. This is required. If `displayAsEllipsis` is `true`, the label will be used for accessibility. */ | ||
label: { | ||
type: String, | ||
required: true, | ||
}, | ||
/** If `true`, the button will display an ellipsis (`...`) */ | ||
displayAsEllipsis: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
/** The accessible label for the button, used by screen readers. This is optional. */ | ||
ariaLabel: { | ||
type: String, | ||
default: '', | ||
}, | ||
/** This specifies where the dropdown appears relative to the element, such as "left" or "right." */ | ||
direction: { | ||
type: String, | ||
default: 'left', | ||
validator: (direction) => ['left', 'right'].includes(direction), | ||
}, | ||
}); | ||
const isValidAction = (action) => { | ||
return action?.label && (action?.url || action?.name); | ||
}; | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<template> | ||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
<path | ||
d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z" | ||
fill="currentColor" | ||
/> | ||
</svg> | ||
</template> |