Refactor #3965 - For Menu

pull/4004/head
Tuğçe Küçükoğlu 2023-05-29 12:09:48 +03:00
parent 67ec99ccd3
commit ce413c9a91
4 changed files with 163 additions and 121 deletions

View File

@ -0,0 +1,114 @@
<script>
import BaseComponent from 'primevue/basecomponent';
import { useStyle } from 'primevue/usestyle';
const styles = `
.p-menu ul {
margin: 0;
padding: 0;
list-style: none;
}
.p-menu .p-menuitem-link {
cursor: pointer;
display: flex;
align-items: center;
text-decoration: none;
overflow: hidden;
position: relative;
}
.p-menu .p-menuitem-text {
line-height: 1;
}
`;
const classes = {
root: ({ instance, props }) => [
'p-menu p-component',
{
'p-menu-overlay': props.popup,
'p-input-filled': instance.$primevue.config.inputStyle === 'filled',
'p-ripple-disabled': instance.$primevue.config.ripple === false
}
],
start: 'p-menu-start',
menu: 'p-menu-list p-reset',
submenuHeader: 'p-submenu-header',
separator: 'p-menuitem-separator',
end: 'p-menu-end',
menuitem: ({ context }) => [
'p-menuitem',
{
'p-focus': context.id === context.focusedOptionId,
'p-disabled': context.disabled()
}
],
content: 'p-menuitem-content',
action: ({ context, routerProps }) => [
'p-menuitem-link',
{
'router-link-active': routerProps && routerProps.isActive,
'router-link-active-exact': context.exact && routerProps && routerProps.isExactActive
}
],
icon: 'p-menuitem-icon',
label: 'p-menuitem-text'
};
const { load: loadStyle, unload: unloadStyle } = useStyle(styles, { id: 'primevue_menu_style', manual: true });
export default {
name: 'BaseMenu',
extends: BaseComponent,
props: {
popup: {
type: Boolean,
default: false
},
model: {
type: Array,
default: null
},
appendTo: {
type: String,
default: 'body'
},
autoZIndex: {
type: Boolean,
default: true
},
baseZIndex: {
type: Number,
default: 0
},
exact: {
type: Boolean,
default: true
},
tabindex: {
type: Number,
default: 0
},
'aria-label': {
type: String,
default: null
},
'aria-labelledby': {
type: String,
default: null
}
},
css: {
classes
},
watch: {
isUnstyled: {
immediate: true,
handler(newValue) {
!newValue && loadStyle();
}
}
}
};
</script>

View File

