Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/menu #76

Merged
merged 13 commits into from
Jul 9, 2024
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 @@ -7,6 +7,7 @@ export * from './icon';
export * from './image';
export * from './input';
export * from './loading';
export * from './menu';
export * from './popup';
export * from './space';
export * from './switch';
Expand Down
76 changes: 76 additions & 0 deletions src/menu/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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';
import { MenuValue, TdMenuProps } from './type';

export interface MenuProps extends TdMenuProps, StyledProps, OmiDOMAttributes {}

@tag('t-menu')
export default class Menu extends Component<MenuProps> {
duenyang marked this conversation as resolved.
Show resolved Hide resolved
static css = [];

static defaultProps = {};
duenyang marked this conversation as resolved.
Show resolved Hide resolved

active = signal<MenuValue>('');

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

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

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

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>
);
}
}
75 changes: 75 additions & 0 deletions src/menu/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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);
}
`;

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, 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,
});

return (
<>
{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>
)}
</>
);
}
}
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];
8 changes: 8 additions & 0 deletions src/menu/defaultProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { TdMenuItemProps, TdMenuProps } from './type';

export const menuDefaultProps: TdMenuProps = {
collapsed: false,
width: '232px',
};

export const menuItemDefaultProps: TdMenuItemProps = {};
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
Loading