diff --git a/examples/sites/demos/apis/tree-menu.js b/examples/sites/demos/apis/tree-menu.js index eb36cf68fb..180dc68fcf 100644 --- a/examples/sites/demos/apis/tree-menu.js +++ b/examples/sites/demos/apis/tree-menu.js @@ -342,6 +342,29 @@ export default { mode: ['pc'], pcDemo: 'show-expand' }, + { + name: 'expand-menu-popable', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '启用一键展开/收起功能下。是否支持悬浮展示子菜单', + 'en-US': + 'when the one click expand/collapse function enabled. whether to support hovering to display submenus' + }, + mode: ['pc'], + pcDemo: 'pop-sub-menu' + }, + { + name: 'popper-class', + type: 'string', + defaultValue: '', + desc: { + 'zh-CN': '悬浮展示子菜单时,弹窗的类名', + 'en-US': 'when hovering to display submenus, the class name of the pop-up window' + }, + mode: ['pc'], + pcDemo: 'pop-sub-menu' + }, { name: 'show-filter', type: 'boolean', diff --git a/examples/sites/demos/pc/app/tree-menu/pop-sub-menu-composition-api.vue b/examples/sites/demos/pc/app/tree-menu/pop-sub-menu-composition-api.vue new file mode 100644 index 0000000000..63c1d609c8 --- /dev/null +++ b/examples/sites/demos/pc/app/tree-menu/pop-sub-menu-composition-api.vue @@ -0,0 +1,123 @@ + + + diff --git a/examples/sites/demos/pc/app/tree-menu/pop-sub-menu.spec.ts b/examples/sites/demos/pc/app/tree-menu/pop-sub-menu.spec.ts new file mode 100644 index 0000000000..40887a76b1 --- /dev/null +++ b/examples/sites/demos/pc/app/tree-menu/pop-sub-menu.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@playwright/test' + +test('静态数据', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('tree-menu#basic-usage') + + const wrap = page.locator('#basic-usage') + const treeMenu = wrap.locator('.tiny-tree-menu') + const treeNode = treeMenu.locator('.tiny-tree-node__wrapper > .tiny-tree-node') + const treeNodeContent = treeNode.locator('> .tiny-tree-node__content') + + await expect(treeNode.filter({ hasText: /^环境准备$/ })).toBeHidden() + await treeNodeContent.filter({ hasText: /^使用指南$/ }).click() + await expect(treeNode.filter({ hasText: /^环境准备$/ })).toBeVisible() + await treeNode.filter({ hasText: /^环境准备$/ }).click() + await expect(treeNode.filter({ hasText: /^环境准备$/ })).toHaveClass(/is-current/) + await treeNodeContent.filter({ hasText: /^使用指南$/ }).click() + await expect(treeNode.filter({ hasText: /^环境准备$/ })).toBeHidden() + + // 过滤功能 + await treeMenu.locator('.tiny-input__inner').fill('新增组件') + await expect(page.getByTitle('新增组件')).toBeVisible() + await expect(treeNodeContent.filter({ hasText: /^使用指南$/ })).toBeHidden() + await treeMenu.locator('.tiny-input__inner').clear() + await expect(treeNodeContent.filter({ hasText: /^使用指南$/ })).toBeVisible() +}) diff --git a/examples/sites/demos/pc/app/tree-menu/pop-sub-menu.vue b/examples/sites/demos/pc/app/tree-menu/pop-sub-menu.vue new file mode 100644 index 0000000000..41a48dfcaa --- /dev/null +++ b/examples/sites/demos/pc/app/tree-menu/pop-sub-menu.vue @@ -0,0 +1,184 @@ + + + + + + + diff --git a/examples/sites/demos/pc/app/tree-menu/webdoc/tree-menu.js b/examples/sites/demos/pc/app/tree-menu/webdoc/tree-menu.js index 9bed1241d4..d07b6ddcef 100644 --- a/examples/sites/demos/pc/app/tree-menu/webdoc/tree-menu.js +++ b/examples/sites/demos/pc/app/tree-menu/webdoc/tree-menu.js @@ -150,6 +150,18 @@ export default { }, codeFiles: ['show-expand.vue'] }, + { + demoId: 'pop-sub-menu', + name: { + 'zh-CN': '折叠弹出', + 'en-US': 'Pop Sub Menu' + }, + desc: { + 'zh-CN': '

折叠模式下,支持弹出子菜单列表。

\n', + 'en-US': '

support pop sub menus when collapsed.

\n' + }, + codeFiles: ['pop-sub-menu.vue'] + }, { demoId: 'custom-icon', name: { diff --git a/packages/renderless/src/tree-menu/vue.ts b/packages/renderless/src/tree-menu/vue.ts index bdd89a6c6c..3edf0d9829 100644 --- a/packages/renderless/src/tree-menu/vue.ts +++ b/packages/renderless/src/tree-menu/vue.ts @@ -77,7 +77,7 @@ export const api = [ export const renderless = ( props: ITreeMenuProps, - { computed, watch, reactive, onMounted }: ISharedRenderlessFunctionParams, + { computed, watch, reactive, onMounted, provide }: ISharedRenderlessFunctionParams, { t, service, emit, vm }: ISharedRenderlessParamUtils ) => { service = service || { base: {} } @@ -128,6 +128,8 @@ export const renderless = ( computedTreeStyle: computedTreeStyle({ props }) }) + provide('tree-menu', vm) + watch( () => props.data, (value) => (state.data = value), diff --git a/packages/theme/src/tree-menu/index.less b/packages/theme/src/tree-menu/index.less index b1ff031518..32f6f3df9d 100644 --- a/packages/theme/src/tree-menu/index.less +++ b/packages/theme/src/tree-menu/index.less @@ -17,6 +17,8 @@ @tree-node-prefix-cls: ~'@{css-prefix}tree-node'; @input-prefix-cls: ~'@{css-prefix}input'; @tree-menu-prefix-cls: ~'@{css-prefix}tree-menu'; +@tree-pop-menu-prefix-cls: ~'@{css-prefix}tree-menu-pop-menu'; +@tree-pop-menu-panel-prefix-cls: ~'@{css-prefix}tree-menu-pop-menu-panel'; .@{tree-menu-prefix-cls} { .inject-TreeMenu-vars(); @@ -212,6 +214,7 @@ .tree-node-name { align-items: center; padding: 0 var(--tv-TreeMenu-node-body-text-padding-x); + display: flex; &:hover { font-weight: var(--tv-TreeMenu-node-name-hover-font-weight); @@ -220,6 +223,7 @@ svg { margin-right: var(--tv-TreeMenu-prefix-icon-margin-right); + flex-shrink: 0; } } } @@ -337,4 +341,73 @@ } } } + + .@{tree-pop-menu-prefix-cls} { + line-height: initial; + } +} + +.@{tree-pop-menu-panel-prefix-cls} { + .inject-TreeMenu-vars(); + padding: var(--tv-TreeMenu-pop-padding) !important; + border-radius: var(--tv-TreeMenu-pop-radius) !important; + box-shadow: var(--tv-TreeMenu-pop-shadow) !important; + width: var(--tv-TreeMenu-pop-width); + transform: translateX(var(--tv-TreeMenu-pop-panel-margin-left)); + + .tree-menu-pop-menu__list { + width: 100%; + + .tiny-tree-menu-pop-menu { + width: 100%; + } + + .reference-wrapper { + display: block; + } + + &-item { + width: 100%; + height: var(--tv-TreeMenu-pop-item-height); + display: flex; + align-items: center; + + .tree-node-name { + svg { + margin-right: var(--tv-TreeMenu-prefix-icon-margin-right); + } + } + + &.hover, + &:hover, + &:active { + background-color: var(--tv-TreeMenu-pop-item-active-bg); + color: var(--tv-TreeMenu-pop-item-active-text-color); + } + + &.has-current { + background-color: var(--tv-TreeMenu-pop-item-active-bg); + color: var(--tv-TreeMenu-pop-item-active-text-color); + } + + &.is-current { + background-color: var(--tv-TreeMenu-pop-item-selected-bg); + color: var(--tv-TreeMenu-pop-item-selected-text-color); + } + + .tree-node { + padding-left: var(--tv-TreeMenu-pop-item-padding-left); + display: flex; + align-items: center; + + &-body { + color: inherit; + } + } + } + } + + &__first { + transform: translateX(var(--tv-TreeMenu-pop-panel-first-margin-left)); + } } diff --git a/packages/theme/src/tree-menu/vars.less b/packages/theme/src/tree-menu/vars.less index 0757c18942..191ab87bcf 100644 --- a/packages/theme/src/tree-menu/vars.less +++ b/packages/theme/src/tree-menu/vars.less @@ -81,4 +81,29 @@ --tv-TreeMenu-node-body-selected-color: var(--tv-color-border-active, #191919); // hover字体颜色 --tv-TreeMenu-node-name-hover-color: var(--tv-color-border-hover, #191919); + + // 弹出面板 边距 + --tv-TreeMenu-pop-padding: var(--tv-space-base); + // 弹出面板 圆角 + --tv-TreeMenu-pop-radius: var(--tv-border-radius-sm); + // 弹出面板 圆角 + --tv-TreeMenu-pop-shadow: var(--tv-shadow-4-down); + // 弹出面板 菜单高度 + --tv-TreeMenu-pop-item-height: 36px; + // 弹出面板 宽度 + --tv-TreeMenu-pop-width: 160px; + // 弹出面板 菜单悬浮、激活背景 + --tv-TreeMenu-pop-item-active-bg: transparent; + // 弹出面板 菜单悬浮、激活文本色 + --tv-TreeMenu-pop-item-active-text-color: var(--tv-base-color-brand); + // 弹出面板 选中背景色 + --tv-TreeMenu-pop-item-selected-bg: transparent; + // 弹出面板 选中文本色 + --tv-TreeMenu-pop-item-selected-text-color: var(--tv-base-color-brand); + // 弹出面板 选中文本色 + --tv-TreeMenu-pop-item-padding-left: 16px; + // 弹出面板 左侧边距 + --tv-TreeMenu-pop-panel-margin-left: -2px; + // 弹出面板 第一层左侧边距 + --tv-TreeMenu-pop-panel-first-margin-left: -38px; } diff --git a/packages/vue/src/tree-menu/src/menu-node.vue b/packages/vue/src/tree-menu/src/menu-node.vue new file mode 100644 index 0000000000..f316c86c84 --- /dev/null +++ b/packages/vue/src/tree-menu/src/menu-node.vue @@ -0,0 +1,50 @@ + + + + + + diff --git a/packages/vue/src/tree-menu/src/pc.vue b/packages/vue/src/tree-menu/src/pc.vue index c59cf094dc..7036e6807e 100644 --- a/packages/vue/src/tree-menu/src/pc.vue +++ b/packages/vue/src/tree-menu/src/pc.vue @@ -78,17 +78,26 @@ @current-change="currentChange" >
@@ -105,6 +114,9 @@ import { $prefix, setup, defineComponent } from '@opentiny/vue-common' import { renderless, api } from '@opentiny/vue-renderless/tree-menu/vue' import Tree from '@opentiny/vue-tree' import Input from '@opentiny/vue-input' +import Tooltip from '@opentiny/vue-tooltip' +import PopMenu from './pop-menu.vue' +import MenuNode from './menu-node.vue' import { iconLeftWardArrow, iconEditorMenuLeft, iconEditorMenuRight } from '@opentiny/vue-icon' import { treeMenuProps } from './props' import '@opentiny/vue-theme/tree-menu/index.less' @@ -129,6 +141,9 @@ export default defineComponent({ components: { TinyTree: Tree, TinyInput: Input, + TinyTooltip: Tooltip, + TinyTreeMenuPopMenu: PopMenu, + TinyTreeMenuNode: MenuNode, IconArrow: iconLeftWardArrow(), IconEditorMenuLeft: iconEditorMenuLeft(), IconEditorMenuRight: iconEditorMenuRight() diff --git a/packages/vue/src/tree-menu/src/pop-menu.vue b/packages/vue/src/tree-menu/src/pop-menu.vue new file mode 100644 index 0000000000..5b9d645e28 --- /dev/null +++ b/packages/vue/src/tree-menu/src/pop-menu.vue @@ -0,0 +1,115 @@ + + + + + + diff --git a/packages/vue/src/tree-menu/src/props.ts b/packages/vue/src/tree-menu/src/props.ts index 69f273c953..393f717c7a 100644 --- a/packages/vue/src/tree-menu/src/props.ts +++ b/packages/vue/src/tree-menu/src/props.ts @@ -67,6 +67,11 @@ export const treeMenuProps = { type: Boolean, default: false }, + expandMenuPopable: { + type: Boolean, + default: false + }, + popperClass: String, collapsible: { type: Boolean, default: true