From ed862293a582f99cdc3f521e9dce65608b21cbd0 Mon Sep 17 00:00:00 2001 From: ming680 <827668037@qq.com> Date: Thu, 20 Jun 2024 14:56:19 +0800 Subject: [PATCH 1/9] feat(menu): type && site --- site/sidebar.config.ts | 2 +- src/index.ts | 1 + src/menu/Menu.tsx | 17 ++++++++++ src/menu/MenuItem.tsx | 17 ++++++++++ src/menu/README.md | 24 ++++++++++++++ src/menu/_example/base.tsx | 10 ++++++ src/menu/index.ts | 13 ++++++++ src/menu/style/index.js | 10 ++++++ src/menu/type.ts | 65 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/menu/Menu.tsx create mode 100644 src/menu/MenuItem.tsx create mode 100644 src/menu/README.md create mode 100644 src/menu/_example/base.tsx create mode 100644 src/menu/index.ts create mode 100644 src/menu/style/index.js create mode 100644 src/menu/type.ts diff --git a/site/sidebar.config.ts b/site/sidebar.config.ts index 6830513..f22b9e0 100644 --- a/site/sidebar.config.ts +++ b/site/sidebar.config.ts @@ -64,7 +64,7 @@ export default [ title: 'Menu 导航菜单', name: 'menu', path: '/components/menu', - // component: () => import('tdesign-web-components/menu/README.md'), + component: () => import('tdesign-web-components/menu/README.md'), }, { title: 'Breadcrumb 面包屑', diff --git a/src/index.ts b/src/index.ts index b76d4d9..cdd6a7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from './button'; export * from './icon'; +export * from './menu'; diff --git a/src/menu/Menu.tsx b/src/menu/Menu.tsx new file mode 100644 index 0000000..8fa7324 --- /dev/null +++ b/src/menu/Menu.tsx @@ -0,0 +1,17 @@ +import { Component, tag } from 'omi'; + +import { StyledProps } from '../common'; +import { TdMenuProps } from './type'; + +export interface MenuProps extends TdMenuProps, StyledProps {} + +@tag('t-menu') +export default class Menu extends Component { + static css = []; + + static defaultProps = {}; + + render() { + return
menus
; + } +} diff --git a/src/menu/MenuItem.tsx b/src/menu/MenuItem.tsx new file mode 100644 index 0000000..ca26360 --- /dev/null +++ b/src/menu/MenuItem.tsx @@ -0,0 +1,17 @@ +import { Component, tag } from 'omi'; + +import { StyledProps } from '../common'; +import { TdMenuItemProps } from './type'; + +export interface MenuItemProps extends TdMenuItemProps, StyledProps {} + +@tag('t-menu-item') +export default class Menu extends Component { + static css = []; + + static defaultProps = {}; + + render() { + return
menu item
; + } +} diff --git a/src/menu/README.md b/src/menu/README.md new file mode 100644 index 0000000..608a873 --- /dev/null +++ b/src/menu/README.md @@ -0,0 +1,24 @@ +--- +title: Menu 导航菜单 +description: 用于承载网站的架构,并提供跳转的菜单列表。 +isComponent: true +usage: { title: '', description: '' } +spline: base +--- + +### 基础使用 + +{{ base }} + + +## API + +### Menu Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- + +### MenuItem Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- diff --git a/src/menu/_example/base.tsx b/src/menu/_example/base.tsx new file mode 100644 index 0000000..bcd51f6 --- /dev/null +++ b/src/menu/_example/base.tsx @@ -0,0 +1,10 @@ +import 'tdesign-web-components/menu'; + +export default function Menu() { + return ( +
+ + +
+ ); +} diff --git a/src/menu/index.ts b/src/menu/index.ts new file mode 100644 index 0000000..09fedf2 --- /dev/null +++ b/src/menu/index.ts @@ -0,0 +1,13 @@ +import './style/index.js'; + +import _Menu from './Menu'; +import _MenuItem from './MenuItem'; + +export type { MenuProps } from './Menu'; +export type { MenuItemProps } from './MenuItem'; +export * from './type'; + +export const Menu = _Menu; +export const MenuItem = _MenuItem; + +export default Menu; diff --git a/src/menu/style/index.js b/src/menu/style/index.js new file mode 100644 index 0000000..2e609bf --- /dev/null +++ b/src/menu/style/index.js @@ -0,0 +1,10 @@ +import { css, globalCSS } from 'omi'; + +// 为了做主题切换 +import styles from '../../_common/style/web/components/menu/_index.less'; + +export const styleSheet = css` + ${styles} +`; + +globalCSS(styleSheet); diff --git a/src/menu/type.ts b/src/menu/type.ts new file mode 100644 index 0000000..2427663 --- /dev/null +++ b/src/menu/type.ts @@ -0,0 +1,65 @@ +import { TElement, TNode } from '../common'; + +export interface TdMenuProps { + /** + * 是否收起菜单 + * @default false + */ + collapsed?: boolean; + /** + * 同级别互斥展开 + * @default false + */ + expandMutex?: boolean; + /** + * 激活菜单项 + */ + value?: MenuValue; + /** + * 菜单宽度。值类型为数组时,分别表示菜单展开和折叠的宽度。[ 展开时的宽度, 折叠时的宽度 ],示例:['200px', '80px'] + * @default '232px' + */ + width?: string | number | Array; + /** + * 激活菜单项发生变化时触发 + */ + onChange?: (value: MenuValue) => void; + /** + * 展开的菜单项发生变化时触发 + */ + onExpand?: (value: Array) => void; +} + +export interface TdMenuItemProps { + /** + * 菜单项内容 + */ + label?: TNode; + /** + * 是否禁用菜单项展开/收起/跳转等功能 + */ + disabled?: boolean; + /** + * 跳转链接 + * @default '' + */ + href?: string; + /** + * 图标 + */ + icon?: TElement; + /** + * 链接或路由跳转方式 + */ + target?: '_blank' | '_self' | '_parent' | '_top'; + /** + * 菜单项唯一标识 + */ + value?: MenuValue; + /** + * 点击时触发 + */ + onClick?: (context: { e: HTMLElement; value: MenuValue }) => void; +} + +export type MenuValue = string | number; From 96fcefe3da52f34d198adcf643db86d81fe4aa6f Mon Sep 17 00:00:00 2001 From: ming680 <827668037@qq.com> Date: Fri, 21 Jun 2024 17:57:03 +0800 Subject: [PATCH 2/9] feat(menu): nest --- .vscode/settings.json | 2 + src/menu/Menu.tsx | 77 ++++++++++++++++++++++++++++++++++++-- src/menu/MenuItem.tsx | 28 ++++++++++++-- src/menu/_example/base.tsx | 9 ++++- src/menu/_util/constant.ts | 1 + src/menu/defaultProps.ts | 9 +++++ 6 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 src/menu/_util/constant.ts create mode 100644 src/menu/defaultProps.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 3da9bc3..589a406 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -30,6 +30,8 @@ "actived", "borderless", "Cascader", + "classname", + "clsx", "Popconfirm", "Swiper", "tdesign" diff --git a/src/menu/Menu.tsx b/src/menu/Menu.tsx index 8fa7324..230eae2 100644 --- a/src/menu/Menu.tsx +++ b/src/menu/Menu.tsx @@ -1,9 +1,44 @@ -import { Component, tag } from 'omi'; +import { Component, ComponentChildren, OmiDOMAttributes, tag } from 'omi'; import { StyledProps } from '../common'; +import { classname, getClassPrefix } from '../utils'; +import { DEFAULT_MENU_WIDTH } from './_util/constant'; +import { menuDefaultProps } from './defaultProps'; import { TdMenuProps } from './type'; -export interface MenuProps extends TdMenuProps, StyledProps {} +export interface MenuProps extends TdMenuProps, StyledProps, OmiDOMAttributes {} + +/** + * 将Component的children转换为数组 + * @param children ComponentChildren | undefined + * @returns ComponentChildren[] + */ +export function getChildrenArray(children?: ComponentChildren) { + if (!children) { + return []; + } + if (Array.isArray(children)) { + return children; + } + return [children]; +} + +/** + * 判断是否某个name的slot + * @param name string + * @param children ComponentChildren | undefined + * @returns boolean + */ +export function hasSlot(name: string, children?: ComponentChildren) { + const childrenArray = getChildrenArray(children); + + return childrenArray.some((child) => { + if (typeof child === 'object' && 'attributes' in child) { + return child.attributes?.slot === name; + } + return false; + }); +} @tag('t-menu') export default class Menu extends Component { @@ -12,6 +47,42 @@ export default class Menu extends Component { static defaultProps = {}; render() { - return
menus
; + const { className, style, width, collapsed } = { + ...menuDefaultProps, + ...this.props, + }; + + const classPrefix = getClassPrefix(); + const menuWidthArr = Array.isArray(width) ? width : [width, DEFAULT_MENU_WIDTH[1]]; + + const hasLogo = hasSlot('logo', this.props.children); + const hasOperations = hasSlot('operations', this.props.children); + + const children = getChildrenArray(this.props.children).filter((item) => item.nodeName === 't-menu-item'); + + return ( +
+
+ {hasLogo && ( +
+ + + +
+ )} +
    {children}
+ {hasOperations && ( +
+ +
+ )} +
+
+ ); } } diff --git a/src/menu/MenuItem.tsx b/src/menu/MenuItem.tsx index ca26360..1ad0644 100644 --- a/src/menu/MenuItem.tsx +++ b/src/menu/MenuItem.tsx @@ -1,17 +1,37 @@ import { Component, tag } from 'omi'; import { StyledProps } from '../common'; +import { classname, getClassPrefix } from '../utils'; import { TdMenuItemProps } from './type'; export interface MenuItemProps extends TdMenuItemProps, StyledProps {} @tag('t-menu-item') export default class Menu extends Component { - static css = []; - - static defaultProps = {}; + static isLightDOM = true; render() { - return
menu item
; + const { label, icon, className, disabled, href, target } = this.props; + + const classPrefix = getClassPrefix(); + + this.className = classname(`${classPrefix}-menu__item`, className, { + [`${classPrefix}-is-disabled`]: disabled, + // [`${classPrefix}-is-active`]: value === active, + [`${classPrefix}-menu__item--plain`]: !icon, + }); + + return ( + <> + {icon} + {href ? ( + + {label} + + ) : ( + {label} + )} + + ); } } diff --git a/src/menu/_example/base.tsx b/src/menu/_example/base.tsx index bcd51f6..1dca07e 100644 --- a/src/menu/_example/base.tsx +++ b/src/menu/_example/base.tsx @@ -3,8 +3,13 @@ import 'tdesign-web-components/menu'; export default function Menu() { return (
- - + + LOGO + + + + {/* operations */} +
); } diff --git a/src/menu/_util/constant.ts b/src/menu/_util/constant.ts new file mode 100644 index 0000000..69b6687 --- /dev/null +++ b/src/menu/_util/constant.ts @@ -0,0 +1 @@ +export const DEFAULT_MENU_WIDTH = [232, 64]; diff --git a/src/menu/defaultProps.ts b/src/menu/defaultProps.ts new file mode 100644 index 0000000..3d3ab0d --- /dev/null +++ b/src/menu/defaultProps.ts @@ -0,0 +1,9 @@ +import { TdMenuItemProps, TdMenuProps } from './type'; + +export const menuDefaultProps: TdMenuProps = { + collapsed: false, + expandMutex: false, + width: '232px', +}; + +export const menuItemDefaultProps: TdMenuItemProps = {}; From a89d5d54d106db0cd996e0ed1824d3fa8313c514 Mon Sep 17 00:00:00 2001 From: ming680 <827668037@qq.com> Date: Fri, 21 Jun 2024 18:02:54 +0800 Subject: [PATCH 3/9] feat(menu): util component --- src/_util/component.ts | 33 +++++++++++++++++++++++++++++++++ src/menu/Menu.tsx | 37 +++---------------------------------- src/menu/MenuItem.tsx | 2 +- 3 files changed, 37 insertions(+), 35 deletions(-) create mode 100644 src/_util/component.ts diff --git a/src/_util/component.ts b/src/_util/component.ts new file mode 100644 index 0000000..e9ba510 --- /dev/null +++ b/src/_util/component.ts @@ -0,0 +1,33 @@ +import { ComponentChildren } from 'omi'; + +/** + * 将Component的children转换为数组 + * @param children ComponentChildren | undefined + * @returns ComponentChildren[] + */ +export function getChildrenArray(children?: ComponentChildren) { + if (!children) { + return []; + } + if (Array.isArray(children)) { + return children; + } + return [children]; +} + +/** + * 判断是否某个name的slot + * @param name string + * @param children ComponentChildren | undefined + * @returns boolean + */ +export function hasSlot(name: string, children?: ComponentChildren) { + const childrenArray = getChildrenArray(children); + + return childrenArray.some((child) => { + if (typeof child === 'object' && 'attributes' in child) { + return child.attributes?.slot === name; + } + return false; + }); +} diff --git a/src/menu/Menu.tsx b/src/menu/Menu.tsx index 230eae2..a4f6c32 100644 --- a/src/menu/Menu.tsx +++ b/src/menu/Menu.tsx @@ -1,45 +1,14 @@ -import { Component, ComponentChildren, OmiDOMAttributes, tag } from 'omi'; +import { Component, OmiDOMAttributes, tag } from 'omi'; +import classname, { getClassPrefix } from '../_util/classname'; +import { getChildrenArray, hasSlot } from '../_util/component'; import { StyledProps } from '../common'; -import { classname, getClassPrefix } from '../utils'; import { DEFAULT_MENU_WIDTH } from './_util/constant'; import { menuDefaultProps } from './defaultProps'; import { TdMenuProps } from './type'; export interface MenuProps extends TdMenuProps, StyledProps, OmiDOMAttributes {} -/** - * 将Component的children转换为数组 - * @param children ComponentChildren | undefined - * @returns ComponentChildren[] - */ -export function getChildrenArray(children?: ComponentChildren) { - if (!children) { - return []; - } - if (Array.isArray(children)) { - return children; - } - return [children]; -} - -/** - * 判断是否某个name的slot - * @param name string - * @param children ComponentChildren | undefined - * @returns boolean - */ -export function hasSlot(name: string, children?: ComponentChildren) { - const childrenArray = getChildrenArray(children); - - return childrenArray.some((child) => { - if (typeof child === 'object' && 'attributes' in child) { - return child.attributes?.slot === name; - } - return false; - }); -} - @tag('t-menu') export default class Menu extends Component { static css = []; diff --git a/src/menu/MenuItem.tsx b/src/menu/MenuItem.tsx index 1ad0644..4a9e884 100644 --- a/src/menu/MenuItem.tsx +++ b/src/menu/MenuItem.tsx @@ -1,7 +1,7 @@ import { Component, tag } from 'omi'; +import classname, { getClassPrefix } from '../_util/classname'; import { StyledProps } from '../common'; -import { classname, getClassPrefix } from '../utils'; import { TdMenuItemProps } from './type'; export interface MenuItemProps extends TdMenuItemProps, StyledProps {} From 9c6b22b447bfb9d45d18ebc835f02ee697f15de3 Mon Sep 17 00:00:00 2001 From: ming680 <827668037@qq.com> Date: Wed, 26 Jun 2024 11:51:13 +0800 Subject: [PATCH 4/9] feat(menu): lightDom --- src/_util/lightDom.ts | 121 +++++++++++++++++++++++++++++++++++++ src/index.ts | 1 + src/menu/Menu.tsx | 2 + src/menu/MenuItem.tsx | 4 ++ src/menu/_example/base.tsx | 6 +- 5 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 src/_util/lightDom.ts diff --git a/src/_util/lightDom.ts b/src/_util/lightDom.ts new file mode 100644 index 0000000..dccbb27 --- /dev/null +++ b/src/_util/lightDom.ts @@ -0,0 +1,121 @@ +import { Component, define } from 'omi'; + +import { TNode } from '../common'; +import parseTNode from './parseTNode'; + +const createStyleSheet = (style: string) => { + const styleSheet = new CSSStyleSheet(); + styleSheet.replaceSync(style); + + Object.defineProperty(styleSheet, 'styleStr', { + get() { + return style; + }, + }); + + return styleSheet; +}; + +type CSSItem = { default: string } | string; + +type ComponentConstructor = { + new (): Component; + is: 'Component'; + isLightDOM: boolean; + css: CSSItem | CSSItem[]; +}; + +const getCssList = (css: ComponentConstructor['css']): string[] => { + if (Array.isArray(css)) { + return css.map((item) => getCssList(item)).flat(); + } + if (typeof css === 'object' && typeof css.default === 'string') { + return [css.default]; + } + if (typeof css === 'string') { + return [css]; + } + return []; +}; + +const findParentRenderRoot = (ele): Document | ShadowRoot => { + if (ele.shadowRoot && ele.renderRoot && ele.renderRoot.adoptedStyleSheets) { + return ele.renderRoot; + } + if (ele.parentElement) { + return findParentRenderRoot(ele.parentElement); + } + return document; +}; + +const lightDomCtorCache: Map = new Map(); + +/** + * 继承 nodeCtor 构建 lightDomCtor + * @param nodeCtor WeElementConstructor + * @returns WeElementConstructor + */ +const buildLightDomCtor = (nodeCtor: ComponentConstructor) => { + const cacheCtor = lightDomCtorCache.get(nodeCtor); + if (cacheCtor) { + return cacheCtor; + } + + class TNodeLightDom extends nodeCtor { + static isLightDOM = true; + + /** + * 处理原 ShadowRoot 的样式表 + * 合并到上层的 ShadowRoot | document 样式表中 + */ + beforeRender() { + const parentElement = findParentRenderRoot(this); + + const cssList = getCssList(nodeCtor.css); + cssList.forEach((style) => { + const preStyleSheet = parentElement.adoptedStyleSheets.find((item) => (item as any).styleStr === style); + if (preStyleSheet) { + return; + } + + const styleSheet = createStyleSheet(style); + parentElement.adoptedStyleSheets = [...parentElement.adoptedStyleSheets, styleSheet]; + }); + } + } + + lightDomCtorCache.set(nodeCtor, TNodeLightDom); + return TNodeLightDom; +}; + +/** + * 转换为 lightDom Node + * @param node TNode + * @returns TNode + */ +export const convertToLightDomNode = (node: TNode) => { + const tNode = parseTNode(node); + if (!(typeof tNode === 'object' && 'nodeName' in tNode && typeof tNode.nodeName === 'string')) { + return tNode; + } + + // 找到之前注册的组件 + const nodeCtor = customElements.get(tNode.nodeName) as ComponentConstructor; + if (!(nodeCtor && nodeCtor.is === 'Component' && !nodeCtor.isLightDOM)) { + return tNode; + } + + // 构建 lightDom 组件 + const lightDomCtor = buildLightDomCtor(nodeCtor); + + // 注册新的组件 + const lightDomNodeName = `${tNode.nodeName}-light-dom`; + if (!customElements.get(lightDomNodeName)) { + define(lightDomNodeName, lightDomCtor); + } + + return { + ...tNode, + nodeName: lightDomNodeName, + }; +}; diff --git a/src/index.ts b/src/index.ts index 3fe8e78..9e1f238 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ export * from './divider'; export * from './icon'; export * from './image'; export * from './input'; +export * from './menu'; export * from './popup'; export * from './space'; export * from './switch'; diff --git a/src/menu/Menu.tsx b/src/menu/Menu.tsx index a4f6c32..62c9344 100644 --- a/src/menu/Menu.tsx +++ b/src/menu/Menu.tsx @@ -44,7 +44,9 @@ export default class Menu extends Component { )} +
    {children}
+ {hasOperations && (
diff --git a/src/menu/MenuItem.tsx b/src/menu/MenuItem.tsx index 4a9e884..e2d972f 100644 --- a/src/menu/MenuItem.tsx +++ b/src/menu/MenuItem.tsx @@ -1,6 +1,7 @@ import { Component, tag } from 'omi'; import classname, { getClassPrefix } from '../_util/classname'; +import { convertToLightDomNode } from '../_util/lightDom'; import { StyledProps } from '../common'; import { TdMenuItemProps } from './type'; @@ -21,9 +22,12 @@ export default class Menu extends Component { [`${classPrefix}-menu__item--plain`]: !icon, }); + const lightIcon = convertToLightDomNode(icon); + return ( <> {icon} + {lightIcon} {href ? ( {label} diff --git a/src/menu/_example/base.tsx b/src/menu/_example/base.tsx index 1dca07e..ae47d70 100644 --- a/src/menu/_example/base.tsx +++ b/src/menu/_example/base.tsx @@ -1,3 +1,4 @@ +import 'tdesign-web-components/icon'; import 'tdesign-web-components/menu'; export default function Menu() { @@ -5,10 +6,7 @@ export default function Menu() {
LOGO - - - - {/* operations */} + }>
); From f5972d708cceb8ea42eeaf26fbb4708a212f3987 Mon Sep 17 00:00:00 2001 From: ming680 <827668037@qq.com> Date: Mon, 8 Jul 2024 01:02:22 +0800 Subject: [PATCH 5/9] feat(menu): menu --- src/menu/Menu.tsx | 20 +++++++++++--- src/menu/MenuItem.tsx | 42 ++++++++++++++++++++++++----- src/menu/README.md | 20 ++++++++++++-- src/menu/_example/base.tsx | 13 --------- src/menu/_example/closable-side.tsx | 40 +++++++++++++++++++++++++++ src/menu/type.ts | 13 ++------- 6 files changed, 112 insertions(+), 36 deletions(-) delete mode 100644 src/menu/_example/base.tsx create mode 100644 src/menu/_example/closable-side.tsx diff --git a/src/menu/Menu.tsx b/src/menu/Menu.tsx index 62c9344..50d44f6 100644 --- a/src/menu/Menu.tsx +++ b/src/menu/Menu.tsx @@ -1,11 +1,11 @@ -import { Component, OmiDOMAttributes, tag } from 'omi'; +import { bind, Component, OmiDOMAttributes, signal, tag } from 'omi'; import classname, { getClassPrefix } from '../_util/classname'; import { getChildrenArray, hasSlot } from '../_util/component'; import { StyledProps } from '../common'; import { DEFAULT_MENU_WIDTH } from './_util/constant'; import { menuDefaultProps } from './defaultProps'; -import { TdMenuProps } from './type'; +import { MenuValue, TdMenuProps } from './type'; export interface MenuProps extends TdMenuProps, StyledProps, OmiDOMAttributes {} @@ -15,12 +15,26 @@ export default class Menu extends Component { static defaultProps = {}; + active = signal(''); + + provide = { + active: this.active, + onChange: this.handleChange, + }; + + @bind + handleChange(value: MenuValue) { + this.fire('change', value); + } + render() { - const { className, style, width, collapsed } = { + const { className, style, width, collapsed, value } = { ...menuDefaultProps, ...this.props, }; + this.active.value = value; + const classPrefix = getClassPrefix(); const menuWidthArr = Array.isArray(width) ? width : [width, DEFAULT_MENU_WIDTH[1]]; diff --git a/src/menu/MenuItem.tsx b/src/menu/MenuItem.tsx index e2d972f..21b109f 100644 --- a/src/menu/MenuItem.tsx +++ b/src/menu/MenuItem.tsx @@ -1,4 +1,4 @@ -import { Component, tag } from 'omi'; +import { bind, Component, tag } from 'omi'; import classname, { getClassPrefix } from '../_util/classname'; import { convertToLightDomNode } from '../_util/lightDom'; @@ -8,25 +8,53 @@ import { TdMenuItemProps } from './type'; export interface MenuItemProps extends TdMenuItemProps, StyledProps {} @tag('t-menu-item') -export default class Menu extends Component { +export default class MenuItem extends Component { static isLightDOM = true; + inject = ['active', 'onChange']; + + constructor() { + super(); + this.addEventListener('click', this.handleClick); + } + + @bind + handleClick(evt: MouseEvent) { + if (!(evt instanceof MouseEvent)) { + // 防止死循环 下面还会 fire('click') 又触发了当前函数的执行 + return; + } + // 阻止自定义dom上绑定的onClick原生事件 + evt.stopImmediatePropagation(); + if (this.props.disabled) { + return; + } + this.fire('click', { + context: this, + value: this.props.value, + }); + this.injection.onChange?.(this.props.value); + } + + uninstalled() { + this.removeEventListener('click', this.handleClick); + } + render() { - const { label, icon, className, disabled, href, target } = this.props; + const { label, icon, className, disabled, href, target, value } = this.props; const classPrefix = getClassPrefix(); + const lightIcon = convertToLightDomNode(icon); + this.className = classname(`${classPrefix}-menu__item`, className, { [`${classPrefix}-is-disabled`]: disabled, - // [`${classPrefix}-is-active`]: value === active, + [`${classPrefix}-is-active`]: value === this.injection.active.value, [`${classPrefix}-menu__item--plain`]: !icon, }); - const lightIcon = convertToLightDomNode(icon); - return ( <> - {icon} {lightIcon} {href ? (
diff --git a/src/menu/README.md b/src/menu/README.md index 608a873..c669eed 100644 --- a/src/menu/README.md +++ b/src/menu/README.md @@ -6,10 +6,11 @@ usage: { title: '', description: '' } spline: base --- -### 基础使用 +### 可收起的侧边导航 -{{ base }} +在侧边导航上提供收起按钮,点击后可以将侧边栏最小化,常见于带有图标的侧边导航。 +{{ closable-side }} ## API @@ -17,8 +18,23 @@ spline: base 名称 | 类型 | 默认值 | 说明 | 必传 -- | -- | -- | -- | -- +className | String | - | 类名 | N +collapsed | Boolean | false | 是否收起菜单 | N +logo | TElement | - | 站点 LOGO。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +operations | TElement | - | 导航操作区域。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +value | String / Number | - | 激活菜单项。TS 类型:`MenuValue` `type MenuValue = string \| number`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/menu/type.ts) | N +width | String / Number / Array | '232px' | 菜单宽度。值类型为数组时,分别表示菜单展开和折叠的宽度。[ 展开时的宽度, 折叠时的宽度 ],示例:['200px', '80px']。TS 类型:`string \| number \| Array` | N +onChange | Function | | TS 类型:`(evt: CustomEvent) => void`
激活菜单项发生变化时触发 | N + ### MenuItem Props + 名称 | 类型 | 默认值 | 说明 | 必传 -- | -- | -- | -- | -- +disabled | Boolean | - | 是否禁用菜单项展开/收起/跳转等功能 | N +href | String | - | 跳转链接 | N +icon | TElement | - | 图标。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +target | String | - | 链接或路由跳转方式。可选项:_blank/_self/_parent/_top | N +value | String / Number | - | 菜单项唯一标识。TS 类型:`MenuValue` | N +onClick | Function | | TS 类型:`(evt: CustomEvent<{ e: MouseEvent, value: MenuValue }>) => void`
点击时触发 | N diff --git a/src/menu/_example/base.tsx b/src/menu/_example/base.tsx deleted file mode 100644 index ae47d70..0000000 --- a/src/menu/_example/base.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import 'tdesign-web-components/icon'; -import 'tdesign-web-components/menu'; - -export default function Menu() { - return ( -
- - LOGO - }> - -
- ); -} diff --git a/src/menu/_example/closable-side.tsx b/src/menu/_example/closable-side.tsx new file mode 100644 index 0000000..7060ba2 --- /dev/null +++ b/src/menu/_example/closable-side.tsx @@ -0,0 +1,40 @@ +import 'tdesign-web-components/button'; +import 'tdesign-web-components/icon'; +import 'tdesign-web-components/menu'; + +import { Component, signal } from 'omi'; + +export default class CloseableSide extends Component { + collapsed = signal(false); + + active = signal('0'); + + render() { + return ( + { + this.active.value = evt.detail; + }} + > + LOGO + } /> + } /> + } /> + } /> + } /> + } /> + } + onClick={() => { + this.collapsed.value = !this.collapsed.value; + }} + /> + + ); + } +} diff --git a/src/menu/type.ts b/src/menu/type.ts index 2427663..d84574b 100644 --- a/src/menu/type.ts +++ b/src/menu/type.ts @@ -6,11 +6,6 @@ export interface TdMenuProps { * @default false */ collapsed?: boolean; - /** - * 同级别互斥展开 - * @default false - */ - expandMutex?: boolean; /** * 激活菜单项 */ @@ -23,11 +18,7 @@ export interface TdMenuProps { /** * 激活菜单项发生变化时触发 */ - onChange?: (value: MenuValue) => void; - /** - * 展开的菜单项发生变化时触发 - */ - onExpand?: (value: Array) => void; + onChange?: (value: CustomEvent) => void; } export interface TdMenuItemProps { @@ -59,7 +50,7 @@ export interface TdMenuItemProps { /** * 点击时触发 */ - onClick?: (context: { e: HTMLElement; value: MenuValue }) => void; + onClick?: (event: CustomEvent<{ e: HTMLElement; value: MenuValue }>) => void; } export type MenuValue = string | number; From 8366f998b4b2661e356996e19e3fdd242db4ffe2 Mon Sep 17 00:00:00 2001 From: ming680 <827668037@qq.com> Date: Mon, 8 Jul 2024 01:03:51 +0800 Subject: [PATCH 6/9] feat(menu): menu --- src/menu/defaultProps.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/menu/defaultProps.ts b/src/menu/defaultProps.ts index 3d3ab0d..04d6f32 100644 --- a/src/menu/defaultProps.ts +++ b/src/menu/defaultProps.ts @@ -2,7 +2,6 @@ import { TdMenuItemProps, TdMenuProps } from './type'; export const menuDefaultProps: TdMenuProps = { collapsed: false, - expandMutex: false, width: '232px', }; From 7bf4b923a88e1f5b38b752addd47287342c3b37f Mon Sep 17 00:00:00 2001 From: ming680 <827668037@qq.com> Date: Mon, 8 Jul 2024 11:56:18 +0800 Subject: [PATCH 7/9] feat(menu): menu item style --- src/_util/lightDom.ts | 10 +--------- src/menu/Menu.tsx | 5 ++++- src/menu/MenuItem.tsx | 8 +++++++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/_util/lightDom.ts b/src/_util/lightDom.ts index dccbb27..a4dcc17 100644 --- a/src/_util/lightDom.ts +++ b/src/_util/lightDom.ts @@ -38,15 +38,7 @@ const getCssList = (css: ComponentConstructor['css']): string[] => { return []; }; -const findParentRenderRoot = (ele): Document | ShadowRoot => { - if (ele.shadowRoot && ele.renderRoot && ele.renderRoot.adoptedStyleSheets) { - return ele.renderRoot; - } - if (ele.parentElement) { - return findParentRenderRoot(ele.parentElement); - } - return document; -}; +const findParentRenderRoot = (ele): Document | ShadowRoot => ele.getRootNode() || document; const lightDomCtorCache: Map = new Map(); diff --git a/src/menu/Menu.tsx b/src/menu/Menu.tsx index 50d44f6..32bbb9d 100644 --- a/src/menu/Menu.tsx +++ b/src/menu/Menu.tsx @@ -2,6 +2,7 @@ import { bind, Component, OmiDOMAttributes, signal, tag } from 'omi'; import classname, { getClassPrefix } from '../_util/classname'; import { getChildrenArray, hasSlot } from '../_util/component'; +import { convertToLightDomNode } from '../_util/lightDom'; import { StyledProps } from '../common'; import { DEFAULT_MENU_WIDTH } from './_util/constant'; import { menuDefaultProps } from './defaultProps'; @@ -41,7 +42,9 @@ export default class Menu extends Component { const hasLogo = hasSlot('logo', this.props.children); const hasOperations = hasSlot('operations', this.props.children); - const children = getChildrenArray(this.props.children).filter((item) => item.nodeName === 't-menu-item'); + const children = getChildrenArray(this.props.children) + .filter((item) => item.nodeName === 't-menu-item') + .map(convertToLightDomNode); return (
{ - static isLightDOM = true; + static css = ` + .${getClassPrefix()}-menu__item--has-icon .${getClassPrefix()}-menu__content, + .${getClassPrefix()}-menu__item--has-icon .${getClassPrefix()}-menu__item-link { + margin-left: var(--td-comp-margin-s); + } + `; inject = ['active', 'onChange']; @@ -51,6 +56,7 @@ export default class MenuItem extends Component { [`${classPrefix}-is-disabled`]: disabled, [`${classPrefix}-is-active`]: value === this.injection.active.value, [`${classPrefix}-menu__item--plain`]: !icon, + [`${classPrefix}-menu__item--has-icon`]: !!lightIcon, }); return ( From 29036b4bc31a3355e128d92554dd4d1286510cde Mon Sep 17 00:00:00 2001 From: ming680 <827668037@qq.com> Date: Mon, 8 Jul 2024 18:43:35 +0800 Subject: [PATCH 8/9] feat(menu): propTypes && defaultProps --- src/menu/Menu.tsx | 18 ++++++++++++------ src/menu/MenuItem.tsx | 10 ++++++++++ src/menu/defaultProps.ts | 8 -------- 3 files changed, 22 insertions(+), 14 deletions(-) delete mode 100644 src/menu/defaultProps.ts diff --git a/src/menu/Menu.tsx b/src/menu/Menu.tsx index 32bbb9d..6ef6ad2 100644 --- a/src/menu/Menu.tsx +++ b/src/menu/Menu.tsx @@ -5,7 +5,6 @@ import { getChildrenArray, hasSlot } from '../_util/component'; import { convertToLightDomNode } from '../_util/lightDom'; import { StyledProps } from '../common'; import { DEFAULT_MENU_WIDTH } from './_util/constant'; -import { menuDefaultProps } from './defaultProps'; import { MenuValue, TdMenuProps } from './type'; export interface MenuProps extends TdMenuProps, StyledProps, OmiDOMAttributes {} @@ -14,7 +13,17 @@ export interface MenuProps extends TdMenuProps, StyledProps, OmiDOMAttributes {} export default class Menu extends Component { static css = []; - static defaultProps = {}; + static defaultProps: TdMenuProps = { + collapsed: false, + width: '232px', + }; + + static propTypes = { + collapsed: Boolean, + value: [String, Number], + width: [String, Number, Array], + onChange: Function, + }; active = signal(''); @@ -29,10 +38,7 @@ export default class Menu extends Component { } render() { - const { className, style, width, collapsed, value } = { - ...menuDefaultProps, - ...this.props, - }; + const { className, style, width, collapsed, value } = this.props; this.active.value = value; diff --git a/src/menu/MenuItem.tsx b/src/menu/MenuItem.tsx index ff34e30..cf69b34 100644 --- a/src/menu/MenuItem.tsx +++ b/src/menu/MenuItem.tsx @@ -16,6 +16,16 @@ export default class MenuItem extends Component { } `; + static propTypes = { + label: Object, + disabled: Boolean, + href: String, + icon: Object, + target: String, + value: [String, Number], + onClick: Function, + }; + inject = ['active', 'onChange']; constructor() { diff --git a/src/menu/defaultProps.ts b/src/menu/defaultProps.ts deleted file mode 100644 index 04d6f32..0000000 --- a/src/menu/defaultProps.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TdMenuItemProps, TdMenuProps } from './type'; - -export const menuDefaultProps: TdMenuProps = { - collapsed: false, - width: '232px', -}; - -export const menuItemDefaultProps: TdMenuItemProps = {}; From a6123e7dab321a68816afde5e0013541a5f7c582 Mon Sep 17 00:00:00 2001 From: ming680 <827668037@qq.com> Date: Mon, 8 Jul 2024 22:26:57 +0800 Subject: [PATCH 9/9] =?UTF-8?q?feat(menu):=20=E6=94=B6=E8=B5=B7=E5=90=8E?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20tooltip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_common | 2 +- src/menu/Menu.tsx | 9 +++++++-- src/menu/MenuItem.tsx | 28 ++++++++++++++++++++++++++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/_common b/src/_common index a3e26cf..26299fe 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit a3e26cf8ce5053ac790b6dcdaee7190cc39a4281 +Subproject commit 26299fef514131890f78fbb48b531fdafb66c5b8 diff --git a/src/menu/Menu.tsx b/src/menu/Menu.tsx index 6ef6ad2..76340b0 100644 --- a/src/menu/Menu.tsx +++ b/src/menu/Menu.tsx @@ -25,9 +25,13 @@ export default class Menu extends Component { onChange: Function, }; - active = signal(''); + private active = signal(''); + + // 这里不能声明 collapsed 会被外部覆盖 + private menuCollapsed = signal(true); provide = { + collapsed: this.menuCollapsed, active: this.active, onChange: this.handleChange, }; @@ -38,9 +42,10 @@ export default class Menu extends Component { } render() { - const { className, style, width, collapsed, value } = this.props; + const { className, style, width, value, collapsed } = this.props; this.active.value = value; + this.menuCollapsed.value = collapsed; const classPrefix = getClassPrefix(); const menuWidthArr = Array.isArray(width) ? width : [width, DEFAULT_MENU_WIDTH[1]]; diff --git a/src/menu/MenuItem.tsx b/src/menu/MenuItem.tsx index cf69b34..f799132 100644 --- a/src/menu/MenuItem.tsx +++ b/src/menu/MenuItem.tsx @@ -1,3 +1,5 @@ +import 'tdesign-web-components/tooltip'; + import { bind, Component, tag } from 'omi'; import classname, { getClassPrefix } from '../_util/classname'; @@ -14,6 +16,18 @@ export default class MenuItem extends Component { .${getClassPrefix()}-menu__item--has-icon .${getClassPrefix()}-menu__item-link { margin-left: var(--td-comp-margin-s); } + + .${getClassPrefix()}-menu__item > ${getClassPrefix()}-tooltip { + position: absolute; + inset: 0; + } + .${getClassPrefix()}-menu__item .${getClassPrefix()}-menu__item-tooltip-inner { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } `; static propTypes = { @@ -26,7 +40,7 @@ export default class MenuItem extends Component { onClick: Function, }; - inject = ['active', 'onChange']; + inject = ['active', 'onChange', 'collapsed']; constructor() { super(); @@ -69,7 +83,7 @@ export default class MenuItem extends Component { [`${classPrefix}-menu__item--has-icon`]: !!lightIcon, }); - return ( + const content = ( <> {lightIcon} {href ? ( @@ -81,5 +95,15 @@ export default class MenuItem extends Component { )} ); + + if (this.injection.collapsed.value && !this.props.disabled) { + return ( + +
{content}
+
+ ); + } + + return content; } }