feat(action-menu): [action-menu] add XDesign theme (#1514)

* feat(action-menu): [action-menu] add XDesign theme

* feat(action-menu): [action-menu] add XDesign theme
This commit is contained in:
gimmyhehe 2024-03-30 09:58:12 +08:00 committed by GitHub
parent 33f32aec46
commit fedf1cb1fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 285 additions and 47 deletions

View File

@ -17,6 +17,17 @@ export default {
mode: ['pc'],
pcDemo: 'max-show-num'
},
{
name: 'mode',
type: '"default" | "card"',
defaultValue: '"default"',
desc: {
'zh-CN': '菜单按钮模式',
'en-US': 'Card mode'
},
mode: ['pc'],
pcDemo: 'card-mode'
},
{
name: 'more-text',
type: 'string',

View File

@ -0,0 +1,31 @@
<template>
<tiny-action-menu :options="options" mode="card"> </tiny-action-menu>
</template>
<script setup>
import { ref } from 'vue'
import { ActionMenu as TinyActionMenu } from '@opentiny/vue'
import { iconWebPlus, iconSuccessful, iconCloseSquare } from '@opentiny/vue-icon'
const options = ref([
{
label: '远程登陆',
icon: iconWebPlus()
},
{
label: '开机',
icon: iconSuccessful()
},
{
label: '关机',
icon: iconCloseSquare()
},
{
label: '重启'
},
{
label: '网络设置',
children: [{ label: '更改安全组' }, { label: '切换VPC', divided: true }]
}
])
</script>

View File

@ -0,0 +1,16 @@
import { test, expect } from '@playwright/test'
test('基本用法', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('action-menu#card-mode')
const wrap = page.locator('#card-mode')
const actionMenu = wrap.locator('.tiny-action-menu')
const visibleItem = actionMenu.locator('.tiny-action-menu__item')
const moreItem = visibleItem.last()
await expect(visibleItem).toHaveCount(4)
await expect(moreItem).not.toHaveText(/更多/)
// 三点图标
await expect(moreItem.locator('circle')).toHaveCount(3)
})

View File

@ -0,0 +1,39 @@
<template>
<tiny-action-menu :options="options" mode="card"> </tiny-action-menu>
</template>
<script>
import { ActionMenu } from '@opentiny/vue'
import { iconWebPlus, iconSuccessful, iconCloseSquare } from '@opentiny/vue-icon'
export default {
components: {
TinyActionMenu: ActionMenu
},
data() {
return {
options: [
{
label: '远程登陆',
icon: iconWebPlus()
},
{
label: '开机',
icon: iconSuccessful()
},
{
label: '关机',
icon: iconCloseSquare()
},
{
label: '重启'
},
{
label: '网络设置',
children: [{ label: '更改安全组' }, { label: '切换VPC', divided: true }]
}
]
}
}
}
</script>

View File

@ -80,6 +80,20 @@ export default {
},
codeFiles: ['spacing.vue']
},
{
demoId: 'card-mode',
name: {
'zh-CN': '菜单模式',
'en-US': 'Mode'
},
desc: {
'zh-CN':
'<p>通过 <code>mode</code> 属性设置菜单模式以适配在不同场景中能够使用,例如:菜单按钮在卡片中使用,可以配置为 <code>card</code>卡片模式字体为黑色间距为10px。 <code>mode</code> 默认为值<code>default</code>。</p>',
'en-US':
'<p>Use the <code>mode</code> attribute to set the menu mode so that the vehicle can be used in different scenarios. For example, if the menu button is used in configuration, it can be configured as <code>card</code>, and the auxiliary mode font is Black with 10px spacing. <code>mode</code> defaults to <code>default</code>. </p>'
},
codeFiles: ['card-mode.vue']
},
{
demoId: 'popper-class',
name: {

View File

@ -12,6 +12,58 @@
import type { IActionMenuRenderlessParams, IActionMenuItemData } from '@/types'
export const computedMaxShowNum =
({ props, state }: Pick<IActionMenuRenderlessParams, 'props' | 'state'>) =>
(): number => {
if (props.maxShowNum !== undefined) {
return props.maxShowNum
}
if (state.isCardMode) {
return 3
} else {
return 2
}
}
export const computedSpacing =
({ props, state }: Pick<IActionMenuRenderlessParams, 'props' | 'state'>) =>
(): string => {
if (props.spacing !== undefined) {
return String(props.spacing).includes('px') ? props.spacing : props.spacing + 'px'
}
if (state.isCardMode) {
return '10px'
} else {
return '5px'
}
}
export const computedMoreText =
({ props, state, t }: Pick<IActionMenuRenderlessParams, 'props' | 'state', 't'>) =>
(): string => {
if (props.moreText !== undefined) {
return props.moreText
}
if (state.isCardMode) {
return ''
} else {
return t('ui.actionMenu.moreText')
}
}
export const computedSuffixIcon =
({ props, state }: Pick<IActionMenuRenderlessParams, 'props' | 'state'>) =>
(): string | Object => {
if (props.suffixIcon) {
return props.suffixIcon
}
if (state.isCardMode) {
return 'tiny-icon-ellipsis'
} else {
return ''
}
}
export const handleMoreClick = (emit: IActionMenuRenderlessParams['emit']) => () => {
emit('more-click')
}

View File

@ -17,27 +17,45 @@ import type {
ISharedRenderlessParamHooks,
IActionMenuRenderlessParamUtils
} from '@/types'
import { handleMoreClick, handleItemClick, visibleChange } from './index'
import {
handleMoreClick,
handleItemClick,
visibleChange,
computedMaxShowNum,
computedSpacing,
computedMoreText,
computedSuffixIcon
} from './index'
export const api = ['state', 'handleMoreClick', 'handleItemClick', 'visibleChange']
export const renderless = (
props: IActionMenuProps,
{ computed, reactive }: ISharedRenderlessParamHooks,
{ emit }: IActionMenuRenderlessParamUtils
{ emit, t }: IActionMenuRenderlessParamUtils
): IActionMenuApi => {
const api = {} as IActionMenuApi
const state: IActionMenuState = reactive({
visibleOptions: computed(() => props.options.slice(0, props.maxShowNum)),
moreOptions: computed(() => props.options.slice(props.maxShowNum)),
spacing: computed(() => (String(props.spacing).includes('px') ? props.spacing : props.spacing + 'px'))
visibleOptions: computed(() => props.options.slice(0, state.maxShowNum)),
isCardMode: computed(() => props.mode === 'card'),
moreOptions: computed(() => props.options.slice(state.maxShowNum)),
spacing: computed(() => api.computedSpacing()),
maxShowNum: computed(() => api.computedMaxShowNum()),
moreText: computed(() => api.computedMoreText()),
suffixIcon: computed(() => api.computedSuffixIcon())
})
const api: IActionMenuApi = {
Object.assign(api, {
state,
handleMoreClick: handleMoreClick(emit),
handleItemClick: handleItemClick(emit),
visibleChange: visibleChange(emit),
state
}
computedMaxShowNum: computedMaxShowNum({ props, state }),
computedSpacing: computedSpacing({ props, state }),
computedMoreText: computedMoreText({ props, state, t }),
computedSuffixIcon: computedSuffixIcon({ props, state })
})
return api
}

View File

@ -10,14 +10,27 @@
*
*/
import type { ComputedRef, ExtractPropTypes, ComponentPublicInstance } from 'vue'
import type { ExtractPropTypes, ComponentPublicInstance } from 'vue'
import type { actionMenuProps } from '@/action-menu/src'
import type { ISharedRenderlessFunctionParams, ISharedRenderlessParamUtils } from './shared.type'
import type {
handleMoreClick,
handleItemClick,
visibleChange,
computedMaxShowNum,
computedSpacing,
computedMoreText,
computedSuffixIcon
} from '../src/action-menu'
export interface IActionMenuState {
visibleOptions: ComputedRef<object>
moreOptions: ComputedRef<object>
spacing: ComputedRef<string | number>
visibleOptions: object
moreOptions: object
isCardMode: boolean
spacing: string | number
maxShowNum: number
moreText: string
suffixIcon: string | Object
}
export type IActionMenuProps = ExtractPropTypes<typeof actionMenuProps>
@ -34,9 +47,13 @@ export interface IActionMenuItemData {
}
export interface IActionMenuApi {
handleMoreClick: () => void
handleItemClick: (data: IActionMenuItemData) => void
visibleChange: (status: boolean) => void
handleMoreClick: ReturnType<typeof handleMoreClick>
handleItemClick: ReturnType<typeof handleItemClick>
visibleChange: ReturnType<typeof visibleChange>
computedMaxShowNum: ReturnType<typeof computedMaxShowNum>
computedSpacing: ReturnType<typeof computedSpacing>
computedMoreText: ReturnType<typeof computedMoreText>
computedSuffixIcon: ReturnType<typeof computedSuffixIcon>
state: IActionMenuState
}

View File

@ -35,6 +35,24 @@
&__wrap {
display: flex;
&.@{action-menu-prefix-cls}__card-mode {
.@{action-menu-prefix-cls}__item {
.tiny-svg {
fill: var(--ti-action-menu-item-card-text-color);
}
.@{dropdown-prefix-cls} {
.@{dropdown-prefix-cls}-trigger {
&:hover {
.tiny-svg {
fill: var(--ti-action-menu-item-card-text-color);
}
}
}
}
}
}
.@{action-menu-prefix-cls}__item {
display: flex;
justify-content: space-between;
@ -54,15 +72,9 @@
}
&:not(.is-disabled):hover {
color: var(--ti-action-menu-item-hover-text-color);
&.@{action-menu-prefix-cls}__item-visable {
.@{dropdown-item-prefix-cls} {
background-color: var(--ti-action-menu-item-hover-bg-color);
&__wrap {
color: var(--ti-action-menu-item-hover-text-color);
}
}
}
@ -80,6 +92,21 @@
&:hover {
background-color: var(--ti-action-menu-item-hover-bg-color);
text-decoration: var(--ti-action-menu-hover-text-decoratio);
color: var(--ti-action-menu-item-hover-text-color);
}
}
&.@{action-menu-prefix-cls}__card-item {
.@{dropdown-item-prefix-cls}__wrap {
color: var(--ti-action-menu-item-card-text-color);
.tiny-svg {
fill: var(--ti-action-menu-item-card-text-color);
}
&:hover {
color: var(--ti-action-menu-item-card-hover-text-color);
}
}
}

View File

@ -9,5 +9,7 @@ export const tinyActionMenuSmbTheme = {
'ti-action-menu-hover-text-decoratio': 'underline',
'ti-dropdown-line-height': 'calc(var(--ti-common-line-height-4) + 2px)',
'ti-action-menu-more-icon-width': 'var(--ti-common-size-5x)',
'ti-action-menu-more-icon-height': 'var(--ti-common-size-5x)'
'ti-action-menu-more-icon-height': 'var(--ti-common-size-5x)',
'ti-action-menu-item-card-text-color': 'var(--ti-common-color-text-primary)',
'ti-action-menu-item-card-hover-text-color': 'var(--ti-common-color-text-primary)'
}

View File

@ -51,4 +51,8 @@
--ti-action-menu-item-svg-margin-bottom: var(--ti-common-space-0, 0px);
// 下拉菜单项图标左侧内边距
--ti-action-menu-item-svg-margin-left: var(--ti-common-space-0, 0px);
// 下拉菜单卡片模式字体颜色
--ti-action-menu-item-card-text-color: var(--ti-common-color-text-link, #526ecc);
// 下拉菜单卡片模式字体颜色
--ti-action-menu-item-card-hover-text-color: var(--ti-common-color-text-link-hover, #344899);
}

View File

@ -3,7 +3,7 @@ export const tinyDropdownItemSmbTheme = {
'ti-dropdown-item-padding-vertical': 'var(--ti-common-space-0)',
'ti-dropdown-item-padding-horizontal': 'var(--ti-common-space-4x)',
'ti-dropdown-item-hover-text-color': 'var(--ti-common-color-text-primary)',
'ti-dropdown-item-icon-color-hover': 'var(--ti-common-color-icon-graybg-hover)',
'ti-dropdown-item-icon-color-hover': 'var(--ti-common-color-text-link-hover)',
'ti-dropdown-item-expand-svg-margin-left': 'var(--ti-common-space-0)',
'ti-dropdown-item-expand-svg-margin-right': 'var(--ti-common-space-2x)',
'ti-dropdown-item-content-margin-left': 'calc(var(--ti-dropdown-item-expand-icon-size) + var(--ti-common-space-2x))',

View File

@ -1,8 +1,8 @@
export const tinyDropdownSmbTheme = {
'ti-dropdown-icon-size': 'var(--ti-common-font-size-2)',
'ti-dropdown-text-color-hover': 'var(--ti-common-color-text-primary)',
'ti-dropdown-icon-color': 'var(--ti-common-color-placeholder)', // TINY-TODO:缺少#808080图标色变量
'ti-dropdown-icon-color-hover': 'var(--ti-common-color-icon-graybg-hover)',
'ti-dropdown-text-color-hover': 'var(--ti-common-color-text-link)',
'ti-dropdown-icon-color': 'var(--ti-common-color-text-link)',
'ti-dropdown-icon-color-hover': 'var(--ti-common-color-text-link)',
'ti-dropdown-caret-svg-margin-horizontal': 'var(--ti-common-space-0)',
'ti-dropdown-caret-button-padding-right': 'var(--ti-common-space-6x)',
'ti-dropdown-caret-line-width': 'var(--ti-common-size-0)',

View File

@ -19,14 +19,10 @@ export const actionMenuProps = {
type: Array,
default: () => []
},
maxShowNum: {
type: Number,
default: 2
},
maxShowNum: Number,
moreText: String,
spacing: {
type: [String, Number],
default: '5px'
type: [String, Number]
},
textField: {
type: String,
@ -48,6 +44,10 @@ export const actionMenuProps = {
showIcon: {
type: Boolean,
default: true
},
mode: {
type: String,
default: 'default'
}
}

View File

@ -1,10 +1,14 @@
<template>
<div class="tiny-action-menu">
<ul class="tiny-action-menu__wrap">
<ul :class="{ 'tiny-action-menu__wrap': true, 'tiny-action-menu__card-mode': state.isCardMode }">
<li
v-for="(visableItem, index) in state.visibleOptions"
:key="index"
:class="['tiny-action-menu__item', 'tiny-action-menu__item-visable', { 'is-disabled': visableItem.disabled }]"
:class="[
'tiny-action-menu__item',
'tiny-action-menu__item-visable',
{ 'is-disabled': visableItem.disabled, 'tiny-action-menu__card-item': state.isCardMode }
]"
>
<tiny-dropdown-item
:item-data="visableItem"
@ -23,14 +27,16 @@
<li v-if="state.moreOptions.length" class="tiny-action-menu__item">
<tiny-dropdown
:title="moreText"
:title="state.moreText"
:trigger="trigger"
:suffix-icon="suffixIcon"
:show-icon="showIcon"
@item-click="handleItemClick"
@handle-click="handleMoreClick"
@visible-change="visibleChange"
>
<template v-if="state.suffixIcon" #suffix-icon>
<component :is="state.suffixIcon"></component>
</template>
<template #dropdown>
<tiny-dropdown-menu :text-field="textField" :popper-class="popperClass">
<tiny-dropdown-item
@ -53,20 +59,22 @@
</template>
<script lang="ts">
import type { IActionMenuApi } from '@opentiny/vue-renderless/types/action-menu.type'
import { setup, $prefix, defineComponent } from '@opentiny/vue-common'
import { renderless, api } from '@opentiny/vue-renderless/action-menu/vue'
import { iconEllipsis } from '@opentiny/vue-icon'
import '@opentiny/vue-theme/action-menu/index.less'
import Dropdown from '@opentiny/vue-dropdown'
import DropdownMenu from '@opentiny/vue-dropdown-menu'
import DropdownItem from '@opentiny/vue-dropdown-item'
import { t } from '@opentiny/vue-locale'
export default defineComponent({
name: $prefix + 'ActionMenu',
components: {
TinyDropdown: Dropdown,
TinyDropdownMenu: DropdownMenu,
TinyDropdownItem: DropdownItem
TinyDropdownItem: DropdownItem,
TinyIconEllipsis: iconEllipsis()
},
emits: ['more-click', 'item-click', 'visible-change'],
props: {
@ -74,17 +82,12 @@ export default defineComponent({
type: Array,
default: () => []
},
maxShowNum: {
type: Number,
default: 2
},
maxShowNum: Number,
moreText: {
type: String,
default: t('ui.actionMenu.moreText')
type: String
},
spacing: {
type: [String, Number],
default: '5px'
type: [String, Number]
},
textField: {
type: String,
@ -106,10 +109,14 @@ export default defineComponent({
showIcon: {
type: Boolean,
default: true
},
mode: {
type: String,
default: 'default'
}
},
setup(props, context) {
return setup({ props, context, renderless, api })
return setup({ props, context, renderless, api }) as unknown as IActionMenuApi
}
})
</script>