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

feat(breadcrumb): add breadcrumb component #108

Merged
merged 7 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion site/sidebar.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export default [
title: 'Breadcrumb 面包屑',
name: 'breadcrumb',
path: '/components/breadcrumb',
// component: () => import('tdesign-web-components/breadcrumb/README.md'),
component: () => import('tdesign-web-components/breadcrumb/README.md'),
},
{
title: 'BackTop 回到顶部',
Expand Down
6 changes: 6 additions & 0 deletions src/_util/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,9 @@ export function setStyle(style: CSSStyleDeclaration, key: string, value: string
(style as any)[key] = `${value}px`;
}
}

// 用于判断节点内容是否溢出
export const isNodeOverflow = (ele: Element | Element[]): boolean => {
const { clientWidth = 0, scrollWidth = 0 } = ele as Element;
return scrollWidth > clientWidth;
};
49 changes: 49 additions & 0 deletions src/breadcrumb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
title: Breadcrumb 面包屑
description: 显示当前页面在系统层级结构的位置,并能返回之前任意层级的页面。
isComponent: true
usage: { title: '', description: '' }
spline: base
---

### 基础面包屑
适用于广泛的基础用法,系统拥有超过两级以上的层级结构,用于切换向上任意层级的内容。
{{ base }}

### 自定义分隔符的面包屑
通过 separator 属性自定义分隔符,建议用图标而非文本符号。
{{ custom }}

### 使用 options 配置面包屑
使用 `options` 属性配置面包屑内容。
{{ options }}

### 带跳转/点击的面包屑
自定义响应点击事件。

{{ href }}
## API

### Breadcrumb Props

名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
className | String | - | 类名 | N
style | Object | - | 样式,TS 类型:`React.CSSProperties` | N
maxItemWidth | String | undefined | 单项最大宽度,超出后会以省略号形式呈现 | N
options | Array | - | 面包屑项,功能同 BreadcrumbItem。TS 类型:`Array<TdBreadcrumbItemProps>` | N
separator | TNode | - | 自定义分隔符。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N


### BreadcrumbItem Props

名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
className | String | - | 类名 | N
style | Object | - | 样式,TS 类型:`React.CSSProperties` | N
content | TNode | - | 子元素。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N
disabled | Boolean | - | 是否禁用当前项点击 | N
href | String | - | 跳转链接 | N
maxWidth | String | undefined | 最大宽度,超出后会以省略号形式呈现。优先级高于 Breadcrumb 中的 maxItemWidth | N
target | String | _self | 链接或路由跳转方式。可选项:_blank/_self/_parent/_top | N
onClick | Function | | TS 类型:`(e: MouseEvent) => void`<br/>点击时触发 | N
15 changes: 15 additions & 0 deletions src/breadcrumb/_example/base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'tdesign-web-components/breadcrumb';

import { Component } from 'omi';

export default class Breadcrumb extends Component {
render() {
return (
<t-breadcrumb max-item-width={'150'}>
<t-breadcrumb-item>页面一</t-breadcrumb-item>
<t-breadcrumb-item>页面2面包屑文案超长时悬浮显示文案全部信息</t-breadcrumb-item>
<t-breadcrumb-item max-width={'160'}> 面包屑中文案过长时可缩略显示,鼠标hover时显示全部 </t-breadcrumb-item>
</t-breadcrumb>
);
}
}
19 changes: 19 additions & 0 deletions src/breadcrumb/_example/custom.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'tdesign-web-components/breadcrumb';

export default function Breadcrumb() {
return (
<>
<t-breadcrumb>
<t-breadcrumb-item>页面1</t-breadcrumb-item>
<t-breadcrumb-item>页面2</t-breadcrumb-item>
<t-breadcrumb-item>页面3</t-breadcrumb-item>
<span slot="separator">{'>'}</span>
</t-breadcrumb>
<t-breadcrumb separator={'///'}>
<t-breadcrumb-item>页面1</t-breadcrumb-item>
<t-breadcrumb-item>页面2</t-breadcrumb-item>
<t-breadcrumb-item>页面3</t-breadcrumb-item>
</t-breadcrumb>
</>
);
}
31 changes: 31 additions & 0 deletions src/breadcrumb/_example/href.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'tdesign-web-components/breadcrumb';
import 'tdesign-web-components/space';

import { Component, signal } from 'omi';

export default class Breadcrumb extends Component {
count = signal(0);

// 点击事件
onClickItem = () => {
this.count.value += 1;
};

render() {
return (
<t-space direction="vertical">
<t-breadcrumb>
<t-breadcrumb-item href="http://tdesign.tencent.com/" target="_blank">
页面1
</t-breadcrumb-item>
<t-breadcrumb-item href="http://tdesign.tencent.com">页面2</t-breadcrumb-item>
<t-breadcrumb-item href="https://tdesign.tencent.com" disabled>
页面3
</t-breadcrumb-item>
<t-breadcrumb-item onClick={this.onClickItem}>自定义点击</t-breadcrumb-item>
</t-breadcrumb>
<div>点击次数: {this.count.value}</div>
</t-space>
);
}
}
6 changes: 6 additions & 0 deletions src/breadcrumb/_example/options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'tdesign-web-components/breadcrumb';

export default function Breadcrumb() {
const options = [{ content: '页面1' }, { content: '页面2' }, { content: '页面3', href: 'https://github.com/' }];
return <t-breadcrumb max-item-width={'150'} options={options}></t-breadcrumb>;
}
123 changes: 123 additions & 0 deletions src/breadcrumb/breadcrumb-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import 'tdesign-icons-web-components/esm/components/chevron-right';
import '../tooltip';

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

