diff --git a/examples/sites/demos/apis/nav-menu.js b/examples/sites/demos/apis/nav-menu.js index e69deb86f8..db24ba1b4c 100644 --- a/examples/sites/demos/apis/nav-menu.js +++ b/examples/sites/demos/apis/nav-menu.js @@ -127,6 +127,16 @@ export default { }, mode: ['pc'], pcDemo: 'slot-toolbar' + }, + { + name: 'icon', + defaultValue: '', + desc: { + 'zh-CN': '自定义菜单图标插槽', + 'en-US': 'Customize the menu icon' + }, + mode: ['pc'], + pcDemo: 'menu-icon' } ] } @@ -153,6 +163,7 @@ interface IMenuItem { interface IDataItem { title: string url: string + icon?: Commonent children?: IDataItem[] }` }, diff --git a/examples/sites/demos/pc/app/nav-menu/menu-icon-composition-api.vue b/examples/sites/demos/pc/app/nav-menu/menu-icon-composition-api.vue new file mode 100644 index 0000000000..a92af90015 --- /dev/null +++ b/examples/sites/demos/pc/app/nav-menu/menu-icon-composition-api.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/examples/sites/demos/pc/app/nav-menu/menu-icon.spec.ts b/examples/sites/demos/pc/app/nav-menu/menu-icon.spec.ts new file mode 100644 index 0000000000..aebb030436 --- /dev/null +++ b/examples/sites/demos/pc/app/nav-menu/menu-icon.spec.ts @@ -0,0 +1,11 @@ +import { test, expect } from '@playwright/test' + +test('菜单图标', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('nav-menu#menu-icon') + const preview = page.locator('#menu-icon') + const icon = preview.locator('.menu-icon') + const iconSvg = preview.locator('.menu-icon svg') + await expect(icon).toBeVisible() + await expect(iconSvg).toBeVisible() +}) diff --git a/examples/sites/demos/pc/app/nav-menu/menu-icon.vue b/examples/sites/demos/pc/app/nav-menu/menu-icon.vue new file mode 100644 index 0000000000..0b9f8cf26c --- /dev/null +++ b/examples/sites/demos/pc/app/nav-menu/menu-icon.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/examples/sites/demos/pc/app/nav-menu/webdoc/nav-menu.js b/examples/sites/demos/pc/app/nav-menu/webdoc/nav-menu.js index ffd34e5398..e22ce670b3 100644 --- a/examples/sites/demos/pc/app/nav-menu/webdoc/nav-menu.js +++ b/examples/sites/demos/pc/app/nav-menu/webdoc/nav-menu.js @@ -54,6 +54,18 @@ export default { }, codeFiles: ['slot-logo.vue'] }, + { + demoId: 'menu-icon', + name: { + 'zh-CN': '菜单图标', + 'en-US': 'Menu Icon' + }, + desc: { + 'zh-CN': '通过 icon 字段配置展示菜单图标。', + 'en-US': 'Display menu icon through icon field of data.' + }, + codeFiles: ['menu-icon.vue'] + }, { demoId: 'before-skip', name: { diff --git a/packages/renderless/src/nav-menu/index.ts b/packages/renderless/src/nav-menu/index.ts index f1fc459e5a..19fe1dd1bd 100644 --- a/packages/renderless/src/nav-menu/index.ts +++ b/packages/renderless/src/nav-menu/index.ts @@ -169,7 +169,8 @@ export const initData = id: item.id, pid: item.pid, isFullUrl: props.allowFullUrl && isFullUrl(router), - target: item.target + target: item.target, + icon: item.icon } } @@ -577,7 +578,7 @@ export const handleTitleMouseenter = const text = target.textContent const font = window.getComputedStyle(target).font const rect = target.getBoundingClientRect() - const res = omitText(text, font, rect.width + 2) + const res = omitText(text, font, rect.width + 4) if (target && res.o) { const tooltip = vm.$refs.tooltip diff --git a/packages/renderless/types/nav-menu.type.ts b/packages/renderless/types/nav-menu.type.ts index 520badb5f7..4082d1d4b1 100644 --- a/packages/renderless/types/nav-menu.type.ts +++ b/packages/renderless/types/nav-menu.type.ts @@ -13,6 +13,7 @@ export interface menuItemType { url: string children?: menuItemType[] target?: string + icon?: any } export interface whitchSubMenuType { diff --git a/packages/theme/src/nav-menu/index.less b/packages/theme/src/nav-menu/index.less index 01406692b6..30dba8311f 100644 --- a/packages/theme/src/nav-menu/index.less +++ b/packages/theme/src/nav-menu/index.less @@ -59,18 +59,37 @@ line-height: var(--tv-NavMenu-height); box-sizing: border-box; + .menu-icon { + display: inline-block; + + svg { + vertical-align: text-bottom; + margin-right: var(--tv-NavMenu-icon-margin-right); + font-size: var(--tv-NavMenu-icon-size); + fill: var(--tv-NavMenu-icon-color); + } + } + &.active, &:hover { // 规范选中后,背景色修改 color: var(--tv-NavMenu-item-text-color); text-decoration: none; font-weight: var(--tv-NavMenu-item-text-font-weight); + + svg { + fill: var(--tv-NavMenu-icon-color-hover); + } } &.selected { position: relative; font-weight: var(--tv-NavMenu-item-text-font-weight); + svg { + fill: var(--tv-NavMenu-icon-color-hover); + } + &:after { content: ' '; width: 100%; @@ -179,6 +198,10 @@ &.selected { position: relative; + svg { + fill: var(--tv-NavMenu-icon-color-hover) !important; + } + &:before { content: ''; width: var(--tv-NavMenu-popmenu-more-item-before-width); @@ -206,6 +229,17 @@ overflow: hidden; text-overflow: ellipsis; + .menu-icon { + display: inline-block; + + svg { + vertical-align: text-bottom; + margin-right: var(--tv-NavMenu-icon-margin-right); + font-size: var(--tv-NavMenu-icon-size); + fill: var(--tv-NavMenu-icon-color); + } + } + &.showicon { width: calc(100% - 12px); } @@ -253,11 +287,26 @@ overflow: hidden; text-overflow: ellipsis; flex: 1; + + .menu-icon { + display: inline-block; + + svg { + vertical-align: text-bottom; + margin-right: var(--tv-NavMenu-icon-margin-right); + font-size: var(--tv-NavMenu-icon-size); + fill: var(--tv-NavMenu-icon-color); + } + } } > span.selected, > a.selected { color: var(--tv-NavMenu-popmenu-selected-text-color); + + svg { + fill: var(--tv-NavMenu-icon-color-hover); + } } &:only-child { @@ -285,7 +334,18 @@ &:hover { color: var(--tv-NavMenu-popmenu-text-color); - text-decoration: underline; + + > a:after, + > span:after { + content: ''; + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: 1px; + background: var(--tv-NavMenu-item-selected-underline-bg-color); + z-index: 1; + } } &.active, @@ -302,6 +362,18 @@ text-decoration: none; font-size: var(--tv-NavMenu-popmenu-text-font-size); cursor: pointer; + position: relative; + + .menu-icon { + display: inline-block; + + svg { + vertical-align: text-bottom; + margin-right: var(--tv-NavMenu-icon-margin-right); + font-size: var(--tv-NavMenu-icon-size); + fill: var(--tv-NavMenu-icon-color); + } + } &.selected { color: var(--tv-NavMenu-popmenu-selected-text-color); @@ -317,15 +389,20 @@ background: var(--tv-NavMenu-item-selected-underline-bg-color); z-index: 1; } - } - &:hover { - color: var(--tv-NavMenu-popmenu-text-color); + svg { + fill: var(--tv-NavMenu-icon-color-hover); + } } + &:hover, &.active, &:active { color: var(--tv-NavMenu-popmenu-text-color); + + svg { + fill: var(--tv-NavMenu-icon-color-hover); + } } } @@ -401,6 +478,10 @@ float: right; margin-left: 10px; } + + svg { + vertical-align: text-bottom; + } } > .slot-mobile-menu { diff --git a/packages/theme/src/nav-menu/vars.less b/packages/theme/src/nav-menu/vars.less index b29d21bf97..256078e3f0 100644 --- a/packages/theme/src/nav-menu/vars.less +++ b/packages/theme/src/nav-menu/vars.less @@ -81,4 +81,12 @@ --tv-NavMenu-slot-logo-icon-color: var(--tv-color-icon-hover, #191919); // 自定义 Logo大小 --tv-NavMenu-slot-logo-icon-size: var(--tv-icon-size, 16px); + // 导航菜单图标右侧边距 + --tv-NavMenu-icon-margin-right: var(--tv-space-base); + // 导航菜单图标大小 + --tv-NavMenu-icon-size: var(--tv-icon-size); + // 导航菜单图标色 + --tv-NavMenu-icon-color: var(--tv-color-icon); + // 导航菜单图标悬浮色 + --tv-NavMenu-icon-color-hover: var(--tv-color-icon-hover); } diff --git a/packages/vue/src/nav-menu/__tests__/nav-menu.test.tsx b/packages/vue/src/nav-menu/__tests__/nav-menu.test.tsx index 9158c7eaf4..74e38f26b1 100644 --- a/packages/vue/src/nav-menu/__tests__/nav-menu.test.tsx +++ b/packages/vue/src/nav-menu/__tests__/nav-menu.test.tsx @@ -1,4 +1,4 @@ -import { IconTotal } from '@opentiny/vue-icon' +import { IconTotal, IconShare } from '@opentiny/vue-icon' import NavMenu from '@opentiny/vue-nav-menu' import { describe, expect, test } from 'vitest' import { mountPcMode } from '@opentiny-internal/vue-test-utils' @@ -6,6 +6,9 @@ import { mountPcMode } from '@opentiny-internal/vue-test-utils' describe('PC Mode', () => { const mount = mountPcMode + const IconTotalComponent = IconTotal() + const IconShareComponent = IconShare() + const navMenuMockData = [ { title: '首页', @@ -200,8 +203,18 @@ describe('PC Mode', () => { ] } ] - - const IconTotalComponent = IconTotal() + const navMenuMockDataWithIcon = [ + { + title: '首页', + url: '', + icon: IconShareComponent + }, + { + title: '导航', + url: '', + icon: '' + } + ] /** * attrs @@ -213,6 +226,12 @@ describe('PC Mode', () => { expect(navMenu.vm.state.data.length).toBe(3) }) + test('menu icon', async () => { + const wrapper = mount(() => ) + const iconCom = wrapper.findComponent({ name: 'TinyIconShare' }) + expect(iconCom.vm).toBeTruthy() + }) + test.todo( 'overflow 设置一级菜单无法在当前菜单容器里显示完全时的处理策略。可选项有 auto / retract / fixed / hidden。默认为 auto' ) @@ -237,4 +256,30 @@ describe('PC Mode', () => { const iconTotalSvg = wrapper.find('.slot-logo') expect(iconTotalSvg.exists()).toBeTruthy() }) + + test('icon slot', async () => { + const wrapper = mount(() => ( + + {{ + icon: () => + }} + + )) + const menuIconDom = wrapper.find('.menu-icon') + expect(menuIconDom.exists()).toBeTruthy() + }) + + test('slot prioity higher than icon attr', async () => { + const wrapper = mount(() => ( + + {{ + icon: () => + }} + + )) + const iconTotalSvg = wrapper.findComponent({ name: 'TinyIconTotal' }) + const iconShareSvg = wrapper.findComponent({ name: 'TinyIconShare' }) + expect(iconTotalSvg.vm).toBeTruthy() + expect(iconShareSvg.exists()).toBe(false) + }) }) diff --git a/packages/vue/src/nav-menu/src/pc.vue b/packages/vue/src/nav-menu/src/pc.vue index af01ce9d6f..938251f106 100644 --- a/packages/vue/src/nav-menu/src/pc.vue +++ b/packages/vue/src/nav-menu/src/pc.vue @@ -36,8 +36,14 @@ @mouseenter="showSubMenu(item.children, { index }, $event)" @mouseleave="willHideSubMenu" @click="clickMenu(item, index)" - >{{ item.title }} + + {{ item.title }} + @@ -85,6 +91,11 @@ @mouseleave="leaveMoreMune" @click="clickMenu(item, index)" > + {{ item.title }} @@ -115,6 +126,16 @@ @mouseleave="handleTitleMouseleave" :class="{ selected: index === state.subIndex && state.subItemSelectedIndex === -1 }" > + {{ group.title }} @@ -136,6 +157,11 @@ selected: getLastChildSelected(item, i, index) }" > + {{ item.title }}