-
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
diff --git a/src/components/SideMenu/SideMenu.stories.js b/src/components/SideMenu/SideMenu.stories.js
index df74ff0e0..78b61da34 100644
--- a/src/components/SideMenu/SideMenu.stories.js
+++ b/src/components/SideMenu/SideMenu.stories.js
@@ -1,4 +1,5 @@
import SideMenu from './SideMenu.vue';
+import {useSideMenu} from '@/composables/useSideMenu.js';
export default {
title: 'Components/SideMenu',
@@ -6,6 +7,13 @@ export default {
render: (args) => ({
components: {SideMenu},
setup() {
+ const {activeItemKey, setExpandedKeys} = useSideMenu('action_required');
+
+ args.expandedKeys = setExpandedKeys([
+ 'action_required',
+ 'editorial_dashboard',
+ ]);
+
function startNewSubmission(actionArgs) {
console.log('startNewSubmission clicked', actionArgs);
}
@@ -26,9 +34,10 @@ export default {
console.error(`No handler for action: ${action}`);
}
}
- return {args, handleActions};
+ return {args, activeItemKey, handleActions};
},
- template: '',
+ template:
+ '',
}),
};
@@ -37,13 +46,13 @@ export const Default = {
items: [
{
label: 'Editorial Dashboard',
+ key: 'editorial_dashboard',
icon: 'Dashboard',
- isCurrent: false,
items: [
{
label: 'Action required by me',
+ key: 'action_required',
link: '#',
- isCurrent: true,
badge: {
slot: 20,
isWarnable: true,
@@ -51,56 +60,56 @@ export const Default = {
},
{
label: 'Assigned to me',
+ key: 'assigned_to_me',
link: '#',
- isCurrent: false,
badge: {
slot: 45,
},
},
{
label: 'Active submissions',
+ key: 'active_submissions',
link: '#',
- isCurrent: false,
badge: {
slot: 100,
},
},
{
label: 'All in submission stage',
+ key: 'all_in_submission_stage',
link: '#',
- isCurrent: false,
badge: {
slot: 35,
},
},
{
label: 'Needs reviewers',
+ key: 'needs_reviewers',
link: '#',
- isCurrent: false,
badge: {
slot: 5,
},
},
{
label: 'Awaiting reviews',
+ key: 'awaiting_reviews',
link: '#',
- isCurrent: false,
badge: {
slot: 6,
},
},
{
label: 'Reviews submitted',
+ key: 'reviews_submitted',
link: '#',
- isCurrent: false,
badge: {
slot: 4,
},
},
{
label: 'Reviews overdue',
+ key: 'reviews_overdue',
link: '#',
- isCurrent: false,
badge: {
slot: 10,
isWarnable: true,
@@ -108,64 +117,64 @@ export const Default = {
},
{
label: 'Author revisions submitted',
+ key: 'author_revisions_submitted',
link: '#',
- isCurrent: false,
badge: {
slot: 5,
},
},
{
label: 'All in review stage',
+ key: 'all_in_review_stage',
link: '#',
- isCurrent: false,
badge: {
slot: 10,
},
},
{
label: 'All in copyediting stage',
+ key: 'all_in_copyediting_stage',
link: '#',
- isCurrent: false,
badge: {
slot: 10,
},
},
{
label: 'All in production stage',
+ key: 'all_in_production_stage',
link: '#',
- isCurrent: false,
badge: {
slot: 12,
},
},
{
label: 'Scheduled for publishing',
+ key: 'scheduled_for_publishing',
link: '#',
- isCurrent: false,
badge: {
slot: 16,
},
},
{
label: 'Published',
+ key: 'published',
link: '#',
- isCurrent: false,
badge: {
slot: 75,
},
},
{
label: 'Declined',
+ key: 'declined',
link: '#',
- isCurrent: false,
badge: {
slot: 10,
},
},
{
label: 'Start a New Submission',
+ key: 'start_a_new_submission',
link: '#',
- isCurrent: false,
action: 'startNewSubmission',
actionArgs: {
param1: 1,
@@ -176,38 +185,38 @@ export const Default = {
},
{
label: 'My Assignments as Reviewer',
+ key: 'my_assignments_as_reviewer',
icon: 'ReviewAssignments',
- isCurrent: false,
link: '#',
},
{
label: 'My Submissions as Author',
+ key: 'my_submissions_as_author',
icon: 'MySubmissions',
- isCurrent: false,
link: '#',
},
{
label: 'Issues',
+ key: 'issues',
icon: 'Issues',
- isCurrent: false,
link: '#',
},
{
label: 'Announcements',
+ key: 'announcements',
icon: 'Announcements',
- isCurrent: false,
link: '#',
},
{
label: 'DOIs',
+ key: 'dois',
icon: 'NavDoi',
- isCurrent: false,
link: '#',
},
{
label: 'Institutes',
+ key: 'institutes',
icon: 'Institutes',
- isCurrent: false,
link: '#',
},
],
@@ -215,58 +224,72 @@ export const Default = {
};
export const WithColorStripe = {
+ render: (args) => ({
+ components: {SideMenu},
+ setup() {
+ const activeItemKey = 'submission_stages';
+ const expandedKeys = ['submission_stages'];
+ const {setExpandedKeys} = useSideMenu(activeItemKey);
+
+ args.activeItemKey = activeItemKey;
+ args.expandedKeys = setExpandedKeys(expandedKeys);
+ return {args};
+ },
+ template:
+ '',
+ }),
args: {
items: [
{
label: 'Submission Stages',
- isCurrent: true,
+ key: 'submission_stages',
items: [
{
label: 'Desk Review',
+ key: 'desk_review',
link: '#',
- isCurrent: false,
colorStripe: 'border-stage-desk-review',
},
{
label: 'Review',
+ key: 'review',
link: '#',
- isCurrent: false,
colorStripe: 'border-stage-in-review',
},
{
label: 'Copyediting',
+ key: 'copyediting',
link: '#',
- isCurrent: false,
colorStripe: 'border-stage-copyediting',
},
{
label: 'Production',
+ key: 'production',
link: '#',
- isCurrent: false,
colorStripe: 'border-stage-production',
},
{
label: 'Scheduled',
+ key: 'scheduled',
link: '#',
- isCurrent: false,
colorStripe: 'border-stage-scheduled-for-publishing',
},
{
label: 'Incomoplete Submission',
+ key: 'incomplete_submission',
link: '#',
- isCurrent: false,
colorStripe: 'border-stage-incomplete-submission',
},
{
label: 'Published',
+ key: 'published',
link: '#',
- isCurrent: false,
colorStripe: 'border-stage-published',
},
{
label: 'Declined',
+ key: 'declined',
link: '#',
- isCurrent: false,
colorStripe: 'border-stage-declined',
},
],
@@ -276,88 +299,102 @@ export const WithColorStripe = {
};
export const ExpandedMenu = {
+ render: (args) => ({
+ components: {SideMenu},
+ setup() {
+ const activeItemKey = 'review_round_1';
+ const expandedKeys = ['workflow', 'review', 'publication'];
+ const {setExpandedKeys} = useSideMenu(activeItemKey);
+
+ args.activeItemKey = activeItemKey;
+ args.expandedKeys = setExpandedKeys(expandedKeys);
+ return {args};
+ },
+ template:
+ '',
+ }),
args: {
items: [
{
label: 'Workflow',
- icon: 'thumbs-up',
- isCurrent: false,
+ key: 'workflow',
+ icon: 'Dashboard',
items: [
{
label: 'Submission',
+ key: 'submission',
link: '#',
- isCurrent: false,
colorStripe: 'border-stage-desk-review',
},
{
label: 'Review',
+ key: 'review',
link: '#',
- isCurrent: false,
items: [
{
label: 'Review Round 1',
+ key: 'review_round_1',
link: '#',
- isCurrent: true,
},
{
label: 'New Review Round',
+ key: 'new_review_round',
link: '#',
- isCurrent: false,
},
],
},
{
label: 'Copyediting',
+ key: 'copyediting',
link: '#',
- isCurrent: false,
},
{
label: 'Production',
+ key: 'production',
link: '#',
- isCurrent: false,
},
],
},
{
label: 'Publication',
+ key: 'publication',
icon: 'MySubmissions',
- isCurrent: false,
isOpen: true,
items: [
{
label: 'Title & Abstract',
+ key: 'title_and_abstract',
link: '#',
- isCurrent: false,
},
{
label: 'Contributors',
+ key: 'contributors',
link: '#',
- isCurrent: false,
},
{
label: 'Metadata',
+ key: 'metadata',
link: '#',
- isCurrent: false,
},
{
label: 'References',
+ key: 'references',
link: '#',
- isCurrent: false,
},
{
label: 'Galleys',
+ key: 'galleys',
link: '#',
- isCurrent: false,
},
{
label: 'Permissions & Disclosure',
+ key: 'permissions_and_disclosure',
link: '#',
- isCurrent: false,
},
{
label: 'Issue',
+ key: 'issue',
link: '#',
- isCurrent: false,
},
],
},
diff --git a/src/components/SideMenu/SideMenu.vue b/src/components/SideMenu/SideMenu.vue
index b04238682..8367765b6 100644
--- a/src/components/SideMenu/SideMenu.vue
+++ b/src/components/SideMenu/SideMenu.vue
@@ -1,6 +1,6 @@
@@ -38,18 +38,17 @@
import PanelMenu from 'primevue/panelmenu';
import Icon from '../Icon/Icon.vue';
import Badge from '../Badge/Badge.vue';
-import {ref, reactive} from 'vue';
+import {ref, reactive, watch} from 'vue';
const props = defineProps({
/**
* An array of item objects for the SideMenu.
* Each object should contain:
- * - `label` (string): The label of the menu item.
+ * - `label` (string, required): The label of the menu item.
+ * - `key` (string, required): A unique key for each item.
* - `link` (string, optional): The URL to navigate to when the item is clicked.
* - `action` (string, optional): A function to be executed when the item is clicked.
* - `actionArgs` (object, optional): An object to be passed as function arguments when the `item.action` is emitted.
- * - `isCurrent` (boolean, optional): Marks the item as the current selection.
- * - `isOpen` (boolean, optional): Identifies if the menu should be expanded.
* - `badge` (object, optional): Contains `slot` (string) and other props for `` customization.
* - `colorStripe` (string, optional): A border class to add a color stripe to the item.
* - `items` (array, optional): An array of child items for nested menus.
@@ -61,38 +60,58 @@ const props = defineProps({
return items.every((item) => {
const hasLabel =
typeof item.label === 'string' && item.label.trim() !== '';
+ const hasKey = typeof item.key === 'string' && item.key.trim() !== '';
const validItem = item.link || item.action || item.items;
- return hasLabel && validItem;
+ return hasLabel && hasKey && validItem;
});
},
},
+ expandedKeys: {
+ type: Object,
+ default: () => {},
+ },
+ activeItemKey: {
+ type: String,
+ required: true,
+ validator: (value) => !!value,
+ },
});
const emit = defineEmits([
/** When a panel menu item's "action" is clicked */
'action',
+ /** When the localActiveItemKey value changes */
+ 'update:activeItemKey',
]);
-const activeItemKey = ref('');
+const localActiveItemKey = ref(props.activeItemKey);
+watch(
+ () => props.activeItemKey,
+ (newActiveItemKey) => {
+ localActiveItemKey.value = newActiveItemKey;
+ },
+);
-// Maps the key and level attributes which are necessary to render the nested menu
-function mapItems(_items, level = 1, _key) {
+const localExpandedKeys = ref({...props.expandedKeys});
+watch(
+ () => props.expandedKeys,
+ (newExpandedKeys) => {
+ localExpandedKeys.value = {...newExpandedKeys};
+ },
+);
+
+// Maps the level attributes which are necessary to render the nested menu
+function mapItems(_items, level = 1) {
const result = [];
- _items.forEach((_item, index) => {
- const key = _key ? `${_key}_${index}` : `item_${index}`;
+ _items.forEach((_item) => {
const item = {
..._item,
- key,
level,
};
if (_item.items) {
- item.items = mapItems(_item.items, level + 1, key);
- }
-
- if (item.isCurrent) {
- activeItemKey.value = key;
+ item.items = mapItems(_item.items, level + 1);
}
result.push(item);
@@ -103,34 +122,6 @@ function mapItems(_items, level = 1, _key) {
const items = reactive(mapItems(props.items));
-// Defines the list of expanded keys, so it renders correctly when the component loads
-function getExpandedKeys(items) {
- const _expandedKeys = {};
-
- function markExpanded(items) {
- let found = false;
- for (const item of items) {
- if (item.isCurrent || item.isOpen) {
- _expandedKeys[item.key] = true;
- found = true;
- }
-
- const isMarkExpanded = item.items && markExpanded(item.items);
-
- if (item.items && isMarkExpanded) {
- _expandedKeys[item.key] = true;
- found = true;
- }
- }
- return found;
- }
-
- markExpanded(items);
- return _expandedKeys;
-}
-
-const expandedKeys = ref(getExpandedKeys(items));
-
const navigationStyling = {
headerContent: () => {
return {
@@ -157,16 +148,16 @@ const navigationStyling = {
};
function handleClick(item) {
- // set the current active item key
- activeItemKey.value = item.key;
+ localActiveItemKey.value = item.key;
+ emit('update:activeItemKey', localActiveItemKey.value);
if (item.action) {
- emit('action', item.action, item.actionArgs);
+ emit('action', item.action, {...item.actionArgs, key: item.key});
}
}
function isActive(item) {
- return item?.key === activeItemKey.value;
+ return localActiveItemKey.value && item?.key === localActiveItemKey.value;
}
function getButtonStyles(item) {
@@ -174,9 +165,9 @@ function getButtonStyles(item) {
const style = {
// Base
- 'inline-flex relative items-center gap-x-1 text-lg-semibold py-2 px-3 w-full border-b border-b-light': true,
+ 'inline-flex relative items-center gap-x-1 text-lg-medium py-2 px-3 w-full border-b border-b-light': true,
// Default button styling
- 'text-primary border-light hover:text-hover disabled:text-disabled bg-secondary':
+ 'text-primary border-light hover:text-hover disabled:text-disabled bg-secondary':
!isActiveItem,
// Active
'text-on-dark bg-selection-dark': isActiveItem,
@@ -191,6 +182,8 @@ function getButtonStyles(item) {
// Additional border styling
'border-t border-t-light': item.key === 'item_0',
'border-s-8': item.colorStripe,
+ // Items with children
+ '!text-lg-bold': item.items && item.level === 1,
};
// set the additional class if the button should include a color stripe
diff --git a/src/components/SideNav/SideNav.mdx b/src/components/SideNav/SideNav.mdx
index 044c897a0..a4da9ea5f 100644
--- a/src/components/SideNav/SideNav.mdx
+++ b/src/components/SideNav/SideNav.mdx
@@ -5,7 +5,7 @@ import * as SideNavStories from './SideNav.stories.js';
-# Side Menu
+# Side Nav
## Usage
diff --git a/src/components/SideNav/SideNav.stories.js b/src/components/SideNav/SideNav.stories.js
index a42957a98..ddfe1e0bd 100644
--- a/src/components/SideNav/SideNav.stories.js
+++ b/src/components/SideNav/SideNav.stories.js
@@ -93,19 +93,19 @@ export const Default = {
isCurrent: false,
icon: '',
submenu: {
- item1: {
+ item2_1: {
name: 'Item 2.1',
url: '#',
isCurrent: false,
icon: '',
},
- item2: {
+ item2_2: {
name: 'Item 2.2',
url: '#',
isCurrent: false,
icon: '',
},
- item3: {
+ item2_3: {
name: 'Item 2.3',
url: '#',
isCurrent: true,
diff --git a/src/components/SideNav/SideNav.vue b/src/components/SideNav/SideNav.vue
index 63c1646b7..876f36f60 100644
--- a/src/components/SideNav/SideNav.vue
+++ b/src/components/SideNav/SideNav.vue
@@ -1,16 +1,21 @@
diff --git a/src/composables/useSideMenu.js b/src/composables/useSideMenu.js
new file mode 100644
index 000000000..655976f06
--- /dev/null
+++ b/src/composables/useSideMenu.js
@@ -0,0 +1,25 @@
+import {ref} from 'vue';
+
+export function useSideMenu(_activeItemKey = '', _expandedKeys = {}) {
+ const expandedKeys = ref(_expandedKeys);
+ const activeItemKey = ref(_activeItemKey);
+
+ const setExpandedKeys = (keys = []) => {
+ // reset expandedKeys
+ expandedKeys.value = {};
+
+ keys.forEach((key) => (expandedKeys.value[key] = true));
+ return expandedKeys;
+ };
+
+ const setActiveItemKey = (key = '') => {
+ activeItemKey.value = key;
+ };
+
+ return {
+ expandedKeys,
+ activeItemKey,
+ setExpandedKeys,
+ setActiveItemKey,
+ };
+}
diff --git a/src/composables/useSideMenu.mdx b/src/composables/useSideMenu.mdx
new file mode 100644
index 000000000..a8cd8287a
--- /dev/null
+++ b/src/composables/useSideMenu.mdx
@@ -0,0 +1,47 @@
+# useSideMenu
+
+`useSideMenu` is a composable function that helps manage the state of `activeItemKey` and `expandedKeys` for a [SideMenu](../?path=/docs/components-sidemenu--docs) component.
+
+```javascript
+const {activeItemKey, setActiveItemKey} = useSideMenu('menuItem1_1', {
+ menuItem1: true,
+ menuItem2: true,
+});
+```
+
+## setActiveItemKey
+
+This function allows you to set a new active item key dynamically from within your component or from other components that consume the `useSideMenu` composable. It accepts a string that represents the key of the item you want to set as active. This value will be assigned to the `activeItemKey` ref.
+
+```javascript
+const {activeItemKey, setActiveItemKey} = useSideMenu('menuItem1_1', {
+ menuItem1: true,
+ menuItem2: true,
+});
+
+function someEvent() {
+ setActiveItemKey('menuItem1_2');
+}
+```
+
+## setExpandedKeys
+
+It allows you to dynamically set which keys should be expanded in the `SideMenu` component. It accepts an array of strings where each string represents a key that should be expanded in the side menu.
+
+```javascript
+const {expandedKeys, setExpandedKeys} = useSideMenu('menuItem1_1', {
+ menuItem1: true,
+ menuItem2: true,
+});
+
+function someEvent() {
+ setExpandedKeys(['menuItem3']);
+ // this will set the expandedKeys ref to { menuItem3: true }
+}
+```
+
+### Import
+
+```javascript
+import {useSideMenu} from '@/composables/useSideMenu.js';
+```