import classname, { getClassPrefix } from '../_util/classname';
import { isNodeOverflow } from '../_util/dom';
import { TNode } from '../common';
import { TdBreadcrumbItemProps } from './type';

interface BreadcrumbItemProps extends TdBreadcrumbItemProps, OmiDOMAttributes {}

interface LocalTBreadcrumb {
separator: TNode | string;
maxItemWidth: string;
}

const localTBreadcrumbOrigin: LocalTBreadcrumb = {
separator: '',
maxItemWidth: undefined,
};

@tag('t-breadcrumb-item')
export default class BreadcrumbItem extends Component<BreadcrumbItemProps> {
static css = [];

static isLightDOM = true;

className = `${getClassPrefix()}-breadcrumb__item`;

static defaultProps = {
href: '',
target: '_self',
isLast: false,
};

static propsType = {
content: [String, Number, Object, Function],
disabled: Boolean,
href: String,
maxWidth: String,
target: String,
onClick: Function,
};

inject = ['tBreadcrumb'];

localTBreadcrumb = localTBreadcrumbOrigin;

isCutOff = false;

install() {
this.injection.tBreadcrumb && (this.localTBreadcrumb = this.injection.tBreadcrumb);
}

adjustCutOff() {
setTimeout(() => {
const breadcrumbText = this.querySelector(`.${getClassPrefix()}-breadcrumb__inner-text`);

if (breadcrumbText?.clientWidth) {
const isCutOff = isNodeOverflow(breadcrumbText);
if (this.isCutOff !== isCutOff) {
this.isCutOff = isCutOff;
this.update();
}
}
});
}

ready(): void {
this.adjustCutOff();
}

updated(): void {
this.adjustCutOff();
}

@bind
handleClick(event: MouseEvent): void {
event.stopImmediatePropagation();
if (!this.props.disabled) {
this.props.onClick?.(event);
}
}

render() {
const { children, className, disabled, href, target, content, maxWidth } = this.props;
const { separator, maxItemWidth } = this.localTBreadcrumb;
const classPrefix = getClassPrefix();
const textClass = [`${classPrefix}-breadcrumb--text-overflow`];
if (disabled) {
textClass.push(`${classPrefix}-is-disabled`);
}
const separatorContent = separator || <t-icon-chevron-right />;
const innerMaxWidth = maxWidth || maxItemWidth || '120';
const maxWithStyle = { 'max-width': `${innerMaxWidth}px` };

const textContent = (
<span class={`${classPrefix}-breadcrumb__inner`} style={maxWithStyle}>
<span class={`${classPrefix}-breadcrumb__inner-text`}>{children || content}</span>
</span>
);

let itemContent;
if (href && !disabled) {
textClass.push(`${classPrefix}-link`);
itemContent = (
<a class={classname(textClass)} href={href} target={target}>
{textContent}
</a>
);
} else {
itemContent = <span class={classname(textClass)}>{textContent}</span>;
}

return (
<div className={classname(this.className, className)} onClick={this.handleClick}>
{this.isCutOff ? <t-tooltip content={children || content}>{itemContent}</t-tooltip> : itemContent}
<span class={`${classPrefix}-breadcrumb__separator`}>{separatorContent}</span>
</div>
);
}
}
52 changes: 52 additions & 0 deletions src/breadcrumb/breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import './breadcrumb-item';

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

import classname, { getClassPrefix } from '../_util/classname';
import { getChildrenArray } from '../_util/component';
import { TdBreadcrumbProps } from './type';

interface BreadcrumbProps extends TdBreadcrumbProps, OmiDOMAttributes {}

@tag('t-breadcrumb')
export default class Breadcrumb extends Component<BreadcrumbProps> {
className = `${getClassPrefix()}-breadcrumb`;

static propTypes = {
maxItemWidth: String,
options: Object,
separator: [String, Number, Object, Function],
};

get contentNodes() {
const { children, options } = this.props;
const childrenArray = getChildrenArray(children);
let content = childrenArray
.filter((child) => child.attributes?.slot !== 'separator' && child.nodeName === 't-breadcrumb-item')
.map((item) => item);

if (options && options.length) {
content = options.map((option) => <t-breadcrumb-item {...option}></t-breadcrumb-item>);
}
return content;
}

provide = {
tBreadcrumb: {
separator: '',
maxItemWidth: '',
},
};

beforeRender(): void {
const { separator, maxItemWidth, children } = this.props;
const separatorSlot = getChildrenArray(children).find((child) => child.attributes?.slot === 'separator');
this.provide.tBreadcrumb.separator = separator || separatorSlot;
this.provide.tBreadcrumb.maxItemWidth = maxItemWidth;
}

render() {
const { className } = this.props;
return <div class={classname(this.className, className)}>{this.contentNodes}</div>;
}
}
11 changes: 11 additions & 0 deletions src/breadcrumb/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import './style/index.js';

import _Breadcrumb from './breadcrumb';
import _BreadcrumbItem from './breadcrumb-item';

export * from './type';

export type { TdBreadcrumbProps, TdBreadcrumbItemProps } from './type.ts';
export const Breadcrumb = _Breadcrumb;
export const BreadcrumbItem = _BreadcrumbItem;
export default Breadcrumb;
10 changes: 10 additions & 0 deletions src/breadcrumb/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/breadcrumb/_index.less';

export const styleSheet = css`
${styles}
`;

globalCSS(styleSheet);
Loading
Loading