Skip to content

Commit

Permalink
Merge pull request #76 from ming680/feature/menu
Browse files Browse the repository at this point in the history
Feature/menu
  • Loading branch information
duenyang authored Jul 9, 2024
2 parents 666f3cb + a6123e7 commit 6ca38c8
Show file tree
Hide file tree
Showing 13 changed files with 394 additions and 10 deletions.
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"actived",
"borderless",
"Cascader",
"classname",
"clsx",
"Popconfirm",
"Swiper",
"tdesign"
Expand Down
2 changes: 1 addition & 1 deletion site/sidebar.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,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 面包屑',
Expand Down
33 changes: 33 additions & 0 deletions src/_util/component.ts
Original file line number Diff line number Diff line change
@@ -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;
});
}
10 changes: 1 addition & 9 deletions src/_util/lightDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ComponentConstructor, ComponentConstructor> = new Map();

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './image';
export * from './input';
export * from './input-number';
export * from './loading';
export * from './menu';
export * from './popup';
export * from './slider';
export * from './space';
Expand Down
87 changes: 87 additions & 0 deletions src/menu/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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 { MenuValue, TdMenuProps } from './type';

export interface MenuProps extends TdMenuProps, StyledProps, OmiDOMAttributes {}

@tag('t-menu')
export default class Menu extends Component<MenuProps> {
static css = [];

static defaultProps: TdMenuProps = {
collapsed: false,
width: '232px',
};

static propTypes = {
collapsed: Boolean,
value: [String, Number],
width: [String, Number, Array],
onChange: Function,
};

private active = signal<MenuValue>('');

// 这里不能声明 collapsed 会被外部覆盖
private menuCollapsed = signal(true);

provide = {
collapsed: this.menuCollapsed,
active: this.active,
onChange: this.handleChange,
};

@bind
handleChange(value: MenuValue) {
this.fire('change', value);
}

render() {
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]];

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')
.map(convertToLightDomNode);

return (
<div
className={classname(`${classPrefix}-default-menu`, className, {
[`${classPrefix}-is-collapsed`]: collapsed,
})}
style={{ width: collapsed ? menuWidthArr[1] : menuWidthArr[0], ...style }}
>
<div className={`${classPrefix}-default-menu__inner`}>
{hasLogo && (
<div className={`${classPrefix}-menu__logo`}>
<span>
<slot name="logo" />
</span>
</div>
)}

<ul className={classname(`${classPrefix}-menu`, `${classPrefix}-menu--scroll`)}>{children}</ul>

{hasOperations && (
<div className={`${classPrefix}-menu__operations`}>
<slot name="operations" />
</div>
)}
</div>
</div>
);
}
}
109 changes: 109 additions & 0 deletions src/menu/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import 'tdesign-web-components/tooltip';

import { bind, Component, tag } from 'omi';

import classname, { getClassPrefix } from '../_util/classname';
import { convertToLightDomNode } from '../_util/lightDom';
import { StyledProps } from '../common';
import { TdMenuItemProps } from './type';

export interface MenuItemProps extends TdMenuItemProps, StyledProps {}

@tag('t-menu-item')
export default class MenuItem extends Component<MenuItemProps> {
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);
}
.${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 = {
label: Object,
disabled: Boolean,
href: String,
icon: Object,
target: String,
value: [String, Number],
onClick: Function,
};

inject = ['active', 'onChange', 'collapsed'];

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, 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 === this.injection.active.value,
[`${classPrefix}-menu__item--plain`]: !icon,
[`${classPrefix}-menu__item--has-icon`]: !!lightIcon,
});

const content = (
<>
{lightIcon}
{href ? (
<a href={href} target={target} className={classname(`${classPrefix}-menu__item-link`)}>
<span className={`${classPrefix}-menu__content`}>{label}</span>
</a>
) : (
<span className={`${classPrefix}-menu__content`}>{label}</span>
)}
</>
);

if (this.injection.collapsed.value && !this.props.disabled) {
return (
<t-tooltip content={label} placement="right">
<div className={`${classPrefix}-menu__item-tooltip-inner`}>{content}</div>
</t-tooltip>
);
}

return content;
}
}
40 changes: 40 additions & 0 deletions src/menu/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: Menu 导航菜单
description: 用于承载网站的架构,并提供跳转的菜单列表。
isComponent: true
usage: { title: '', description: '' }
spline: base
---

### 可收起的侧边导航

在侧边导航上提供收起按钮,点击后可以将侧边栏最小化,常见于带有图标的侧边导航。

{{ closable-side }}

## API

### Menu Props

名称 | 类型 | 默认值 | 说明 | 必传
-- | -- | -- | -- | --
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<string \| number>` | N
onChange | Function | | TS 类型:`(evt: CustomEvent<MenuValue>) => void`<br/>激活菜单项发生变化时触发 | 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`<br/>点击时触发 | N
40 changes: 40 additions & 0 deletions src/menu/_example/closable-side.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<t-menu
value={this.active.value}
collapsed={this.collapsed.value}
onChange={(evt) => {
this.active.value = evt.detail;
}}
>
<span slot="logo">LOGO</span>
<t-menu-item label="仪表盘" value="0" icon={<t-icon name="app" />} />
<t-menu-item label="资源列表" value="1" icon={<t-icon name="code" />} />
<t-menu-item label="调度平台" value="2" icon={<t-icon name="file" />} />
<t-menu-item label="精准监控" value="3" icon={<t-icon name="user" />} />
<t-menu-item label="根目录" value="4" icon={<t-icon name="rollback" />} />
<t-menu-item label="消息区" value="5" icon={<t-icon name="mail" />} />
<t-button
slot="operations"
variant="text"
shape="square"
icon={<t-icon name="view-list" />}
onClick={() => {
this.collapsed.value = !this.collapsed.value;
}}
/>
</t-menu>
);
}
}
1 change: 1 addition & 0 deletions src/menu/_util/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_MENU_WIDTH = [232, 64];
13 changes: 13 additions & 0 deletions src/menu/index.ts
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 10 additions & 0 deletions src/menu/style/index.js
Original file line number Diff line number Diff line change
@@ -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);
Loading

0 comments on commit 6ca38c8

Please sign in to comment.