@ -169,6 +169,11 @@ export interface MenuProps {
* @type {MenuPassThroughOptions} * @type {MenuPassThroughOptions}
*/ */
pt?: MenuPassThroughOptions; pt?: MenuPassThroughOptions;
/**
* When enabled, it removes component related styles in the core.
* @defaultValue false
*/
unstyled?: boolean;
} }
/** /**

View File

@ -1,14 +1,14 @@
<template> <template>
<Portal :appendTo="appendTo" :disabled="!popup"> <Portal :appendTo="appendTo" :disabled="!popup">
<transition name="p-connected-overlay" @enter="onEnter" @leave="onLeave" @after-leave="onAfterLeave"> <transition name="p-connected-overlay" @enter="onEnter" @leave="onLeave" @after-leave="onAfterLeave">
<div v-if="popup ? overlayVisible : true" :ref="containerRef" :id="id" :class="containerClass" @click="onOverlayClick" v-bind="{ ...$attrs, ...ptm('root') }"> <div v-if="popup ? overlayVisible : true" :ref="containerRef" :id="id" :class="cx('root')" @click="onOverlayClick" v-bind="{ ...$attrs, ...ptm('root') }" data-pc-name="menu">
<div v-if="$slots.start" class="p-menu-start" v-bind="ptm('start')"> <div v-if="$slots.start" :class="cx('start')" v-bind="ptm('start')">
<slot name="start"></slot> <slot name="start"></slot>
</div> </div>
<ul <ul
:ref="listRef" :ref="listRef"
:id="id + '_list'" :id="id + '_list'"
class="p-menu-list p-reset" :class="cx('menu')"
role="menu" role="menu"
:tabindex="tabindex" :tabindex="tabindex"
:aria-activedescendant="focused ? focusedOptionId : undefined" :aria-activedescendant="focused ? focusedOptionId : undefined"
@ -21,19 +21,19 @@
> >
<template v-for="(item, i) of model" :key="label(item) + i.toString()"> <template v-for="(item, i) of model" :key="label(item) + i.toString()">
<template v-if="item.items && visible(item) && !item.separator"> <template v-if="item.items && visible(item) && !item.separator">
<li v-if="item.items" :id="id + '_' + i" class="p-submenu-header" role="none" v-bind="ptm('submenuHeader')"> <li v-if="item.items" :id="id + '_' + i" :class="cx('submenuHeader')" role="none" v-bind="ptm('submenuHeader')">
<slot name="item" :item="item">{{ label(item) }}</slot> <slot name="item" :item="item">{{ label(item) }}</slot>
</li> </li>
<template v-for="(child, j) of item.items" :key="child.label + i + '_' + j"> <template v-for="(child, j) of item.items" :key="child.label + i + '_' + j">
<PVMenuitem v-if="visible(child) && !child.separator" :id="id + '_' + i + '_' + j" :item="child" :templates="$slots" :exact="exact" :focusedOptionId="focusedOptionId" @item-click="itemClick" :pt="pt" /> <PVMenuitem v-if="visible(child) && !child.separator" :id="id + '_' + i + '_' + j" :item="child" :templates="$slots" :exact="exact" :focusedOptionId="focusedOptionId" @item-click="itemClick" :pt="pt" />
<li v-else-if="visible(child) && child.separator" :key="'separator' + i + j" :class="separatorClass(item)" :style="child.style" role="separator" v-bind="ptm('separator')"></li> <li v-else-if="visible(child) && child.separator" :key="'separator' + i + j" :class="[cx('separator'), item.class]" :style="child.style" role="separator" v-bind="ptm('separator')"></li>
</template> </template>
</template> </template>
<li v-else-if="visible(item) && item.separator" :key="'separator' + i.toString()" :class="separatorClass(item)" :style="item.style" role="separator" v-bind="ptm('separator')"></li> <li v-else-if="visible(item) && item.separator" :key="'separator' + i.toString()" :class="[cx('separator'), item.class]" :style="item.style" role="separator" v-bind="ptm('separator')"></li>
<PVMenuitem v-else :key="label(item) + i.toString()" :id="id + '_' + i" :item="item" :templates="$slots" :exact="exact" :focusedOptionId="focusedOptionId" @item-click="itemClick" :pt="pt" /> <PVMenuitem v-else :key="label(item) + i.toString()" :id="id + '_' + i" :item="item" :templates="$slots" :exact="exact" :focusedOptionId="focusedOptionId" @item-click="itemClick" :pt="pt" />
</template> </template>
</ul> </ul>
<div v-if="$slots.end" class="p-menu-end" v-bind="ptm('end')"> <div v-if="$slots.end" :class="cx('end')" v-bind="ptm('end')">
<slot name="end"></slot> <slot name="end"></slot>
</div> </div>
</div> </div>
@ -42,55 +42,17 @@
</template> </template>
<script> <script>
import BaseComponent from 'primevue/basecomponent';
import OverlayEventBus from 'primevue/overlayeventbus'; import OverlayEventBus from 'primevue/overlayeventbus';
import Portal from 'primevue/portal'; import Portal from 'primevue/portal';
import { ConnectedOverlayScrollHandler, DomHandler, UniqueComponentId, ZIndexUtils } from 'primevue/utils'; import { ConnectedOverlayScrollHandler, DomHandler, UniqueComponentId, ZIndexUtils } from 'primevue/utils';
import BaseMenu from './BaseMenu.vue';
import Menuitem from './Menuitem.vue'; import Menuitem from './Menuitem.vue';
export default { export default {
name: 'Menu', name: 'Menu',
extends: BaseComponent, extends: BaseMenu,
inheritAttrs: false, inheritAttrs: false,
emits: ['show', 'hide', 'focus', 'blur'], emits: ['show', 'hide', 'focus', 'blur'],
props: {
popup: {
type: Boolean,
default: false
},
model: {
type: Array,
default: null
},
appendTo: {
type: String,
default: 'body'
},
autoZIndex: {
type: Boolean,
default: true
},
baseZIndex: {
type: Number,
default: 0
},
exact: {
type: Boolean,
default: true
},
tabindex: {
type: Number,
default: 0
},
'aria-label': {
type: String,
default: null
},
'aria-labelledby': {
type: String,
default: null
}
},
data() { data() {
return { return {
id: this.$attrs.id, id: this.$attrs.id,
@ -238,12 +200,12 @@ export default {
event.preventDefault(); event.preventDefault();
}, },
onEndKey(event) { onEndKey(event) {
this.changeFocusedOptionIndex(DomHandler.find(this.container, 'li.p-menuitem:not(.p-disabled)').length - 1); this.changeFocusedOptionIndex(DomHandler.find(this.container, 'li[data-pc-section="menuitem"][data-p-disabled="false"]').length - 1);
event.preventDefault(); event.preventDefault();
}, },
onEnterKey(event) { onEnterKey(event) {
const element = DomHandler.findSingle(this.list, `li[id="${`${this.focusedOptionIndex}`}"]`); const element = DomHandler.findSingle(this.list, `li[id="${`${this.focusedOptionIndex}`}"]`);
const anchorElement = element && DomHandler.findSingle(element, '.p-menuitem-link'); const anchorElement = element && DomHandler.findSingle(element, 'a[data-pc-section="action"]');
this.popup && DomHandler.focus(this.target); this.popup && DomHandler.focus(this.target);
anchorElement ? anchorElement.click() : element && element.click(); anchorElement ? anchorElement.click() : element && element.click();
@ -254,19 +216,19 @@ export default {
this.onEnterKey(event); this.onEnterKey(event);
}, },
findNextOptionIndex(index) { findNextOptionIndex(index) {
const links = DomHandler.find(this.container, 'li.p-menuitem:not(.p-disabled)'); const links = DomHandler.find(this.container, 'li[data-pc-section="menuitem"][data-p-disabled="false"]');
const matchedOptionIndex = [...links].findIndex((link) => link.id === index); const matchedOptionIndex = [...links].findIndex((link) => link.id === index);
return matchedOptionIndex > -1 ? matchedOptionIndex + 1 : 0; return matchedOptionIndex > -1 ? matchedOptionIndex + 1 : 0;
}, },
findPrevOptionIndex(index) { findPrevOptionIndex(index) {
const links = DomHandler.find(this.container, 'li.p-menuitem:not(.p-disabled)'); const links = DomHandler.find(this.container, 'li[data-pc-section="menuitem"][data-p-disabled="false"]');
const matchedOptionIndex = [...links].findIndex((link) => link.id === index); const matchedOptionIndex = [...links].findIndex((link) => link.id === index);
return matchedOptionIndex > -1 ? matchedOptionIndex - 1 : 0; return matchedOptionIndex > -1 ? matchedOptionIndex - 1 : 0;
}, },
changeFocusedOptionIndex(index) { changeFocusedOptionIndex(index) {
const links = DomHandler.find(this.container, 'li.p-menuitem:not(.p-disabled)'); const links = DomHandler.find(this.container, 'li[data-pc-section="menuitem"][data-p-disabled="false"]');
let order = index >= links.length ? links.length - 1 : index < 0 ? 0 : index; let order = index >= links.length ? links.length - 1 : index < 0 ? 0 : index;
order > -1 && (this.focusedOptionIndex = links[order].getAttribute('id')); order > -1 && (this.focusedOptionIndex = links[order].getAttribute('id'));
@ -284,6 +246,7 @@ export default {
this.target = null; this.target = null;
}, },
onEnter(el) { onEnter(el) {
DomHandler.addStyles(el, { position: 'absolute', top: '0', left: '0' });
this.alignOverlay(); this.alignOverlay();
this.bindOutsideClickListener(); this.bindOutsideClickListener();
this.bindResizeListener(); this.bindResizeListener();
@ -379,9 +342,6 @@ export default {
label(item) { label(item) {
return typeof item.label === 'function' ? item.label() : item.label; return typeof item.label === 'function' ? item.label() : item.label;
}, },
separatorClass(item) {
return ['p-menuitem-separator', item.class];
},
onOverlayClick(event) { onOverlayClick(event) {
OverlayEventBus.emit('overlay-click', { OverlayEventBus.emit('overlay-click', {
originalEvent: event, originalEvent: event,
@ -396,16 +356,6 @@ export default {
} }
}, },
computed: { computed: {
containerClass() {
return [
'p-menu p-component',
{
'p-menu-overlay': this.popup,
'p-input-filled': this.$primevue.config.inputStyle === 'filled',
'p-ripple-disabled': this.$primevue.config.ripple === false
}
];
},
focusedOptionId() { focusedOptionId() {
return this.focusedOptionIndex !== -1 ? this.focusedOptionIndex : null; return this.focusedOptionIndex !== -1 ? this.focusedOptionIndex : null;
} }
@ -416,30 +366,3 @@ export default {
} }
}; };
</script> </script>
<style>
.p-menu-overlay {
position: absolute;
top: 0;
left: 0;
}
.p-menu ul {
margin: 0;
padding: 0;
list-style: none;
}
.p-menu .p-menuitem-link {
cursor: pointer;
display: flex;
align-items: center;
text-decoration: none;
overflow: hidden;
position: relative;
}
.p-menu .p-menuitem-text {
line-height: 1;
}
</style>

View File

@ -1,18 +1,29 @@
<template> <template>
<li v-if="visible()" :id="id" :class="containerClass()" role="menuitem" :style="item.style" :aria-label="label()" :aria-disabled="disabled()" v-bind="getPTOptions('menuitem')"> <li
<div class="p-menuitem-content" @click="onItemClick($event)" v-bind="getPTOptions('content')"> v-if="visible()"
:id="id"
:class="[getCXOptions('menuitem'), item.class]"
role="menuitem"
:style="item.style"
:aria-label="label()"
:aria-disabled="disabled()"
v-bind="getPTOptions('menuitem')"
:data-p-focused="isItemFocused()"
:data-p-disabled="disabled() || false"
>
<div :class="getCXOptions('content')" @click="onItemClick($event)" v-bind="getPTOptions('content')">
<template v-if="!templates.item"> <template v-if="!templates.item">
<router-link v-if="item.to && !disabled()" v-slot="{ navigate, href, isActive, isExactActive }" :to="item.to" custom> <router-link v-if="item.to && !disabled()" v-slot="{ navigate, href, isActive, isExactActive }" :to="item.to" custom>
<a v-ripple :href="href" :class="linkClass({ isActive, isExactActive })" tabindex="-1" aria-hidden="true" @click="onItemActionClick($event, navigate)" v-bind="getPTOptions('action')"> <a v-ripple :href="href" :class="getCXOptions('action', { isActive, isExactActive })" tabindex="-1" aria-hidden="true" @click="onItemActionClick($event, navigate)" v-bind="getPTOptions('action')">
<component v-if="templates.itemicon" :is="templates.itemicon" :item="item" :class="iconClass" /> <component v-if="templates.itemicon" :is="templates.itemicon" :item="item" :class="[getCXOptions('icon'), item.icon]" />
<span v-else-if="item.icon" :class="iconClass" v-bind="getPTOptions('icon')" /> <span v-else-if="item.icon" :class="[getCXOptions('icon'), item.icon]" v-bind="getPTOptions('icon')" />
<span class="p-menuitem-text" v-bind="getPTOptions('label')">{{ label() }}</span> <span :class="getCXOptions('label')" v-bind="getPTOptions('label')">{{ label() }}</span>
</a> </a>
</router-link> </router-link>
<a v-else v-ripple :href="item.url" :class="linkClass()" :target="item.target" tabindex="-1" aria-hidden="true" v-bind="getPTOptions('action')"> <a v-else v-ripple :href="item.url" :class="getCXOptions('action')" :target="item.target" tabindex="-1" aria-hidden="true" v-bind="getPTOptions('action')">
<component v-if="templates.itemicon" :is="templates.itemicon" :item="item" :class="iconClass" /> <component v-if="templates.itemicon" :is="templates.itemicon" :item="item" :class="[getCXOptions('icon'), item.icon]" />
<span v-else-if="item.icon" :class="iconClass" v-bind="getPTOptions('icon')" /> <span v-else-if="item.icon" :class="[getCXOptions('icon'), item.icon]" v-bind="getPTOptions('icon')" />
<span class="p-menuitem-text" v-bind="getPTOptions('label')">{{ label() }}</span> <span :class="getCXOptions('label')" v-bind="getPTOptions('label')">{{ label() }}</span>
</a> </a>
</template> </template>
<component v-else :is="templates.item" :item="item"></component> <component v-else :is="templates.item" :item="item"></component>
@ -21,13 +32,13 @@
</template> </template>
<script> <script>
import BaseComponent from 'primevue/basecomponent';
import Ripple from 'primevue/ripple'; import Ripple from 'primevue/ripple';
import { ObjectUtils } from 'primevue/utils'; import { ObjectUtils } from 'primevue/utils';
import BaseMenu from './BaseMenu.vue';
export default { export default {
name: 'Menuitem', name: 'Menuitem',
extends: BaseComponent, extends: BaseMenu,
inheritAttrs: false, inheritAttrs: false,
emits: ['item-click'], emits: ['item-click'],
props: { props: {
@ -48,6 +59,12 @@ export default {
} }
}); });
}, },
getCXOptions(key, params) {
return this.cx(key, {
...params,
context: this
});
},
isItemFocused() { isItemFocused() {
return this.focusedOptionId === this.id; return this.focusedOptionId === this.id;
}, },
@ -60,18 +77,6 @@ export default {
command && command({ originalEvent: event, item: this.item.item }); command && command({ originalEvent: event, item: this.item.item });
this.$emit('item-click', { originalEvent: event, item: this.item, id: this.id }); this.$emit('item-click', { originalEvent: event, item: this.item, id: this.id });
}, },
containerClass() {
return ['p-menuitem', this.item.class, { 'p-focus': this.id === this.focusedOptionId, 'p-disabled': this.disabled() }];
},
linkClass(routerProps) {
return [
'p-menuitem-link',
{
'router-link-active': routerProps && routerProps.isActive,
'router-link-active-exact': this.exact && routerProps && routerProps.isExactActive
}
];
},
visible() { visible() {
return typeof this.item.visible === 'function' ? this.item.visible() : this.item.visible !== false; return typeof this.item.visible === 'function' ? this.item.visible() : this.item.visible !== false;
}, },
@ -82,11 +87,6 @@ export default {
return typeof this.item.label === 'function' ? this.item.label() : this.item.label; return typeof this.item.label === 'function' ? this.item.label() : this.item.label;
} }
}, },
computed: {
iconClass() {
return ['p-menuitem-icon', this.item.icon];
}
},
directives: { directives: {
ripple: Ripple ripple: Ripple
} }